functional-ruby 0.7.7 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +92 -152
- data/doc/memo.txt +192 -0
- data/doc/pattern_matching.txt +485 -0
- data/doc/protocol.txt +221 -0
- data/doc/record.txt +144 -0
- data/doc/thread_safety.txt +8 -0
- data/lib/functional.rb +48 -18
- data/lib/functional/abstract_struct.rb +161 -0
- data/lib/functional/delay.rb +117 -0
- data/lib/functional/either.rb +222 -0
- data/lib/functional/memo.rb +93 -0
- data/lib/functional/method_signature.rb +72 -0
- data/lib/functional/option.rb +209 -0
- data/lib/functional/pattern_matching.rb +117 -100
- data/lib/functional/protocol.rb +157 -0
- data/lib/functional/protocol_info.rb +193 -0
- data/lib/functional/record.rb +155 -0
- data/lib/functional/type_check.rb +112 -0
- data/lib/functional/union.rb +152 -0
- data/lib/functional/version.rb +3 -1
- data/spec/functional/abstract_struct_shared.rb +154 -0
- data/spec/functional/complex_pattern_matching_spec.rb +205 -0
- data/spec/functional/configuration_spec.rb +17 -0
- data/spec/functional/delay_spec.rb +147 -0
- data/spec/functional/either_spec.rb +237 -0
- data/spec/functional/memo_spec.rb +207 -0
- data/spec/functional/option_spec.rb +292 -0
- data/spec/functional/pattern_matching_spec.rb +279 -276
- data/spec/functional/protocol_info_spec.rb +444 -0
- data/spec/functional/protocol_spec.rb +274 -0
- data/spec/functional/record_spec.rb +175 -0
- data/spec/functional/type_check_spec.rb +103 -0
- data/spec/functional/union_spec.rb +110 -0
- data/spec/spec_helper.rb +6 -4
- metadata +55 -45
- data/lib/functional/behavior.rb +0 -138
- data/lib/functional/behaviour.rb +0 -2
- data/lib/functional/catalog.rb +0 -487
- data/lib/functional/collection.rb +0 -403
- data/lib/functional/inflect.rb +0 -127
- data/lib/functional/platform.rb +0 -120
- data/lib/functional/search.rb +0 -132
- data/lib/functional/sort.rb +0 -41
- data/lib/functional/utilities.rb +0 -189
- data/md/behavior.md +0 -188
- data/md/catalog.md +0 -32
- data/md/collection.md +0 -32
- data/md/inflect.md +0 -32
- data/md/pattern_matching.md +0 -512
- data/md/platform.md +0 -32
- data/md/search.md +0 -32
- data/md/sort.md +0 -32
- data/md/utilities.md +0 -55
- data/spec/functional/behavior_spec.rb +0 -528
- data/spec/functional/catalog_spec.rb +0 -1206
- data/spec/functional/collection_spec.rb +0 -752
- data/spec/functional/inflect_spec.rb +0 -85
- data/spec/functional/integration_spec.rb +0 -205
- data/spec/functional/platform_spec.rb +0 -501
- data/spec/functional/search_spec.rb +0 -187
- data/spec/functional/sort_spec.rb +0 -61
- data/spec/functional/utilities_spec.rb +0 -277
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Functional
|
4
|
+
|
5
|
+
context Configuration do
|
6
|
+
|
7
|
+
it 'configures the gem' do
|
8
|
+
cfg = nil
|
9
|
+
Functional.configure do |config|
|
10
|
+
cfg = config
|
11
|
+
end
|
12
|
+
|
13
|
+
expect(cfg).to be_a Configuration
|
14
|
+
expect(cfg).to eq Functional.configuration
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,147 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Functional
|
4
|
+
|
5
|
+
describe Delay do
|
6
|
+
|
7
|
+
let!(:fulfilled_value) { 10 }
|
8
|
+
let!(:rejected_reason) { StandardError.new('mojo jojo') }
|
9
|
+
|
10
|
+
let(:pending_subject) do
|
11
|
+
Delay.new{ fulfilled_value }
|
12
|
+
end
|
13
|
+
|
14
|
+
let(:fulfilled_subject) do
|
15
|
+
delay = Delay.new{ fulfilled_value }
|
16
|
+
delay.tap{ delay.value }
|
17
|
+
end
|
18
|
+
|
19
|
+
let(:rejected_subject) do
|
20
|
+
delay = Delay.new{ raise rejected_reason }
|
21
|
+
delay.tap{ delay.value }
|
22
|
+
end
|
23
|
+
|
24
|
+
specify{ Functional::Protocol::Satisfy! Delay, :Disposition }
|
25
|
+
|
26
|
+
context '#initialize' do
|
27
|
+
|
28
|
+
it 'sets the state to :pending' do
|
29
|
+
expect(Delay.new{ nil }.state).to eq :pending
|
30
|
+
expect(Delay.new{ nil }).to be_pending
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'raises an exception when no block given' do
|
34
|
+
expect {
|
35
|
+
Delay.new
|
36
|
+
}.to raise_error(ArgumentError)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
context '#state' do
|
41
|
+
|
42
|
+
it 'is :pending when first created' do
|
43
|
+
f = pending_subject
|
44
|
+
expect(f.state).to eq(:pending)
|
45
|
+
expect(f).to be_pending
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'is :fulfilled when the handler completes' do
|
49
|
+
f = fulfilled_subject
|
50
|
+
expect(f.state).to eq(:fulfilled)
|
51
|
+
expect(f).to be_fulfilled
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'is :rejected when the handler raises an exception' do
|
55
|
+
f = rejected_subject
|
56
|
+
expect(f.state).to eq(:rejected)
|
57
|
+
expect(f).to be_rejected
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
context '#value' do
|
62
|
+
|
63
|
+
let(:task){ proc{ nil } }
|
64
|
+
|
65
|
+
it 'blocks the caller when :pending and timeout is nil' do
|
66
|
+
f = pending_subject
|
67
|
+
expect(f.value).to be_truthy
|
68
|
+
expect(f).to be_fulfilled
|
69
|
+
end
|
70
|
+
|
71
|
+
it 'is nil when :rejected' do
|
72
|
+
expected = rejected_subject.value
|
73
|
+
expect(expected).to be_nil
|
74
|
+
end
|
75
|
+
|
76
|
+
it 'is set to the return value of the block when :fulfilled' do
|
77
|
+
expected = fulfilled_subject.value
|
78
|
+
expect(expected).to eq fulfilled_value
|
79
|
+
end
|
80
|
+
|
81
|
+
it 'does not call the block before #value is called' do
|
82
|
+
expect(task).not_to receive(:call).with(any_args)
|
83
|
+
Delay.new(&task)
|
84
|
+
end
|
85
|
+
|
86
|
+
it 'calls the block when #value is called' do
|
87
|
+
expect(task).to receive(:call).once.with(any_args).and_return(nil)
|
88
|
+
Delay.new(&task).value
|
89
|
+
end
|
90
|
+
|
91
|
+
it 'only calls the block once no matter how often #value is called' do
|
92
|
+
expect(task).to receive(:call).once.with(any_args).and_return(nil)
|
93
|
+
delay = Delay.new(&task)
|
94
|
+
5.times{ delay.value }
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
context '#reason' do
|
99
|
+
|
100
|
+
it 'is nil when :pending' do
|
101
|
+
expect(pending_subject.reason).to be_nil
|
102
|
+
end
|
103
|
+
|
104
|
+
it 'is nil when :fulfilled' do
|
105
|
+
expect(fulfilled_subject.reason).to be_nil
|
106
|
+
end
|
107
|
+
|
108
|
+
it 'is set to error object of the exception when :rejected' do
|
109
|
+
expect(rejected_subject.reason).to be_a(Exception)
|
110
|
+
expect(rejected_subject.reason.to_s).to match(/#{rejected_reason}/)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
context 'predicates' do
|
115
|
+
|
116
|
+
specify '#value? returns true when :fulfilled' do
|
117
|
+
expect(pending_subject).to_not be_value
|
118
|
+
expect(fulfilled_subject).to be_value
|
119
|
+
expect(rejected_subject).to_not be_value
|
120
|
+
end
|
121
|
+
|
122
|
+
specify '#reason? returns true when :rejected' do
|
123
|
+
expect(pending_subject).to_not be_reason
|
124
|
+
expect(fulfilled_subject).to_not be_reason
|
125
|
+
expect(rejected_subject).to be_reason
|
126
|
+
end
|
127
|
+
|
128
|
+
specify '#fulfilled? returns true when :fulfilled' do
|
129
|
+
expect(pending_subject).to_not be_fulfilled
|
130
|
+
expect(fulfilled_subject).to be_fulfilled
|
131
|
+
expect(rejected_subject).to_not be_fulfilled
|
132
|
+
end
|
133
|
+
|
134
|
+
specify '#rejected? returns true when :rejected' do
|
135
|
+
expect(pending_subject).to_not be_rejected
|
136
|
+
expect(fulfilled_subject).to_not be_rejected
|
137
|
+
expect(rejected_subject).to be_rejected
|
138
|
+
end
|
139
|
+
|
140
|
+
specify '#pending? returns true when :pending' do
|
141
|
+
expect(pending_subject).to be_pending
|
142
|
+
expect(fulfilled_subject).to_not be_pending
|
143
|
+
expect(rejected_subject).to_not be_pending
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
@@ -0,0 +1,237 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require_relative 'abstract_struct_shared'
|
3
|
+
|
4
|
+
module Functional
|
5
|
+
|
6
|
+
describe Either do
|
7
|
+
|
8
|
+
let!(:value){ 42 }
|
9
|
+
let!(:reason){ StandardError.new }
|
10
|
+
|
11
|
+
let!(:expected_fields){ [:left, :right] }
|
12
|
+
let!(:expected_values){ [value, nil] }
|
13
|
+
|
14
|
+
let(:struct_class) { Either }
|
15
|
+
let(:struct_object) { Either.left(value) }
|
16
|
+
let(:other_object) { Either.left(Object.new) }
|
17
|
+
|
18
|
+
let(:left_subject){ Either.left(reason) }
|
19
|
+
let(:right_subject){ Either.right(value) }
|
20
|
+
|
21
|
+
it_should_behave_like :abstract_struct
|
22
|
+
|
23
|
+
specify{ Functional::Protocol::Satisfy! Either, :Either }
|
24
|
+
specify{ Functional::Protocol::Satisfy! Either, :Disposition }
|
25
|
+
|
26
|
+
context 'initialization' do
|
27
|
+
|
28
|
+
it 'cannot be constructed directly' do
|
29
|
+
expect {
|
30
|
+
Either.new
|
31
|
+
}.to raise_error(NameError)
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'sets the left value when constructed by #left' do
|
35
|
+
expect(Either.left(value).left).to eq value
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'sets the right value when constructed by #right' do
|
39
|
+
expect(Either.right(value).right).to eq value
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'freezes the new object' do
|
43
|
+
expect(Either.left(:foo)).to be_frozen
|
44
|
+
expect(Either.right(:foo)).to be_frozen
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'aliases #left to #reason' do
|
48
|
+
expect(Either.reason(value).left).to eq value
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'aliases #right to #value' do
|
52
|
+
expect(Either.value(value).right).to eq value
|
53
|
+
end
|
54
|
+
|
55
|
+
context '#error' do
|
56
|
+
|
57
|
+
it 'sets left to a StandardError with backtrace when no arguments given' do
|
58
|
+
either = Either.error
|
59
|
+
expect(either.left).to be_a StandardError
|
60
|
+
expect(either.left.message).to_not be nil
|
61
|
+
expect(either.left.backtrace).to_not be_empty
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'sets left to a StandardError with the given message' do
|
65
|
+
message = 'custom error message'
|
66
|
+
either = Either.error(message)
|
67
|
+
expect(either.left).to be_a StandardError
|
68
|
+
expect(either.left.message).to eq message
|
69
|
+
expect(either.left.backtrace).to_not be_empty
|
70
|
+
end
|
71
|
+
|
72
|
+
it 'sets left to an object of the given class with the given message' do
|
73
|
+
message = 'custom error message'
|
74
|
+
error_class = ArgumentError
|
75
|
+
either = Either.error(message, error_class)
|
76
|
+
expect(either.left).to be_a error_class
|
77
|
+
expect(either.left.message).to eq message
|
78
|
+
expect(either.left.backtrace).to_not be_empty
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
context 'state' do
|
84
|
+
|
85
|
+
specify '#left? returns true when the left value is set' do
|
86
|
+
expect(left_subject).to be_left
|
87
|
+
end
|
88
|
+
|
89
|
+
specify '#left? returns false when the right value is set' do
|
90
|
+
expect(right_subject).to_not be_left
|
91
|
+
end
|
92
|
+
|
93
|
+
specify '#right? returns true when the right value is set' do
|
94
|
+
expect(right_subject).to be_right
|
95
|
+
end
|
96
|
+
|
97
|
+
specify '#right? returns false when the left value is set' do
|
98
|
+
expect(left_subject).to_not be_right
|
99
|
+
end
|
100
|
+
|
101
|
+
specify '#left returns the left value when left is set' do
|
102
|
+
expect(left_subject.left).to eq reason
|
103
|
+
end
|
104
|
+
|
105
|
+
specify '#left returns nil when right is set' do
|
106
|
+
expect(right_subject.left).to be_nil
|
107
|
+
end
|
108
|
+
|
109
|
+
specify '#right returns the right value when right is set' do
|
110
|
+
expect(right_subject.right).to eq value
|
111
|
+
end
|
112
|
+
|
113
|
+
specify '#right returns nil when left is set' do
|
114
|
+
expect(left_subject.right).to be_nil
|
115
|
+
end
|
116
|
+
|
117
|
+
specify 'aliases #left? as #reason?' do
|
118
|
+
expect(left_subject.reason?).to be true
|
119
|
+
end
|
120
|
+
|
121
|
+
specify 'aliases #right? as #value?' do
|
122
|
+
expect(right_subject.value?).to be true
|
123
|
+
end
|
124
|
+
|
125
|
+
specify 'aliases #left as #reason' do
|
126
|
+
expect(left_subject.reason).to eq reason
|
127
|
+
expect(right_subject.reason).to be_nil
|
128
|
+
end
|
129
|
+
|
130
|
+
specify 'aliases #right as #value' do
|
131
|
+
expect(right_subject.value).to eq value
|
132
|
+
expect(left_subject.value).to be_nil
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
context '#swap' do
|
137
|
+
|
138
|
+
it 'converts a left projection into a right projection' do
|
139
|
+
subject = Either.left(:foo)
|
140
|
+
swapped = subject.swap
|
141
|
+
expect(swapped).to be_right
|
142
|
+
expect(swapped.left).to be_nil
|
143
|
+
expect(swapped.right).to eq :foo
|
144
|
+
end
|
145
|
+
|
146
|
+
it 'converts a right projection into a left projection' do
|
147
|
+
subject = Either.right(:foo)
|
148
|
+
swapped = subject.swap
|
149
|
+
expect(swapped).to be_left
|
150
|
+
expect(swapped.right).to be_nil
|
151
|
+
expect(swapped.left).to eq :foo
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
context '#either' do
|
156
|
+
|
157
|
+
it 'passes the left value to the left proc when left' do
|
158
|
+
expected = nil
|
159
|
+
subject = Either.left(100)
|
160
|
+
subject.either(
|
161
|
+
->(left) { expected = left },
|
162
|
+
->(right) { expected = -1 }
|
163
|
+
)
|
164
|
+
expect(expected).to eq 100
|
165
|
+
end
|
166
|
+
|
167
|
+
it 'returns the value of the left proc when left' do
|
168
|
+
subject = Either.left(100)
|
169
|
+
expect(
|
170
|
+
subject.either(
|
171
|
+
->(left) { left * 2 },
|
172
|
+
->(right) { nil }
|
173
|
+
)
|
174
|
+
).to eq 200
|
175
|
+
end
|
176
|
+
|
177
|
+
it 'passes the right value to the right proc when right' do
|
178
|
+
expected = nil
|
179
|
+
subject = Either.right(100)
|
180
|
+
subject.either(
|
181
|
+
->(right) { expected = -1 },
|
182
|
+
->(right) { expected = right }
|
183
|
+
)
|
184
|
+
expect(expected).to eq 100
|
185
|
+
end
|
186
|
+
|
187
|
+
it 'returns the value of the right proc when right' do
|
188
|
+
subject = Either.right(100)
|
189
|
+
expect(
|
190
|
+
subject.either(
|
191
|
+
->(right) { nil },
|
192
|
+
->(right) { right * 2 }
|
193
|
+
)
|
194
|
+
).to eq 200
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
context '#iff' do
|
199
|
+
|
200
|
+
it 'returns a lefty with the given left value when the boolean is true' do
|
201
|
+
subject = Either.iff(:foo, :bar, true)
|
202
|
+
expect(subject).to be_left
|
203
|
+
expect(subject.left).to eq :foo
|
204
|
+
end
|
205
|
+
|
206
|
+
it 'returns a righty with the given right value when the boolean is false' do
|
207
|
+
subject = Either.iff(:foo, :bar, false)
|
208
|
+
expect(subject).to be_right
|
209
|
+
expect(subject.right).to eq :bar
|
210
|
+
end
|
211
|
+
|
212
|
+
it 'returns a lefty with the given left value when the block is truthy' do
|
213
|
+
subject = Either.iff(:foo, :bar){ :baz }
|
214
|
+
expect(subject).to be_left
|
215
|
+
expect(subject.left).to eq :foo
|
216
|
+
end
|
217
|
+
|
218
|
+
it 'returns a righty with the given right value when the block is false' do
|
219
|
+
subject = Either.iff(:foo, :bar){ false }
|
220
|
+
expect(subject).to be_right
|
221
|
+
expect(subject.right).to eq :bar
|
222
|
+
end
|
223
|
+
|
224
|
+
it 'returns a righty with the given right value when the block is nil' do
|
225
|
+
subject = Either.iff(:foo, :bar){ nil }
|
226
|
+
expect(subject).to be_right
|
227
|
+
expect(subject.right).to eq :bar
|
228
|
+
end
|
229
|
+
|
230
|
+
it 'raises an exception when both a boolean and a block are given' do
|
231
|
+
expect {
|
232
|
+
subject = Either.iff(:foo, :bar, true){ nil }
|
233
|
+
}.to raise_error(ArgumentError)
|
234
|
+
end
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
@@ -0,0 +1,207 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Functional
|
4
|
+
|
5
|
+
describe Memo do
|
6
|
+
|
7
|
+
def create_new_memo_class
|
8
|
+
Class.new do
|
9
|
+
include Functional::Memo
|
10
|
+
|
11
|
+
class << self
|
12
|
+
attr_accessor :count
|
13
|
+
end
|
14
|
+
|
15
|
+
self.count = 0
|
16
|
+
|
17
|
+
def self.add(a, b)
|
18
|
+
self.count += 1
|
19
|
+
a + b
|
20
|
+
end
|
21
|
+
memoize :add
|
22
|
+
|
23
|
+
def self.increment(n)
|
24
|
+
self.count += 1
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.exception(ex = StandardError)
|
28
|
+
raise ex
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
subject{ create_new_memo_class }
|
34
|
+
|
35
|
+
context 'specification' do
|
36
|
+
|
37
|
+
it 'raises an exception when the method is not defined' do
|
38
|
+
expect {
|
39
|
+
subject.memoize(:bogus)
|
40
|
+
}.to raise_error(NameError)
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'raises an exception when the given method has already been memoized' do
|
44
|
+
expect{
|
45
|
+
subject.memoize(:add)
|
46
|
+
}.to raise_error(ArgumentError)
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'allocates a different cache for each class/module' do
|
50
|
+
class_1 = create_new_memo_class
|
51
|
+
class_2 = create_new_memo_class
|
52
|
+
|
53
|
+
10.times do
|
54
|
+
class_1.add(0, 0)
|
55
|
+
class_2.add(0, 0)
|
56
|
+
end
|
57
|
+
|
58
|
+
expect(class_1.count).to eq 1
|
59
|
+
expect(class_2.count).to eq 1
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'works when included in a class' do
|
63
|
+
subject = Class.new do
|
64
|
+
include Functional::Memo
|
65
|
+
class << self
|
66
|
+
attr_accessor :count
|
67
|
+
end
|
68
|
+
self.count = 0
|
69
|
+
def self.foo
|
70
|
+
self.count += 1
|
71
|
+
end
|
72
|
+
memoize :foo
|
73
|
+
end
|
74
|
+
|
75
|
+
10.times{ subject.foo }
|
76
|
+
expect(subject.count).to eq 1
|
77
|
+
end
|
78
|
+
|
79
|
+
it 'works when included in a module' do
|
80
|
+
subject = Module.new do
|
81
|
+
include Functional::Memo
|
82
|
+
class << self
|
83
|
+
attr_accessor :count
|
84
|
+
end
|
85
|
+
self.count = 0
|
86
|
+
def self.foo
|
87
|
+
self.count += 1
|
88
|
+
end
|
89
|
+
memoize :foo
|
90
|
+
end
|
91
|
+
|
92
|
+
10.times{ subject.foo }
|
93
|
+
expect(subject.count).to eq 1
|
94
|
+
end
|
95
|
+
|
96
|
+
it 'works when extended by a module' do
|
97
|
+
subject = Module.new do
|
98
|
+
extend Functional::Memo
|
99
|
+
class << self
|
100
|
+
attr_accessor :count
|
101
|
+
end
|
102
|
+
self.count = 0
|
103
|
+
def self.foo
|
104
|
+
self.count += 1
|
105
|
+
end
|
106
|
+
memoize :foo
|
107
|
+
end
|
108
|
+
|
109
|
+
10.times{ subject.foo }
|
110
|
+
expect(subject.count).to eq 1
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
context 'caching behavior' do
|
115
|
+
|
116
|
+
it 'calls the real method on first instance of given args' do
|
117
|
+
subject.add(1, 2)
|
118
|
+
expect(subject.count).to eq 1
|
119
|
+
end
|
120
|
+
|
121
|
+
it 'calls the real method on first instance of given args' do
|
122
|
+
subject.add(1, 2)
|
123
|
+
expect(subject.count).to eq 1
|
124
|
+
end
|
125
|
+
|
126
|
+
it 'uses the memo on second instance of given args' do
|
127
|
+
5.times { subject.add(1, 2) }
|
128
|
+
expect(subject.count).to eq 1
|
129
|
+
end
|
130
|
+
|
131
|
+
it 'calls the real method when given a block' do
|
132
|
+
5.times { subject.add(1, 2){ nil } }
|
133
|
+
expect(subject.count).to eq 5
|
134
|
+
end
|
135
|
+
|
136
|
+
it 'raises an exception when arity does not match' do
|
137
|
+
expect {
|
138
|
+
subject.add
|
139
|
+
}.to raise_error(ArgumentError)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
context 'maximum cache size' do
|
144
|
+
|
145
|
+
it 'raises an exception when given a non-positive :at_most' do
|
146
|
+
expect {
|
147
|
+
subject.memoize(:increment, at_most: -1)
|
148
|
+
}.to raise_error(ArgumentError)
|
149
|
+
end
|
150
|
+
|
151
|
+
it 'sets no limit when :at_most not given' do
|
152
|
+
subject.memoize(:increment)
|
153
|
+
10000.times{|i| subject.increment(i) }
|
154
|
+
expect(subject.count).to eq 10000
|
155
|
+
end
|
156
|
+
|
157
|
+
it 'calls the real method when the :at_most size is reached' do
|
158
|
+
subject.memoize(:increment, at_most: 5)
|
159
|
+
10000.times{|i| subject.increment(i % 10) }
|
160
|
+
expect(subject.count).to eq 5005
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
context 'thread safety' do
|
165
|
+
|
166
|
+
let(:mutex){ Mutex.new }
|
167
|
+
|
168
|
+
before(:each) do
|
169
|
+
allow(Mutex).to receive(:new).with(no_args).and_return(mutex)
|
170
|
+
allow(mutex).to receive(:lock).with(no_args).and_return(mutex)
|
171
|
+
allow(mutex).to receive(:unlock).with(no_args).and_return(mutex)
|
172
|
+
end
|
173
|
+
|
174
|
+
it 'locks a mutex whenever a memoized function is called' do
|
175
|
+
expect(mutex).to receive(:lock).exactly(:once).with(no_args)
|
176
|
+
|
177
|
+
subject.memoize(:increment)
|
178
|
+
subject.increment(0)
|
179
|
+
end
|
180
|
+
|
181
|
+
it 'unlocks the mutex whenever a memoized function is called' do
|
182
|
+
expect(mutex).to receive(:unlock).exactly(:once).with(no_args)
|
183
|
+
|
184
|
+
subject.memoize(:increment)
|
185
|
+
subject.increment(0)
|
186
|
+
end
|
187
|
+
|
188
|
+
it 'unlocks the mutex when the method call raises an exception' do
|
189
|
+
expect(mutex).to receive(:unlock).exactly(:once).with(no_args)
|
190
|
+
|
191
|
+
subject.memoize(:exception)
|
192
|
+
begin
|
193
|
+
subject.exception
|
194
|
+
rescue
|
195
|
+
# suppress
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
it 'uses different mutexes for different functions' do
|
200
|
+
expect(Mutex).to receive(:new).with(no_args).exactly(3).times.and_return(mutex)
|
201
|
+
# once for memoize(:add) in the definition
|
202
|
+
subject.memoize(:increment)
|
203
|
+
subject.memoize(:exception)
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|