diff --git a/.rspec b/.rspec new file mode 100644 index 0000000..c99d2e7 --- /dev/null +++ b/.rspec @@ -0,0 +1 @@ +--require spec_helper diff --git a/Gemfile.lock b/Gemfile.lock index 2a444d3..af59ca7 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,39 +1,41 @@ PATH remote: . specs: - berater (0.0.1) + berater (0.0.2) redis GEM remote: https://rubygems.org/ specs: - activesupport (6.1.1) - concurrent-ruby (~> 1.0, >= 1.0.2) - i18n (>= 1.6, < 2) - minitest (>= 5.1) - tzinfo (~> 2.0) - zeitwerk (~> 2.3) - clockwork (2.0.4) - activesupport - tzinfo - concurrent-ruby (1.1.8) - i18n (1.8.7) - concurrent-ruby (~> 1.0) - minitest (5.14.3) + byebug (11.1.3) + diff-lcs (1.4.4) rake (13.0.3) - redis (4.1.3) - tzinfo (2.0.4) - concurrent-ruby (~> 1.0) - zeitwerk (2.4.2) + redis (4.2.5) + rspec (3.10.0) + rspec-core (~> 3.10.0) + rspec-expectations (~> 3.10.0) + rspec-mocks (~> 3.10.0) + rspec-core (3.10.1) + rspec-support (~> 3.10.0) + rspec-expectations (3.10.1) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.10.0) + rspec-mocks (3.10.1) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.10.0) + rspec-support (3.10.1) + timecop (0.9.2) PLATFORMS ruby + x86_64-darwin-19 DEPENDENCIES berater! - clockwork - minitest + byebug rake + rspec + timecop BUNDLED WITH - 1.17.3 + 2.2.2 diff --git a/Rakefile b/Rakefile index debc11c..8dce7e8 100644 --- a/Rakefile +++ b/Rakefile @@ -1,8 +1,8 @@ -require 'rake/testtask' +require 'rake' +require 'rspec/core/rake_task' -Rake::TestTask.new do |t| - t.libs << 'test' -end +RSpec::Core::RakeTask.new desc "Run tests" -task :default => :test +task :default => :spec +task :test => :spec diff --git a/berater.gemspec b/berater.gemspec index 204342e..66260da 100644 --- a/berater.gemspec +++ b/berater.gemspec @@ -1,7 +1,7 @@ -$LOAD_PATH.unshift 'lib' -package_name = 'berater' -require "#{package_name}" -package = Object.const_get package_name.capitalize +package_name = Dir.glob('*.gemspec')[0].split('.')[0] +require_relative "lib/#{package_name}/version" + +package = Berater Gem::Specification.new do |s| @@ -14,10 +14,12 @@ Gem::Specification.new do |s| s.license = 'MIT' s.files = Dir.glob('lib/**/*') - s.test_files = Dir.glob('test/**/test_*') + s.test_files = Dir.glob('spec/**/*_spec.rb') s.add_runtime_dependency 'redis' - s.add_development_dependency 'clockwork' + + s.add_development_dependency 'byebug' s.add_development_dependency 'rake' - s.add_development_dependency 'minitest' + s.add_development_dependency 'rspec' + s.add_development_dependency 'timecop' end diff --git a/lib/berater.rb b/lib/berater.rb index bb3fac3..bda4cb0 100644 --- a/lib/berater.rb +++ b/lib/berater.rb @@ -1,30 +1,30 @@ -module Berater - VERSION = '0.0.1' +require 'berater/version' - class << self - def init redis - @@redis = redis - end +module Berater + extend self + class LimitExceeded < RuntimeError; end - def incr key, limit, seconds - ts = Time.now.to_i + attr_accessor :redis - # bucket into time slot - rkey = "%s:%s:%d" % [ self.to_s, key, ts - ts % seconds ] + def configure redis + self.redis = redis + end - count = @@redis.multi do - @@redis.incr rkey - @@redis.expire rkey, seconds * 2 - end.first + def incr key, limit, seconds + ts = Time.now.to_i - raise LimitExceeded if count > limit + # bucket into time slot + rkey = "%s:%s:%d" % [ self.to_s, key, ts - ts % seconds ] - count + count, _ = redis.multi do + redis.incr rkey + redis.expire rkey, seconds * 2 end - end + raise LimitExceeded if count > limit - class LimitExceeded < RuntimeError; end + count + end end diff --git a/lib/berater/version.rb b/lib/berater/version.rb new file mode 100644 index 0000000..1b646d9 --- /dev/null +++ b/lib/berater/version.rb @@ -0,0 +1,3 @@ +module Berater + VERSION = '0.0.2' +end diff --git a/spec/berater_spec.rb b/spec/berater_spec.rb new file mode 100644 index 0000000..d27590a --- /dev/null +++ b/spec/berater_spec.rb @@ -0,0 +1,34 @@ +describe Berater do + it { is_expected.to respond_to :configure } + + it 'connects to Redis' do + expect(Berater.redis.ping).to eq 'PONG' + end + + def incr + Berater.incr 'key', 5, 1 + end + + it 'counts' do + expect(incr).to eq 1 + end + + it 'counts many times' do + 5.times do |i| + expect(incr).to eq (i + 1) # i is 0-offset + end + end + + it 'limits excessive calls' do + 5.times do |i| + expect(incr).to eq (i + 1) # i is 0-offset + end + + expect { incr }.to raise_error(Berater::LimitExceeded) + end + + it 'works with symbols' do + count = Berater.incr :key, 5, 1 + expect(count).to eq 1 + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..48c74f4 --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,42 @@ +# $LOAD_PATH.unshift 'lib' + +require 'berater' +require 'byebug' +require 'redis' +require 'timecop' + +class Numeric + def seconds; self; end + alias :second :seconds + + def minutes; self * 60; end + alias :minute :minutes + + def hours; self * 3600; end + alias :hour :hours + + def days; self * 86400; end + alias :day :days +end + + +RSpec.configure do |config| + config.before do + Berater.configure Redis.new + Berater.redis.flushall + end + + # allow 'fit' examples + config.filter_run_when_matching :focus + + config.around(:each) do |example| + # only with blocks + Timecop.safe_mode = true + + # Freeze time by default + Timecop.freeze do + example.run + end + end +end + diff --git a/test/test.rb b/test/test.rb deleted file mode 100644 index f0c52c3..0000000 --- a/test/test.rb +++ /dev/null @@ -1,32 +0,0 @@ -require 'clockwork' -require 'berater' -require 'minitest/autorun' -require 'redis' - - -class BeraterTest < Minitest::Test - - def test_all - redis = Redis.new - Berater.init redis - key = self.to_s - - # make sure Redis is running - assert_nil redis.get(key) - - assert_equal( - Berater.incr(key, 2, 1.day), - 1 - ) - - assert_equal( - Berater.incr(key, 2, 1.day), - 2 - ) - - assert_raises Berater::LimitExceeded do - Berater.incr(key, 2, 1.day) - end - end - -end