berater 0.1.3 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,46 @@
1
+ require 'berater/dsl'
2
+
3
+ describe Berater do
4
+ using Berater::DSL
5
+
6
+ it 'instatiates an Unlimiter' do
7
+ limiter = Berater.new(:key) { unlimited }
8
+ expect(limiter).to be_a Berater::Unlimiter
9
+ expect(limiter.key).to be :key
10
+ end
11
+
12
+ it 'instatiates an Inhibiter' do
13
+ limiter = Berater.new(:key) { inhibited }
14
+ expect(limiter).to be_a Berater::Inhibitor
15
+ expect(limiter.key).to be :key
16
+ end
17
+
18
+ it 'instatiates a RateLimiter' do
19
+ limiter = Berater.new(:key) { 1.per second }
20
+ expect(limiter).to be_a Berater::RateLimiter
21
+ expect(limiter.key).to be :key
22
+ expect(limiter.capacity).to be 1
23
+ expect(limiter.interval).to be :second
24
+ end
25
+
26
+ it 'instatiates a ConcurrencyLimiter' do
27
+ limiter = Berater.new(:key, timeout: 2) { 1.at_once }
28
+ expect(limiter).to be_a Berater::ConcurrencyLimiter
29
+ expect(limiter.key).to be :key
30
+ expect(limiter.capacity).to be 1
31
+ expect(limiter.timeout).to be 2
32
+ end
33
+
34
+ it 'does not accept args and dsl block' do
35
+ expect {
36
+ Berater.new(:key, 2) { 3.at_once }
37
+ }.to raise_error(ArgumentError)
38
+ end
39
+
40
+ it 'requires either mode or dsl block' do
41
+ expect {
42
+ Berater.new(:key)
43
+ }.to raise_error(ArgumentError)
44
+ end
45
+
46
+ end
data/spec/dsl_spec.rb ADDED
@@ -0,0 +1,72 @@
1
+ require 'berater/dsl'
2
+
3
+ describe Berater::DSL do
4
+ def check(expected, &block)
5
+ expect(Berater::DSL.eval(&block)).to eq expected
6
+ end
7
+
8
+ context 'rate mode' do
9
+ it 'has keywords' do
10
+ check(:second) { second }
11
+ check(:minute) { minute }
12
+ check(:hour) { hour }
13
+ end
14
+
15
+ it 'parses' do
16
+ check([ 1, :second ]) { 1.per second }
17
+ check([ 3, :minute ]) { 3.per minute }
18
+ check([ 5, :hour ]) { 5.every hour }
19
+ end
20
+
21
+ it 'cleans up afterward' do
22
+ check([ 1, :second ]) { 1.per second }
23
+
24
+ expect(Integer).not_to respond_to(:per)
25
+ expect(Integer).not_to respond_to(:every)
26
+ end
27
+
28
+ it 'works with variables' do
29
+ count = 1
30
+ interval = :second
31
+
32
+ check([ count, interval ]) { count.per interval }
33
+ end
34
+ end
35
+
36
+ context 'concurrency mode' do
37
+ it 'parses' do
38
+ check([ 1 ]) { 1.at_once }
39
+ check([ 3 ]) { 3.at_a_time }
40
+ check([ 5 ]) { 5.concurrently }
41
+ end
42
+
43
+ it 'cleans up afterward' do
44
+ check([ 1 ]) { 1.at_once }
45
+
46
+ expect(Integer).not_to respond_to(:at_once)
47
+ expect(Integer).not_to respond_to(:at_a_time)
48
+ expect(Integer).not_to respond_to(:concurrently)
49
+ end
50
+
51
+ it 'works with constants' do
52
+ class Foo
53
+ CAPACITY = 3
54
+ end
55
+
56
+ check([ Foo::CAPACITY ]) { Foo::CAPACITY.at_once }
57
+ end
58
+ end
59
+
60
+ context 'unlimited mode' do
61
+ it 'has keywords' do
62
+ check(:unlimited) { unlimited }
63
+ end
64
+ end
65
+
66
+ context 'inhibited mode' do
67
+ it 'has keywords' do
68
+ check(:inhibited) { inhibited }
69
+ end
70
+ end
71
+
72
+ end
@@ -1,37 +1,23 @@
1
1
  describe Berater::Inhibitor do
2
- before { Berater.mode = :inhibited }
3
-
4
2
  describe '.new' do
5
3
  it 'initializes without any arguments or options' do
6
4
  expect(described_class.new).to be_a described_class
7
5
  end
8
6
 
9
7
  it 'initializes with any arguments and options' do
10
- expect(described_class.new(:abc, x: 123)).to be_a described_class
8
+ expect(described_class.new(:abc, :def, x: 123)).to be_a described_class
11
9
  end
12
10
 
13
11
  it 'has default values' do
14
- expect(described_class.new.key).to eq described_class.to_s
12
+ expect(described_class.new.key).to be :inhibitor
15
13
  expect(described_class.new.redis).to be Berater.redis
16
14
  end
17
15
  end
18
16
 
19
- describe '.limit' do
20
- it 'always limits' do
21
- expect { described_class.limit }.to be_inhibited
22
- end
23
-
24
- it 'works with any arguments or options' do
25
- expect { described_class.limit(:abc, x: 123) }.to be_inhibited
26
- end
27
- end
28
-
29
17
  describe '#limit' do
30
- let(:limiter) { described_class.new }
18
+ subject { described_class.new }
31
19
 
32
- it 'always limits' do
33
- expect { described_class.limit }.to be_inhibited
34
- end
20
+ it_behaves_like 'it is overloaded'
35
21
  end
36
22
 
37
23
  end
@@ -0,0 +1,71 @@
1
+ describe Berater::Limiter do
2
+ it 'can not be initialized' do
3
+ expect { described_class.new }.to raise_error(NotImplementedError)
4
+ end
5
+
6
+ describe 'abstract methods' do
7
+ let(:limiter) { Class.new(described_class).new(:key, 1) }
8
+
9
+ it do
10
+ expect { limiter.limit }.to raise_error(NotImplementedError)
11
+ expect { limiter.overloaded? }.to raise_error(NotImplementedError)
12
+ end
13
+ end
14
+
15
+ describe '==' do
16
+ let(:limiter) { Berater::RateLimiter.new(:key, 1, :second) }
17
+
18
+ it 'equals itself' do
19
+ expect(limiter).to eq limiter
20
+ end
21
+
22
+ it 'equals something with the same initialization parameters' do
23
+ expect(limiter).to eq(
24
+ Berater::RateLimiter.new(:key, 1, :second)
25
+ )
26
+ end
27
+
28
+ it 'equals something with equvalent initialization parameters' do
29
+ expect(limiter).to eq(
30
+ Berater::RateLimiter.new(:key, 1, 1)
31
+ )
32
+ end
33
+
34
+ it 'does not equal something different' do
35
+ expect(limiter).not_to eq(
36
+ Berater::RateLimiter.new(:key, 2, :second)
37
+ )
38
+
39
+ expect(limiter).not_to eq(
40
+ Berater::RateLimiter.new(:keyz, 1, :second)
41
+ )
42
+
43
+ expect(limiter).not_to eq(
44
+ Berater::RateLimiter.new(:key, 1, :minute)
45
+ )
46
+ end
47
+
48
+ it 'does not equal something altogether different' do
49
+ expect(limiter).not_to eq(
50
+ Berater::ConcurrencyLimiter.new(:key, 1)
51
+ )
52
+ end
53
+
54
+ it 'works for ConcurrencyLimiter too' do
55
+ limiter = Berater::ConcurrencyLimiter.new(:key, 1)
56
+ expect(limiter).to eq limiter
57
+
58
+ expect(limiter).not_to eq(
59
+ Berater::ConcurrencyLimiter.new(:key, 1, timeout: 1)
60
+ )
61
+ end
62
+
63
+ it 'and the others' do
64
+ unlimiter = Berater::Unlimiter.new
65
+ expect(unlimiter).to eq unlimiter
66
+
67
+ expect(unlimiter).not_to eq Berater::Inhibitor.new
68
+ end
69
+ end
70
+
71
+ end
@@ -0,0 +1,97 @@
1
+ describe Berater::LuaScript do
2
+ subject { Berater::LuaScript('return redis.call("PING")') }
3
+
4
+ before { redis.script(:flush) }
5
+
6
+ let(:redis) { Berater.redis }
7
+
8
+ it { is_expected.to be_a Berater::LuaScript }
9
+
10
+ describe '#eval' do
11
+ def ping
12
+ expect(subject.eval(redis)).to eq 'PONG'
13
+ end
14
+
15
+ it { ping }
16
+
17
+ it 'loads the script into redis' do
18
+ expect(redis).to receive(:evalsha).once.and_call_original
19
+ expect(redis).to receive(:eval).once.and_call_original
20
+ ping
21
+ expect(subject.loaded?(redis)).to be true
22
+ end
23
+ end
24
+
25
+ describe '#load' do
26
+ it 'loads script into redis' do
27
+ expect(redis.script(:exists, subject.sha)).to be false
28
+ subject.load(redis)
29
+ expect(redis.script(:exists, subject.sha)).to be true
30
+ end
31
+
32
+ it 'returns the sha' do
33
+ expect(subject.load(redis)).to eq subject.sha
34
+ end
35
+
36
+ it 'validates the returned sha' do
37
+ allow(redis).to receive(:script).with(:flush).and_call_original
38
+ expect(redis).to receive(:script).with(:load, String).and_return('abc')
39
+ expect { subject.load(redis) }.to raise_error(RuntimeError)
40
+ end
41
+ end
42
+
43
+ describe '#loaded?' do
44
+ it do
45
+ expect(subject.loaded?(redis)).to be false
46
+ subject.load(redis)
47
+ expect(subject.loaded?(redis)).to be true
48
+ end
49
+ end
50
+
51
+ describe '#to_s' do
52
+ it { expect(subject.to_s).to be subject.source }
53
+ end
54
+
55
+ describe '#minify' do
56
+ subject do
57
+ expect(
58
+ Berater::LuaScript(lua).send(:minify)
59
+ ).to eq expected
60
+ end
61
+
62
+ context do
63
+ let(:lua) do <<-LUA
64
+ -- this comment gets removed
65
+ redis.call('PING') -- this one too
66
+ LUA
67
+ end
68
+
69
+ let(:expected) { "redis.call('PING')" }
70
+
71
+ it { subject }
72
+ end
73
+
74
+ context 'with if statement' do
75
+ let(:lua) do <<~LUA
76
+ if condition then
77
+ call
78
+ end
79
+
80
+ return 123
81
+ LUA
82
+ end
83
+
84
+ let(:expected) do
85
+ [
86
+ 'if condition then',
87
+ 'call',
88
+ 'end',
89
+ 'return 123'
90
+ ].join "\n"
91
+ end
92
+
93
+ it { subject }
94
+ end
95
+ end
96
+
97
+ end
@@ -1,6 +1,6 @@
1
1
  describe 'be_overloaded' do
2
2
  context 'Berater::Unlimiter' do
3
- let(:limiter) { Berater.new(:unlimited) }
3
+ let(:limiter) { Berater.new(:key, :unlimited) }
4
4
 
5
5
  it { expect(limiter).not_to be_overloaded }
6
6
  it { expect(limiter).not_to be_inhibited }
@@ -16,10 +16,16 @@ describe 'be_overloaded' do
16
16
  it { expect { limiter.limit }.not_to be_inhibited }
17
17
  it { expect { limiter.limit }.not_to be_overrated }
18
18
  it { expect { limiter.limit }.not_to be_incapacitated }
19
+
20
+ it 'catches false positives' do
21
+ expect {
22
+ expect { limiter }.to be_overloaded
23
+ }.to fail
24
+ end
19
25
  end
20
26
 
21
27
  context 'Berater::Inhibitor' do
22
- let(:limiter) { Berater.new(:inhibited) }
28
+ let(:limiter) { Berater.new(:key, :inhibited) }
23
29
 
24
30
  it { expect(limiter).to be_overloaded }
25
31
  it { expect(limiter).to be_inhibited }
@@ -29,10 +35,16 @@ describe 'be_overloaded' do
29
35
 
30
36
  it { expect { limiter.limit }.to be_overloaded }
31
37
  it { expect { limiter.limit }.to be_inhibited }
38
+
39
+ it 'catches false negatives' do
40
+ expect {
41
+ expect { limiter }.not_to be_overloaded
42
+ }.to fail
43
+ end
32
44
  end
33
45
 
34
46
  context 'Berater::RateLimiter' do
35
- let(:limiter) { Berater.new(:rate, 1, :second) }
47
+ let(:limiter) { Berater.new(:key, 1, :second) }
36
48
 
37
49
  it { expect(limiter).not_to be_overloaded }
38
50
  it { expect(limiter).not_to be_inhibited }
@@ -67,7 +79,7 @@ describe 'be_overloaded' do
67
79
  end
68
80
 
69
81
  context 'Berater::ConcurrencyLimiter' do
70
- let(:limiter) { Berater.new(:concurrency, 1) }
82
+ let(:limiter) { Berater.new(:key, 1) }
71
83
 
72
84
  it { expect(limiter).not_to be_overloaded }
73
85
  it { expect(limiter).not_to be_inhibited }
@@ -1,165 +1,187 @@
1
1
  describe Berater::RateLimiter do
2
- before { Berater.mode = :rate }
2
+ it_behaves_like 'a limiter', Berater.new(:key, 3, :second)
3
3
 
4
4
  describe '.new' do
5
- let(:limiter) { described_class.new(1, :second) }
5
+ let(:limiter) { described_class.new(:key, 1, :second) }
6
6
 
7
7
  it 'initializes' do
8
- expect(limiter.count).to eq 1
9
- expect(limiter.interval).to eq 1
8
+ expect(limiter.key).to be :key
9
+ expect(limiter.capacity).to eq 1
10
+ expect(limiter.interval).to eq :second
10
11
  end
11
12
 
12
13
  it 'has default values' do
13
- expect(limiter.key).to eq described_class.to_s
14
14
  expect(limiter.redis).to be Berater.redis
15
15
  end
16
16
  end
17
17
 
18
- describe '#count' do
19
- def expect_count(count)
20
- limiter = described_class.new(count, :second)
21
- expect(limiter.count).to eq count
18
+ describe '#capacity' do
19
+ def expect_capacity(capacity)
20
+ limiter = described_class.new(:key, capacity, :second)
21
+ expect(limiter.capacity).to eq capacity
22
22
  end
23
23
 
24
- it { expect_count(0) }
25
- it { expect_count(1) }
26
- it { expect_count(100) }
24
+ it { expect_capacity(0) }
25
+ it { expect_capacity(1) }
26
+ it { expect_capacity(100) }
27
27
 
28
28
  context 'with erroneous values' do
29
- def expect_bad_count(count)
29
+ def expect_bad_capacity(capacity)
30
30
  expect do
31
- described_class.new(count, :second)
31
+ described_class.new(:key, capacity, :second)
32
32
  end.to raise_error ArgumentError
33
33
  end
34
34
 
35
- it { expect_bad_count(0.5) }
36
- it { expect_bad_count(-1) }
37
- it { expect_bad_count('1') }
38
- it { expect_bad_count(:one) }
35
+ it { expect_bad_capacity(0.5) }
36
+ it { expect_bad_capacity(-1) }
37
+ it { expect_bad_capacity('1') }
38
+ it { expect_bad_capacity(:one) }
39
39
  end
40
40
  end
41
41
 
42
42
  describe '#interval' do
43
- def expect_interval(interval, expected)
44
- limiter = described_class.new(1, interval)
45
- expect(limiter.interval).to eq expected
46
- end
47
-
48
- context 'with ints' do
49
- it { expect_interval(0, 0) }
50
- it { expect_interval(1, 1) }
51
- it { expect_interval(33, 33) }
52
- end
53
-
54
- context 'with symbols' do
55
- it { expect_interval(:sec, 1) }
56
- it { expect_interval(:second, 1) }
57
- it { expect_interval(:seconds, 1) }
43
+ # see spec/utils_spec.rb for more
58
44
 
59
- it { expect_interval(:min, 60) }
60
- it { expect_interval(:minute, 60) }
61
- it { expect_interval(:minutes, 60) }
45
+ subject { described_class.new(:key, 1, :second) }
62
46
 
63
- it { expect_interval(:hour, 3600) }
64
- it { expect_interval(:hours, 3600) }
65
- end
66
-
67
- context 'with strings' do
68
- it { expect_interval('sec', 1) }
69
- it { expect_interval('minute', 60) }
70
- it { expect_interval('hours', 3600) }
71
- end
72
-
73
- context 'with erroneous values' do
74
- def expect_bad_interval(interval)
75
- expect do
76
- described_class.new(1, interval)
77
- end.to raise_error(ArgumentError)
78
- end
79
-
80
- it { expect_bad_interval(-1) }
81
- it { expect_bad_interval(:secondz) }
82
- it { expect_bad_interval('huor') }
47
+ it 'saves the interval in original and microsecond format' do
48
+ expect(subject.interval).to be :second
49
+ expect(subject.instance_variable_get(:@interval_usec)).to be 10**6
83
50
  end
84
51
  end
85
52
 
86
53
  describe '#limit' do
87
- let(:limiter) { described_class.new(3, :second) }
54
+ let(:limiter) { described_class.new(:key, 3, :second) }
88
55
 
89
56
  it 'works' do
90
- expect(limiter.limit).to eq 1
91
- end
92
-
93
- it 'counts' do
94
- expect(limiter.limit).to eq 1
95
- expect(limiter.limit).to eq 2
96
- expect(limiter.limit).to eq 3
97
- end
98
-
99
- it 'yields' do
100
57
  expect {|b| limiter.limit(&b) }.to yield_control
101
58
  expect(limiter.limit { 123 }).to eq 123
102
59
  end
103
60
 
61
+ it 'works without a block' do
62
+ expect(limiter.limit).to be_a Berater::Lock
63
+ end
64
+
104
65
  it 'limits excessive calls' do
105
66
  3.times { limiter.limit }
106
67
 
107
- expect { limiter.limit }.to be_overrated
68
+ expect(limiter).to be_overrated
108
69
  end
109
70
 
110
- it 'limit resets over time' do
111
- expect(limiter.limit).to eq 1
112
- expect(limiter.limit).to eq 2
113
- expect(limiter.limit).to eq 3
71
+ it 'limit resets over time, with millisecond precision' do
72
+ 3.times { limiter.limit }
114
73
  expect(limiter).to be_overrated
115
74
 
116
- # travel forward a second
75
+ # travel forward to just before the count decrements
76
+ Timecop.freeze(0.333)
77
+ expect(limiter).to be_overrated
78
+
79
+ # traveling one more millisecond will decrement the count
80
+ Timecop.freeze(0.001)
81
+ limiter.limit
82
+ expect(limiter).to be_overrated
83
+
84
+ # traveling 1 second will reset the count
117
85
  Timecop.freeze(1)
118
86
 
119
- expect(limiter.limit).to eq 1
120
- expect(limiter.limit).to eq 2
121
- expect(limiter.limit).to eq 3
87
+ 3.times { limiter.limit }
122
88
  expect(limiter).to be_overrated
123
89
  end
124
- end
125
90
 
126
- context 'with same key, different limiters' do
127
- let(:limiter_one) { described_class.new(1, :second) }
128
- let(:limiter_two) { described_class.new(1, :second) }
91
+ it 'accepts a dynamic capacity' do
92
+ limiter = described_class.new(:key, 1, :second)
129
93
 
130
- it 'works as expected' do
131
- expect(limiter_one.limit).to eq 1
94
+ expect { limiter.limit(capacity: 0) }.to be_overrated
95
+ 5.times { limiter.limit(capacity: 10) }
96
+ expect { limiter }.to be_overrated
97
+ end
98
+
99
+ context 'works with cost parameter' do
100
+ it { expect { limiter.limit(cost: 4) }.to be_overrated }
101
+
102
+ it 'works within limit' do
103
+ limiter.limit(cost: 3)
104
+ expect { limiter.limit }.to be_overrated
105
+ end
106
+
107
+ it 'resets over time' do
108
+ limiter.limit(cost: 3)
109
+ expect(limiter).to be_overrated
110
+
111
+ Timecop.freeze(1)
112
+ expect(limiter).not_to be_overrated
113
+ end
132
114
 
133
- expect { limiter_one }.to be_overrated
134
- expect { limiter_two }.to be_overrated
115
+ it 'can be a Float' do
116
+ 2.times { limiter.limit(cost: 1.5) }
117
+ expect(limiter).to be_overrated
118
+ end
135
119
  end
136
- end
137
120
 
138
- context 'with different keys, same limiter' do
139
- let(:limiter) { described_class.new(1, :second) }
121
+ context 'with same key, different limiters' do
122
+ let(:limiter_one) { described_class.new(:key, 1, :second) }
123
+ let(:limiter_two) { described_class.new(:key, 1, :second) }
140
124
 
141
- it 'works as expected' do
142
- expect { limiter.limit(key: :one) }.not_to be_overrated
143
- expect { limiter.limit(key: :one) }.to be_overrated
125
+ it 'works as expected' do
126
+ expect(limiter_one.limit).not_to be_overrated
144
127
 
145
- expect { limiter.limit(key: :two) }.not_to be_overrated
146
- expect { limiter.limit(key: :two) }.to be_overrated
128
+ expect(limiter_one).to be_overrated
129
+ expect(limiter_two).to be_overrated
130
+ end
131
+ end
132
+
133
+ context 'with different keys, different limiters' do
134
+ let(:limiter_one) { described_class.new(:one, 1, :second) }
135
+ let(:limiter_two) { described_class.new(:two, 2, :second) }
136
+
137
+ it 'works as expected' do
138
+ expect(limiter_one.limit).not_to be_overrated
139
+ expect(limiter_two.limit).not_to be_overrated
140
+
141
+ expect(limiter_one).to be_overrated
142
+ expect(limiter_two.limit).not_to be_overrated
143
+
144
+ expect(limiter_one).to be_overrated
145
+ expect(limiter_two).to be_overrated
146
+ end
147
+ end
148
+ end
149
+
150
+ describe '#overloaded?' do
151
+ let(:limiter) { described_class.new(:key, 1, :second) }
152
+
153
+ it 'works' do
154
+ expect(limiter.overloaded?).to be false
155
+ limiter.limit
156
+ expect(limiter.overloaded?).to be true
157
+ Timecop.freeze(1)
158
+ expect(limiter.overloaded?).to be false
147
159
  end
148
160
  end
149
161
 
150
- context 'with different keys, different limiters' do
151
- let(:limiter_one) { described_class.new(1, :second, key: :one) }
152
- let(:limiter_two) { described_class.new(2, :second, key: :two) }
162
+ describe '#to_s' do
163
+ def check(capacity, interval, expected)
164
+ expect(
165
+ described_class.new(:key, capacity, interval).to_s
166
+ ).to match(expected)
167
+ end
153
168
 
154
- it 'works as expected' do
155
- expect(limiter_one.limit).to eq 1
156
- expect(limiter_two.limit).to eq 1
169
+ it 'works with symbols' do
170
+ check(1, :second, /1 per second/)
171
+ check(1, :minute, /1 per minute/)
172
+ check(1, :hour, /1 per hour/)
173
+ end
157
174
 
158
- expect { limiter_one.limit }.to be_overrated
159
- expect(limiter_two.limit).to eq 2
175
+ it 'works with strings' do
176
+ check(1, 'second', /1 per second/)
177
+ check(1, 'minute', /1 per minute/)
178
+ check(1, 'hour', /1 per hour/)
179
+ end
160
180
 
161
- expect { limiter_one.limit }.to be_overrated
162
- expect { limiter_two.limit }.to be_overrated
181
+ it 'works with integers' do
182
+ check(1, 1, /1 every second/)
183
+ check(1, 2, /1 every 2 seconds/)
184
+ check(2, 3, /2 every 3 seconds/)
163
185
  end
164
186
  end
165
187