in_threads 1.4.0 → 1.5.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.
@@ -1,692 +1,746 @@
1
- require 'spec_helper'
1
+ require 'rspec'
2
+ require 'rspec/retry'
2
3
  require 'in_threads'
3
4
 
4
- # Test item with value
5
- class ValueItem
6
- # Use fast_check? for matching
7
- module FastCheckMatcher
8
- def self.===(item)
9
- unless item.respond_to?(:fast_check?)
10
- fail "#{item.inspect} doesn't respont to fast_check?"
11
- end
12
- item.fast_check?
13
- end
14
- end
5
+ RSpec.configure do |config|
6
+ config.order = :random
15
7
 
16
- def initialize(i, value)
17
- @i, @value = i, value
18
- end
8
+ config.verbose_retry = true
19
9
 
20
- def ==(other)
21
- id == other.id if other.is_a?(self.class)
10
+ config.around :each, :flaky do |ex|
11
+ ex.run_with_retry :retry => 3
22
12
  end
13
+ end
23
14
 
24
- def value
25
- sleep
26
- @value
27
- end
15
+ # check if break causes LocalJumpError
16
+ # not in jruby in mri < 1.9
17
+ # https://github.com/jruby/jruby/issues/4697
18
+ SKIP_IF_BREAK_IN_THREAD_IS_IGNORED = begin
19
+ Thread.new{ break }.join
20
+ 'can not handle break in thread'
21
+ rescue LocalJumpError
22
+ false
23
+ end
28
24
 
29
- def check?
30
- sleep
31
- fast_check?
32
- end
25
+ def describe_enum_method(method, &block)
26
+ if Enumerable.method_defined?(method) || method.to_s == 'each'
27
+ describe "##{method}", &block
28
+ else
29
+ describe "##{method}" do
30
+ let(:enum){ 10.times }
33
31
 
34
- def fast_check?
35
- !!@value
32
+ it 'is not defined' do
33
+ expect{ enum.in_threads.send(method) }.
34
+ to raise_error(NoMethodError){ |error| expect(error.name.to_s).to eq(method.to_s) }
35
+ end
36
+ end
36
37
  end
38
+ end
37
39
 
38
- def touch_n_value(*args)
39
- touch(*args); value
40
- end
40
+ class TestObject
41
+ SLEEP_TIME = 0.002
41
42
 
42
- def touch_n_check?(*args)
43
- touch(*args); check?
43
+ def initialize(value)
44
+ @value = value
44
45
  end
45
46
 
46
- protected
47
-
48
- def id
49
- [self.class, @i, @value]
47
+ def fetch
48
+ @value
50
49
  end
51
50
 
52
- private
53
-
54
- def sleep
55
- Kernel.sleep 0.01
51
+ def compute
52
+ wait; @value
56
53
  end
57
- end
58
54
 
59
- # Test item with random value
60
- class RandItem < ValueItem
61
- def initialize(i)
62
- super(i, Kernel.rand)
63
- end
55
+ private
64
56
 
65
- def fast_check?
66
- @value < 0.5
57
+ def wait
58
+ sleep SLEEP_TIME
67
59
  end
68
60
  end
69
61
 
70
- # Pure class with Enumerable instead of Array
71
- class TestEnum
72
- include Enumerable
73
-
74
- def initialize(count, &block)
75
- @items = Array.new(count){ |i| block[i] }
76
- end
62
+ describe InThreads do
63
+ let!(:mutex){ Mutex.new }
77
64
 
78
- def each(&block)
79
- @items.each(&block)
80
- self
65
+ # Check if all threads are joined
66
+ let!(:threads_before){ Thread.list }
67
+ after do
68
+ threads = (Thread.list - threads_before).reject do |thread|
69
+ thread.respond_to?(:backtrace) && thread.backtrace.none?{ |l| l =~ /in_threads/ }
70
+ end
71
+ expect(threads).to eq([]), 'expected all created threads to be joined'
81
72
  end
82
- end
83
73
 
84
- class TestException < StandardError; end
74
+ describe 'initialization' do
75
+ it 'complains about using with non enumerable' do
76
+ expect{ InThreads.new(1) }.to raise_error(ArgumentError)
77
+ end
85
78
 
86
- ENUM_METHODS = Enumerable.instance_methods.map(&:to_s)
87
- def describe_enum_method(method, &block)
88
- if ENUM_METHODS.include?(method)
89
- describe "##{method}", &block
90
- else
91
- describe "##{method}" do
92
- it 'is not defined' do
93
- exception_regexp =
94
- /^undefined method `#{Regexp.escape(method)}' .*\bInThreads\b/
95
- expect{ enum.in_threads.send(method) }.
96
- to raise_error(NoMethodError, exception_regexp)
79
+ [1..10, 10.times, {}, []].each do |o|
80
+ it "does not complain about using with #{o.class}" do
81
+ expect{ InThreads.new(o) }.not_to raise_error
97
82
  end
98
83
  end
99
- end
100
- end
101
84
 
102
- describe 'in_threads' do
103
- let(:items){ Array.new(30){ |i| RandItem.new(i) } }
104
- let(:enum){ TestEnum.new(items) }
105
-
106
- # small coefficient, should be more if sleep time coefficient is bigger
107
- let(:speed_coef){ 0.666 }
85
+ it 'complains about using less than 2 threads' do
86
+ expect{ InThreads.new(10.times, 1) }.to raise_error(ArgumentError)
87
+ end
108
88
 
109
- def measure
110
- start = Time.now
111
- yield
112
- Time.now - start
89
+ it 'does not complain about using 2 or more threads' do
90
+ expect{ InThreads.new(10.times, 2) }.not_to raise_error
91
+ end
113
92
  end
114
93
 
115
- describe 'consistency' do
116
- describe 'verifying params' do
117
- it 'complains about using with non enumerable' do
118
- expect{ InThreads.new(1) }.to raise_error(ArgumentError)
119
- end
94
+ describe '#in_threads' do
95
+ context 'when applied to an instance of InThreads' do
96
+ let(:threaded){ 10.times.in_threads(10) }
97
+ let(:double_threaded){ threaded.in_threads(20) }
120
98
 
121
- [1..10, 10.times, {}, []].each do |o|
122
- it "does not complain about using with #{o.class}" do
123
- expect{ InThreads.new(o) }.not_to raise_error
124
- end
99
+ it 'creates new instance' do
100
+ expect(double_threaded.class).to eq(threaded.class)
101
+ expect(double_threaded.object_id).not_to eq(threaded.object_id)
125
102
  end
126
103
 
127
- it 'complains about using less than 2 threads' do
128
- expect{ 10.times.in_threads(1) }.to raise_error(ArgumentError)
104
+ it 'changes the maximum thread count' do
105
+ expect(threaded.thread_count).to eq(10)
106
+ expect(double_threaded.thread_count).to eq(20)
129
107
  end
130
108
 
131
- it 'does not complain about using 2 or more threads' do
132
- expect{ 10.times.in_threads(2) }.not_to raise_error
109
+ it 'preserves the enumerable' do
110
+ expect(threaded.enumerable).to be(double_threaded.enumerable)
133
111
  end
134
112
  end
113
+ end
135
114
 
136
- describe 'in_threads method' do
137
- it 'does not change existing instance' do
138
- threaded = enum.in_threads(10)
139
- expect{ threaded.in_threads(20) }.not_to change(threaded, :thread_count)
140
- end
141
-
142
- it 'creates new instance with different title when called on '\
143
- 'WithProgress' do
144
- threaded = enum.in_threads(10)
145
- tthreaded = threaded.in_threads(20)
146
- expect(threaded.thread_count).to eq(10)
147
- expect(tthreaded.thread_count).to eq(20)
148
- expect(tthreaded.class).to eq(threaded.class)
149
- expect(tthreaded.object_id).not_to eq(threaded.object_id)
150
- expect(tthreaded.enumerable).to eq(threaded.enumerable)
151
- end
152
- end
115
+ describe 'consistency' do
116
+ let(:enum){ 100.times }
153
117
 
154
- describe 'thread count' do
155
- let(:items){ Array.new(100){ |i| ValueItem.new(i, i < 50) } }
118
+ describe 'runs in specified number of threads' do
119
+ let(:enum){ 40.times }
120
+ let(:threads){ 4 }
156
121
 
157
122
  %w[each map all?].each do |method|
158
- it "runs in specified number of threads for #{method}" do
159
- @thread_count = 0
160
- @max_thread_count = 0
161
- @mutex = Mutex.new
162
- enum.in_threads(4).send(method) do |o|
163
- @mutex.synchronize do
164
- @thread_count += 1
165
- @max_thread_count = [@max_thread_count, @thread_count].max
123
+ it "for ##{method}", :flaky do
124
+ thread_count = 0
125
+ max_thread_count = 0
126
+ enum.in_threads(threads).send(method) do
127
+ mutex.synchronize do
128
+ thread_count += 1
129
+ max_thread_count = [max_thread_count, thread_count].max
166
130
  end
167
- res = o.check?
168
- @mutex.synchronize do
169
- @thread_count -= 1
131
+ sleep TestObject::SLEEP_TIME
132
+ mutex.synchronize do
133
+ thread_count -= 1
170
134
  end
171
- res
172
135
  end
173
- expect(@thread_count).to eq(0)
174
- expect(@max_thread_count).to eq(4)
136
+ expect(thread_count).to eq(0)
137
+ expect(max_thread_count).to eq(threads)
175
138
  end
176
139
  end
177
140
  end
178
141
 
179
- describe 'underlying enumerable usage' do
142
+ describe 'exception/break handling' do
180
143
  %w[each map all?].each do |method|
181
- it "calls underlying enumerable.each only once for #{method}" do
182
- enum = Array.new(100){ |i| ValueItem.new(i, i < 50) }
183
-
184
- expect(enum).to receive(:each).once.and_call_original
185
- enum.in_threads(13).send(method, &:check?)
186
- end
187
- end
144
+ describe "for ##{method}" do
145
+ it 'passes exception raised in block' do
146
+ expect{ enum.in_threads.send(method){ fail 'expected' } }.to raise_error('expected')
147
+ end
188
148
 
189
- it 'does not yield all elements when not needed' do
190
- enum = []
191
- def enum.each
192
- 100.times{ yield 1 }
193
- fail
194
- end
149
+ it 'passes exception raised during iteration' do
150
+ def enum.each
151
+ fail 'expected'
152
+ end
195
153
 
196
- enum.in_threads(13).all?{ false }
197
- end
198
- end
154
+ expect{ enum.in_threads.send(method){} }.to raise_error('expected')
155
+ end
199
156
 
200
- describe 'block arguments' do
201
- before do
202
- def enum.each
203
- yield
204
- yield 1
205
- yield 2, 3
206
- yield 4, 5, 6
207
- end
208
- end
157
+ it 'handles break', :skip => SKIP_IF_BREAK_IN_THREAD_IS_IGNORED do
158
+ expect(enum).not_to receive(:unexpected)
159
+ def enum.each(&block)
160
+ 20.times(&block)
161
+ unexpected
162
+ end
209
163
 
210
- it 'passes all to methods ignoring block result' do
211
- o = double
212
- enum.each do |*args|
213
- expect(o).to receive(:notify).with(args)
214
- end
215
- enum.in_threads.each do |*args|
216
- o.notify(args)
217
- end
218
- end
164
+ value = double
165
+ expect(enum.in_threads(10).send(method) do
166
+ break value
167
+ end).to eq(value)
168
+ end
219
169
 
220
- it 'passes all to methods using block result' do
221
- o = double
222
- enum.each do |*args|
223
- expect(o).to receive(:notify).with(args)
224
- end
225
- enum.in_threads.map do |*args|
226
- o.notify(args)
227
- end
228
- end
229
- end
230
- end
170
+ it 'stops iterating after exception' do
171
+ expect(enum).not_to receive(:unexpected)
172
+ def enum.each(&block)
173
+ 20.times(&block)
174
+ unexpected
175
+ end
231
176
 
232
- describe 'methods' do
233
- missing_methods =
234
- (Enumerable.instance_methods - InThreads.instance_methods).map(&:to_sym)
235
- (missing_methods - InThreads::INCOMPATIBLE_METHODS).each do |method|
236
- pending method
237
- end
177
+ expect do
178
+ enum.in_threads(10).send(method) do |i|
179
+ fail 'expected' if i == 5
180
+ sleep TestObject::SLEEP_TIME
181
+ end
182
+ end.to raise_error('expected')
183
+ end
238
184
 
239
- def check_test_exception(enum, &block)
240
- expect{ block[enum.in_threads] }.to raise_exception(TestException)
241
- expect{ block[enum.in_threads(1000)] }.to raise_exception(TestException)
242
- end
185
+ it 'finishes blocks started before exception' do
186
+ started = []
187
+ finished = []
243
188
 
244
- describe '#each' do
245
- it 'returns same enum after running' do
246
- expect(enum.in_threads.each(&:value)).to eq(enum)
247
- end
189
+ expect do
190
+ enum.in_threads(10).send(method) do |i|
191
+ fail 'expected' if i == 5
192
+ mutex.synchronize{ started << i }
193
+ sleep TestObject::SLEEP_TIME
194
+ mutex.synchronize{ finished << i }
195
+ end
196
+ end.to raise_error('expected')
248
197
 
249
- it 'executes block for each element' do
250
- enum.each{ |o| expect(o).to receive(:touch).once }
251
- enum.in_threads.each(&:touch_n_value)
252
- end
198
+ expect(finished).to match_array(started)
199
+ end
253
200
 
254
- it 'runs faster with threads', :retry => 3 do
255
- expect(measure{ enum.in_threads.each(&:value) }).
256
- to be < measure{ enum.each(&:value) } * speed_coef
257
- end
201
+ context 'exception order' do
202
+ before do
203
+ stub_const('Order', Queue.new)
204
+ end
258
205
 
259
- it 'runs faster with more threads', :retry => 3 do
260
- expect(measure{ enum.in_threads(10).each(&:value) }).
261
- to be < measure{ enum.in_threads(2).each(&:value) } * speed_coef
262
- end
206
+ it 'passes exception raised during iteration if it happens earlier than in block' do
207
+ def enum.each(&block)
208
+ 5.times(&block)
209
+ begin
210
+ fail 'expected'
211
+ ensure
212
+ Order.push nil
213
+ end
214
+ end
215
+
216
+ expect do
217
+ enum.in_threads(10).send(method) do
218
+ Thread.pass while Order.empty?
219
+ sleep TestObject::SLEEP_TIME
220
+ fail 'unexpected'
221
+ end
222
+ end.to raise_error('expected')
223
+ end
263
224
 
264
- it 'returns same enum without block' do
265
- expect(enum.in_threads.each.to_a).to eq(enum.each.to_a)
266
- end
225
+ it 'passes exception raised in block if it happens earlier than during iteration' do
226
+ def enum.each(&block)
227
+ 5.times(&block)
228
+ Thread.pass while Order.empty?
229
+ sleep TestObject::SLEEP_TIME
230
+ fail 'unexpected'
231
+ end
232
+
233
+ expect do
234
+ enum.in_threads(10).send(method) do
235
+ begin
236
+ fail 'expected'
237
+ ensure
238
+ Order.push nil
239
+ end
240
+ end
241
+ end.to raise_error('expected')
242
+ end
267
243
 
268
- it 'raises exception in outer thread' do
269
- check_test_exception(enum) do |threaded|
270
- threaded.each{ fail TestException }
244
+ it 'passes first exception raised in block' do
245
+ expect do
246
+ enum.in_threads(10).send(method) do |i|
247
+ if i == 5
248
+ begin
249
+ fail 'expected'
250
+ ensure
251
+ Order.push nil
252
+ end
253
+ else
254
+ Thread.pass while Order.empty?
255
+ sleep TestObject::SLEEP_TIME
256
+ fail 'unexpected'
257
+ end
258
+ end
259
+ end.to raise_error('expected')
260
+ end
261
+ end
271
262
  end
272
263
  end
273
264
  end
274
265
 
275
- %w[each_with_index enum_with_index].each do |method|
276
- describe_enum_method method do
277
- let(:runner){ proc{ |o, _i| o.value } }
266
+ it 'does not yield all elements when not needed' do
267
+ expect(enum).not_to receive(:unexpected)
278
268
 
279
- it 'returns same result with threads' do
280
- expect(enum.in_threads.send(method, &runner)).
281
- to eq(enum.send(method, &runner))
282
- end
269
+ def enum.each(&block)
270
+ 100.times(&block)
271
+ unexpected
272
+ end
283
273
 
284
- it 'fires same objects' do
285
- enum.send(method){ |o, i| expect(o).to receive(:touch).with(i).once }
286
- enum.in_threads.send(method){ |o, i| o.touch_n_value(i) }
287
- end
274
+ enum.in_threads(10).all?{ false }
275
+ end
288
276
 
289
- it 'runs faster with threads', :retry => 3 do
290
- expect(measure{ enum.in_threads.send(method, &runner) }).
291
- to be < measure{ enum.send(method, &runner) } * speed_coef
277
+ describe 'calls underlying enumerable #each only once' do
278
+ %w[each map all?].each do |method|
279
+ it "for ##{method}" do
280
+ expect(enum).to receive(:each).once.and_call_original
281
+ enum.in_threads.send(method){ sleep TestObject::SLEEP_TIME }
292
282
  end
283
+ end
284
+ end
293
285
 
294
- it 'returns same enum without block' do
295
- expect(enum.in_threads.send(method).to_a).
296
- to eq(enum.send(method).to_a)
297
- end
286
+ describe 'block arguments' do
287
+ %w[each map all? each_entry each_with_index].each do |method|
288
+ describe_enum_method method do
289
+ it 'passes arguments as for not threaded call' do
290
+ enum = Class.new do
291
+ include Enumerable
292
+
293
+ def each
294
+ yield
295
+ yield 1
296
+ yield 2, 3
297
+ yield 4, 5, 6
298
+ end
299
+ end.new
300
+
301
+ expected = []
302
+ enum.send(method) do |a, b, c|
303
+ expected << [a, b, c]
304
+ end
298
305
 
299
- it 'raises exception in outer thread' do
300
- check_test_exception(enum) do |threaded|
301
- threaded.send(method){ fail TestException }
306
+ yielded = []
307
+ enum.in_threads.send(method) do |a, b, c|
308
+ mutex.synchronize{ yielded << [a, b, c] }
309
+ end
310
+
311
+ expect(yielded).to match_array(expected)
302
312
  end
303
313
  end
304
314
  end
305
315
  end
316
+ end
306
317
 
307
- describe '#reverse_each' do
308
- it 'returns same result with threads' do
309
- expect(enum.in_threads.reverse_each(&:value)).
310
- to eq(enum.reverse_each(&:value))
311
- end
318
+ describe 'methods' do
319
+ define :be_faster_than do
320
+ coef = 0.666 # small coefficient, should be more if sleep time is bigger
312
321
 
313
- it 'fires same objects in reverse order' do
314
- @order = double('order', :notify => nil)
315
- expect(@order).to receive(:notify).with(items.last).ordered
316
- expect(@order).to receive(:notify).with(items[items.length / 2]).ordered
317
- expect(@order).to receive(:notify).with(items.first).ordered
318
- enum.each{ |o| expect(o).to receive(:touch).once }
319
- @mutex = Mutex.new
320
- enum.in_threads.reverse_each do |o|
321
- @mutex.synchronize{ @order.notify(o) }
322
- o.touch_n_value
323
- end
322
+ def measure
323
+ start = Time.now
324
+ yield
325
+ Time.now - start
324
326
  end
325
327
 
326
- it 'runs faster with threads', :retry => 3 do
327
- expect(measure{ enum.in_threads.reverse_each(&:value) }).
328
- to be < measure{ enum.reverse_each(&:value) } * speed_coef
328
+ match do |actual|
329
+ measure(&actual) < measure(&block_arg) * coef
329
330
  end
330
331
 
331
- it 'returns same enum without block' do
332
- expect(enum.in_threads.reverse_each.to_a).to eq(enum.reverse_each.to_a)
333
- end
332
+ failure_message{ "expected to be faster (coef. #{coef})" }
334
333
 
335
- it 'raises exception in outer thread' do
336
- check_test_exception(enum) do |threaded|
337
- threaded.reverse_each{ fail TestException }
338
- end
339
- end
334
+ supports_block_expectations
340
335
  end
341
336
 
342
- %w[
343
- all? any? none? one?
344
- detect find find_index drop_while take_while
345
- ].each do |method|
346
- describe "##{method}" do
347
- let(:items){ Array.new(100){ |i| ValueItem.new(i, i.odd?) } }
337
+ it 'lists all incompatible methods' do
338
+ expect(InThreads::INCOMPATIBLE_METHODS.sort_by(&:to_s)).
339
+ to include(*(
340
+ Enumerable.instance_methods.map(&:to_sym) -
341
+ InThreads.public_instance_methods(false).map(&:to_sym)
342
+ ).sort_by(&:to_s))
343
+ end
348
344
 
349
- it 'returns same result with threads' do
350
- expect(enum.in_threads.send(method, &:check?)).
351
- to eq(enum.send(method, &:check?))
352
- end
345
+ context 'threaded' do
346
+ let(:item_count){ 40 }
347
+ let(:value_proc){ proc{ rand } }
348
+ let(:items){ Array.new(item_count){ |i| TestObject.new(value_proc[i]) } }
349
+ let(:enum){ items }
353
350
 
354
- it 'fires same objects but not all' do
355
- a = []
356
- enum.send(method) do |o|
357
- a << o
358
- o.check?
359
- end
351
+ describe_enum_method :each do
352
+ it 'returns same enum after running' do
353
+ expect(enum.in_threads.each(&:compute)).to eq(enum)
354
+ end
360
355
 
361
- @a = []
362
- @mutex = Mutex.new
363
- enum.in_threads.send(method) do |o|
364
- @mutex.synchronize{ @a << o }
365
- o.check?
356
+ it 'executes block for each element' do
357
+ yielded = []
358
+ enum.in_threads.each do |item|
359
+ mutex.synchronize{ yielded << item }
366
360
  end
361
+ expect(yielded).to match_array(items)
362
+ end
367
363
 
368
- expect(@a.length).to be >= a.length
369
- expect(@a.length).to be <= items.length * 0.5
364
+ it 'runs faster with threads', :flaky do
365
+ expect{ enum.in_threads.each(&:compute) }.
366
+ to be_faster_than{ enum.each(&:compute) }
370
367
  end
371
368
 
372
- it 'runs faster with threads', :retry => 3 do
373
- boolean = %w[all? drop_while take_while].include?(method)
374
- enum = Array.new(30){ |i| ValueItem.new(i, boolean) }
375
- expect(measure{ enum.in_threads.send(method, &:check?) }).
376
- to be < measure{ enum.send(method, &:check?) } * speed_coef
369
+ it 'runs faster with more threads', :flaky do
370
+ expect{ enum.in_threads(10).each(&:compute) }.
371
+ to be_faster_than{ enum.in_threads(2).each(&:compute) }
377
372
  end
378
373
 
379
- it 'raises exception in outer thread' do
380
- check_test_exception(enum) do |threaded|
381
- threaded.send(method){ fail TestException }
382
- end
374
+ it 'returns same enum without block' do
375
+ expect(enum.in_threads.each.to_a).to eq(enum.each.to_a)
383
376
  end
384
377
  end
385
- end
386
378
 
387
- %w[partition find_all select reject count].each do |method|
388
- describe "##{method}" do
389
- it 'returns same result with threads' do
390
- expect(enum.in_threads.send(method, &:check?)).
391
- to eq(enum.send(method, &:check?))
392
- end
379
+ %w[each_with_index enum_with_index].each do |method|
380
+ describe_enum_method method do
381
+ let(:block){ proc{ |o, _i| o.compute } }
393
382
 
394
- it 'fires same objects' do
395
- enum.send(method){ |o| expect(o).to receive(:touch).once }
396
- enum.in_threads.send(method, &:touch_n_check?)
397
- end
383
+ it 'returns same result with threads' do
384
+ expect(enum.in_threads.send(method, &block)).
385
+ to eq(enum.send(method, &block))
386
+ end
398
387
 
399
- it 'runs faster with threads', :retry => 3 do
400
- expect(measure{ enum.in_threads.send(method, &:check?) }).
401
- to be < measure{ enum.send(method, &:check?) } * speed_coef
402
- end
388
+ it 'yields same objects' do
389
+ yielded = []
390
+ enum.in_threads.send(method) do |o, i|
391
+ mutex.synchronize{ yielded << [o, i] }
392
+ end
393
+ expect(yielded).to match_array(enum.send(method))
394
+ end
403
395
 
404
- it 'raises exception in outer thread' do
405
- check_test_exception(enum) do |threaded|
406
- threaded.send(method){ fail TestException }
396
+ it 'runs faster with threads', :flaky do
397
+ expect{ enum.in_threads.send(method, &block) }.
398
+ to be_faster_than{ enum.send(method, &block) }
399
+ end
400
+
401
+ it 'returns same enum without block' do
402
+ expect(enum.in_threads.send(method).to_a).
403
+ to eq(enum.send(method).to_a)
407
404
  end
408
405
  end
409
406
  end
410
- end
411
407
 
412
- %w[
413
- collect map
414
- group_by max_by min_by minmax_by sort_by
415
- sum uniq
416
- ].each do |method|
417
- describe_enum_method method do
408
+ describe_enum_method :reverse_each do
409
+ let(:item_count){ 100 }
410
+
418
411
  it 'returns same result with threads' do
419
- expect(enum.in_threads.send(method, &:value)).
420
- to eq(enum.send(method, &:value))
412
+ expect(enum.in_threads.reverse_each(&:compute)).
413
+ to eq(enum.reverse_each(&:compute))
421
414
  end
422
415
 
423
- it 'fires same objects' do
424
- enum.send(method){ |o| expect(o).to receive(:touch).once; 0 }
425
- enum.in_threads.send(method, &:touch_n_value)
416
+ it 'yields same objects in reverse order' do
417
+ yielded = []
418
+ enum.in_threads.reverse_each do |o|
419
+ mutex.synchronize{ yielded << o }
420
+ end
421
+
422
+ expect(yielded).to match_array(items)
423
+ expect(yielded.index(items.last)).
424
+ to be < yielded.index(items[items.length / 4])
425
+ expect(yielded.index(items.first)).
426
+ to be > yielded.index(items[-items.length / 4])
426
427
  end
427
428
 
428
- it 'runs faster with threads', :retry => 3 do
429
- expect(measure{ enum.in_threads.send(method, &:value) }).
430
- to be < measure{ enum.send(method, &:value) } * speed_coef
429
+ it 'runs faster with threads', :flaky do
430
+ expect{ enum.in_threads.reverse_each(&:compute) }.
431
+ to be_faster_than{ enum.reverse_each(&:compute) }
431
432
  end
432
433
 
433
- it 'raises exception in outer thread' do
434
- check_test_exception(enum) do |threaded|
435
- threaded.send(method){ fail TestException }
436
- end
434
+ it 'returns same enum without block' do
435
+ expect(enum.in_threads.reverse_each.to_a).
436
+ to eq(enum.reverse_each.to_a)
437
437
  end
438
438
  end
439
- end
440
439
 
441
- %w[each_cons each_slice enum_slice enum_cons].each do |method|
442
- describe_enum_method method do
443
- let(:runner){ proc{ |a| a.each(&:value) } }
440
+ %w[
441
+ all? any? none? one?
442
+ detect find find_index drop_while take_while
443
+ ].each do |method|
444
+ describe_enum_method method do
445
+ let(:value_proc){ proc{ |i| i.odd? } }
444
446
 
445
- it 'fires same objects' do
446
- enum.send(method, 3) do |a|
447
- expect(a.first).to receive(:touch).with(a).once
447
+ it 'returns same result with threads' do
448
+ expect(enum.in_threads.send(method, &:compute)).
449
+ to eq(enum.send(method, &:compute))
448
450
  end
449
- enum.in_threads.send(method, 3){ |a| a.first.touch_n_value(a) }
450
- end
451
451
 
452
- it 'returns same with block' do
453
- expect(enum.in_threads.send(method, 3, &runner)).
454
- to eq(enum.send(method, 3, &runner))
455
- end
452
+ it 'yields same objects but not all' do
453
+ expected = []
454
+ enum.send(method) do |o|
455
+ expected << o
456
+ o.compute
457
+ end
456
458
 
457
- it 'runs faster with threads', :retry => 3 do
458
- expect(measure{ enum.in_threads.send(method, 3, &runner) }).
459
- to be < measure{ enum.send(method, 3, &runner) } * speed_coef
460
- end
459
+ yielded = []
460
+ enum.in_threads.send(method) do |o|
461
+ mutex.synchronize{ yielded << o }
462
+ o.compute
463
+ end
461
464
 
462
- it 'returns same without block' do
463
- expect(enum.in_threads.send(method, 3).to_a).
464
- to eq(enum.send(method, 3).to_a)
465
- end
465
+ expect(yielded.length).to be >= expected.length
466
+ expect(yielded.length).to be <= items.length * 0.5
467
+ end
466
468
 
467
- it 'raises exception in outer thread' do
468
- check_test_exception(enum) do |threaded|
469
- threaded.send(method, 3){ fail TestException }
469
+ context 'speed' do
470
+ let(:value_proc) do
471
+ proc{ %w[all? drop_while take_while].include?(method) }
472
+ end
473
+
474
+ it 'runs faster with threads', :flaky do
475
+ expect{ enum.in_threads.send(method, &:compute) }.
476
+ to be_faster_than{ enum.send(method, &:compute) }
477
+ end
470
478
  end
471
479
  end
472
480
  end
473
- end
474
481
 
475
- describe '#zip' do
476
- let(:runner){ proc{ |a| a.each(&:value) } }
482
+ %w[partition find_all select reject count].each do |method|
483
+ describe_enum_method method do
484
+ let(:value_proc){ proc{ rand < 0.5 } }
477
485
 
478
- it 'fires same objects' do
479
- enum.zip(enum, enum) do |a|
480
- expect(a.first).to receive(:touch).with(a).once
481
- end
486
+ it 'returns same result with threads' do
487
+ expect(enum.in_threads.send(method, &:compute)).
488
+ to eq(enum.send(method, &:compute))
489
+ end
482
490
 
483
- enum.in_threads.zip(enum, enum) do |a|
484
- a.first.touch_n_value(a)
485
- end
486
- end
491
+ it 'yields same objects' do
492
+ yielded = []
493
+ enum.in_threads.send(method) do |o|
494
+ mutex.synchronize{ yielded << o }
495
+ end
496
+ expect(yielded).to match_array(items)
497
+ end
487
498
 
488
- it 'returns same with block' do
489
- expect(enum.in_threads.zip(enum, enum, &runner)).
490
- to eq(enum.zip(enum, enum, &runner))
499
+ it 'runs faster with threads', :flaky do
500
+ expect{ enum.in_threads.send(method, &:compute) }.
501
+ to be_faster_than{ enum.send(method, &:compute) }
502
+ end
503
+ end
491
504
  end
492
505
 
493
- it 'runs faster with threads', :retry => 3 do
494
- expect(measure{ enum.in_threads.zip(enum, enum, &runner) }).
495
- to be < measure{ enum.zip(enum, enum, &runner) } * speed_coef
496
- end
506
+ %w[
507
+ collect map
508
+ group_by max_by min_by minmax_by sort_by
509
+ sum uniq
510
+ ].each do |method|
511
+ describe_enum_method method do
512
+ it 'returns same result with threads' do
513
+ expect(enum.in_threads.send(method, &:compute)).
514
+ to eq(enum.send(method, &:compute))
515
+ end
497
516
 
498
- it 'returns same without block' do
499
- expect(enum.in_threads.zip(enum, enum)).to eq(enum.zip(enum, enum))
500
- end
517
+ it 'yields same objects' do
518
+ yielded = []
519
+ enum.in_threads.send(method) do |o|
520
+ mutex.synchronize{ yielded << o }
521
+ o.compute
522
+ end
523
+ expect(yielded).to match_array(items)
524
+ end
501
525
 
502
- it 'raises exception in outer thread' do
503
- check_test_exception(enum) do |threaded|
504
- threaded.zip(enum, enum){ fail TestException }
526
+ it 'runs faster with threads', :flaky do
527
+ expect{ enum.in_threads.send(method, &:compute) }.
528
+ to be_faster_than{ enum.send(method, &:compute) }
529
+ end
505
530
  end
506
531
  end
507
- end
508
532
 
509
- describe '#cycle' do
510
- it 'fires same objects' do
511
- enum.cycle(1){ |o| expect(o).to receive(:touch).exactly(3).times }
512
- enum.in_threads.cycle(3, &:touch_n_value)
513
- end
533
+ %w[each_cons each_slice enum_slice enum_cons].each do |method|
534
+ describe_enum_method method do
535
+ let(:block){ proc{ |a| a.each(&:compute) } }
514
536
 
515
- it 'runs faster with threads', :retry => 3 do
516
- expect(measure{ enum.in_threads.cycle(3, &:value) }).
517
- to be < measure{ enum.cycle(3, &:value) } * speed_coef
518
- end
537
+ it 'yields same objects' do
538
+ yielded = []
539
+ enum.in_threads.send(method, 3) do |a|
540
+ mutex.synchronize{ yielded << a }
541
+ end
542
+ expect(yielded).to match_array(items.send(method, 3))
543
+ end
519
544
 
520
- it 'returns same enum without block' do
521
- expect(enum.in_threads.cycle(3).to_a).to eq(enum.cycle(3).to_a)
522
- end
545
+ it 'returns same with block' do
546
+ expect(enum.in_threads.send(method, 3, &block)).
547
+ to eq(enum.send(method, 3, &block))
548
+ end
549
+
550
+ it 'runs faster with threads', :flaky do
551
+ expect{ enum.in_threads.send(method, 3, &block) }.
552
+ to be_faster_than{ enum.send(method, 3, &block) }
553
+ end
523
554
 
524
- it 'raises exception in outer thread' do
525
- check_test_exception(enum) do |threaded|
526
- threaded.cycle{ fail TestException }
555
+ it 'returns same without block' do
556
+ expect(enum.in_threads.send(method, 3).to_a).
557
+ to eq(enum.send(method, 3).to_a)
558
+ end
527
559
  end
528
560
  end
529
- end
530
561
 
531
- %w[grep grep_v].each do |method|
532
- describe_enum_method method do
533
- let(:matcher){ ValueItem::FastCheckMatcher }
562
+ describe_enum_method :zip do
563
+ let(:block){ proc{ |a| a.each(&:compute) } }
534
564
 
535
- it 'fires same objects' do
536
- enum.each do |o|
537
- if o.fast_check? == (method == 'grep')
538
- expect(o).to receive(:touch)
539
- else
540
- expect(o).not_to receive(:touch)
541
- end
565
+ it 'yields same objects' do
566
+ yielded = []
567
+ enum.in_threads.zip(enum, enum) do |a|
568
+ mutex.synchronize{ yielded << a }
542
569
  end
543
- enum.in_threads.send(method, matcher, &:touch_n_value)
570
+ expect(yielded).to match_array(enum.zip(enum, enum))
544
571
  end
545
572
 
546
573
  it 'returns same with block' do
547
- expect(enum.in_threads.send(method, matcher, &:value)).
548
- to eq(enum.send(method, matcher, &:value))
574
+ expect(enum.in_threads.zip(enum, enum, &block)).
575
+ to eq(enum.zip(enum, enum, &block))
549
576
  end
550
577
 
551
- it 'runs faster with threads', :retry => 3 do
552
- expect(measure{ enum.in_threads.send(method, matcher, &:value) }).
553
- to be < measure{ enum.send(method, matcher, &:value) } * speed_coef
578
+ it 'runs faster with threads', :flaky do
579
+ expect{ enum.in_threads.zip(enum, enum, &block) }.
580
+ to be_faster_than{ enum.zip(enum, enum, &block) }
554
581
  end
555
582
 
556
583
  it 'returns same without block' do
557
- expect(enum.in_threads.send(method, matcher)).
558
- to eq(enum.send(method, matcher))
584
+ expect(enum.in_threads.zip(enum, enum)).
585
+ to eq(enum.zip(enum, enum))
559
586
  end
587
+ end
560
588
 
561
- it 'raises exception in outer thread' do
562
- check_test_exception(enum) do |threaded|
563
- threaded.send(method, matcher){ fail TestException }
589
+ describe_enum_method :cycle do
590
+ it 'yields same objects' do
591
+ yielded = []
592
+ enum.in_threads.cycle(3) do |o|
593
+ mutex.synchronize{ yielded << o }
564
594
  end
595
+ expect(yielded).to match_array(enum.cycle(3))
565
596
  end
566
- end
567
- end
568
597
 
569
- describe_enum_method 'each_entry' do
570
- before do
571
- def enum.each
572
- 10.times{ yield }
573
- 10.times{ yield 1 }
574
- 10.times{ yield 2, 3 }
575
- 10.times{ yield 4, 5, 6 }
598
+ it 'runs faster with threads', :flaky do
599
+ expect{ enum.in_threads.cycle(3, &:compute) }.
600
+ to be_faster_than{ enum.cycle(3, &:compute) }
576
601
  end
577
- end
578
- let(:runner){ proc{ |o| ValueItem.new(0, o).value } }
579
602
 
580
- it 'returns same result with threads' do
581
- expect(enum.in_threads.each_entry(&runner)).
582
- to eq(enum.each_entry(&runner))
583
- end
584
-
585
- it 'executes block for each element' do
586
- @o = double('order')
587
- enum.each_entry do |*o|
588
- expect(@o).to receive(:notify).with(o)
589
- end
590
- @mutex = Mutex.new
591
- enum.in_threads.each_entry do |*o|
592
- @mutex.synchronize{ @o.notify(o) }
593
- runner[]
603
+ it 'returns same enum without block' do
604
+ expect(enum.in_threads.cycle(3).to_a).
605
+ to eq(enum.cycle(3).to_a)
594
606
  end
595
607
  end
596
608
 
597
- it 'runs faster with threads', :retry => 3 do
598
- expect(measure{ enum.in_threads.each_entry(&runner) }).
599
- to be < measure{ enum.each_entry(&runner) } * speed_coef
600
- end
609
+ %w[grep grep_v].each do |method|
610
+ describe_enum_method method do
611
+ let(:value_proc){ proc{ rand < 0.5 } }
601
612
 
602
- it 'returns same enum without block' do
603
- expect(enum.in_threads.each_entry.to_a).to eq(enum.each_entry.to_a)
604
- end
613
+ let(:matcher) do
614
+ double.tap do |matcher|
615
+ def matcher.===(item)
616
+ item.fetch
617
+ end
618
+ end
619
+ end
620
+
621
+ it 'yields same objects' do
622
+ yielded = []
623
+ enum.in_threads.send(method, matcher) do |item|
624
+ mutex.synchronize{ yielded << item }
625
+ end
626
+ expect(yielded).to match_array(enum.send(method, matcher))
627
+ end
628
+
629
+ it 'returns same with block' do
630
+ expect(enum.in_threads.send(method, matcher, &:compute)).
631
+ to eq(enum.send(method, matcher, &:compute))
632
+ end
633
+
634
+ it 'runs faster with threads', :flaky do
635
+ expect{ enum.in_threads.send(method, matcher, &:compute) }.
636
+ to be_faster_than{ enum.send(method, matcher, &:compute) }
637
+ end
605
638
 
606
- it 'raises exception in outer thread' do
607
- check_test_exception(enum) do |threaded|
608
- threaded.each_entry{ fail TestException }
639
+ it 'returns same without block' do
640
+ expect(enum.in_threads.send(method, matcher)).
641
+ to eq(enum.send(method, matcher))
642
+ end
609
643
  end
610
644
  end
611
- end
612
645
 
613
- %w[flat_map collect_concat].each do |method|
614
- describe_enum_method method do
615
- let(:items){ Array.new(20){ |i| RandItem.new(i) }.each_slice(3).to_a }
616
- let(:runner){ proc{ |a| a.map(&:value) } }
646
+ describe_enum_method :each_entry do
647
+ before do
648
+ def enum.each
649
+ (count / 3).times do
650
+ yield
651
+ yield 1
652
+ yield 2, 3
653
+ end
654
+ end
655
+ end
656
+ let(:block){ proc{ |o| TestObject.new(o).compute } }
617
657
 
618
658
  it 'returns same result with threads' do
619
- expect(enum.in_threads.send(method, &runner)).
620
- to eq(enum.send(method, &runner))
659
+ expect(enum.in_threads.each_entry(&block)).
660
+ to eq(enum.each_entry(&block))
621
661
  end
622
662
 
623
- it 'fires same objects' do
624
- enum.send(method) do |a|
625
- a.each do |o|
626
- expect(o).to receive(:touch).with(a).once
627
- end
663
+ it 'executes block for each element' do
664
+ expected = []
665
+ enum.each_entry do |*o|
666
+ expected << o
628
667
  end
629
668
 
630
- enum.in_threads.send(method) do |a|
631
- a.each do |o|
632
- o.touch_n_value(a)
633
- end
669
+ yielded = []
670
+ enum.in_threads.each_entry do |*o|
671
+ mutex.synchronize{ yielded << o }
634
672
  end
673
+
674
+ expect(yielded).to match_array(expected)
635
675
  end
636
676
 
637
- it 'runs faster with threads', :retry => 3 do
638
- expect(measure{ enum.in_threads.send(method, &runner) }).
639
- to be < measure{ enum.send(method, &runner) } * speed_coef
677
+ it 'runs faster with threads', :flaky do
678
+ expect{ enum.in_threads.each_entry(&block) }.
679
+ to be_faster_than{ enum.each_entry(&block) }
640
680
  end
641
681
 
642
682
  it 'returns same enum without block' do
643
- expect(enum.in_threads.send(method).to_a).
644
- to eq(enum.send(method).to_a)
683
+ expect(enum.in_threads.each_entry.to_a).
684
+ to eq(enum.each_entry.to_a)
645
685
  end
686
+ end
687
+
688
+ %w[flat_map collect_concat].each do |method|
689
+ describe_enum_method method do
690
+ let(:items){ super().each_slice(3).to_a }
691
+ let(:block){ proc{ |a| a.map(&:compute) } }
646
692
 
647
- it 'raises exception in outer thread' do
648
- check_test_exception(enum) do |threaded|
649
- threaded.send(method){ fail TestException }
693
+ it 'returns same result with threads' do
694
+ expect(enum.in_threads.send(method, &block)).
695
+ to eq(enum.send(method, &block))
696
+ end
697
+
698
+ it 'yields same objects' do
699
+ yielded = []
700
+ enum.in_threads.send(method) do |a|
701
+ mutex.synchronize{ yielded << a }
702
+ end
703
+ expect(yielded).to match_array(items)
704
+ end
705
+
706
+ it 'runs faster with threads', :flaky do
707
+ expect{ enum.in_threads.send(method, &block) }.
708
+ to be_faster_than{ enum.send(method, &block) }
709
+ end
710
+
711
+ it 'returns same enum without block' do
712
+ expect(enum.in_threads.send(method).to_a).
713
+ to eq(enum.send(method).to_a)
650
714
  end
651
715
  end
652
716
  end
653
717
  end
654
718
 
655
719
  context 'unthreaded' do
720
+ let(:enum){ (1..10).each }
721
+
656
722
  %w[inject reduce].each do |method|
657
- describe "##{method}" do
723
+ describe_enum_method method do
658
724
  it 'returns same result' do
659
- combiner = proc{ |memo, o| memo + o.value }
725
+ combiner = proc{ |memo, o| memo + o }
660
726
  expect(enum.in_threads.send(method, 0, &combiner)).
661
727
  to eq(enum.send(method, 0, &combiner))
662
728
  end
663
-
664
- it 'raises exception in outer thread' do
665
- check_test_exception(enum) do |threaded|
666
- threaded.send(method){ fail TestException }
667
- end
668
- end
669
729
  end
670
730
  end
671
731
 
672
732
  %w[max min minmax sort].each do |method|
673
- describe "##{method}" do
733
+ describe_enum_method method do
674
734
  it 'returns same result' do
675
- comparer = proc{ |a, b| a.value <=> b.value }
735
+ comparer = proc{ |a, b| a <=> b }
676
736
  expect(enum.in_threads.send(method, &comparer)).
677
737
  to eq(enum.send(method, &comparer))
678
738
  end
679
-
680
- it 'raises exception in outer thread' do
681
- check_test_exception(enum) do |threaded|
682
- threaded.send(method){ fail TestException }
683
- end
684
- end
685
739
  end
686
740
  end
687
741
 
688
742
  %w[to_a entries].each do |method|
689
- describe "##{method}" do
743
+ describe_enum_method method do
690
744
  it 'returns same result' do
691
745
  expect(enum.in_threads.send(method)).to eq(enum.send(method))
692
746
  end
@@ -694,7 +748,7 @@ describe 'in_threads' do
694
748
  end
695
749
 
696
750
  %w[drop take].each do |method|
697
- describe "##{method}" do
751
+ describe_enum_method method do
698
752
  it 'returns same result' do
699
753
  expect(enum.in_threads.send(method, 2)).to eq(enum.send(method, 2))
700
754
  end
@@ -702,7 +756,7 @@ describe 'in_threads' do
702
756
  end
703
757
 
704
758
  %w[first].each do |method|
705
- describe "##{method}" do
759
+ describe_enum_method method do
706
760
  it 'returns same result' do
707
761
  expect(enum.in_threads.send(method)).to eq(enum.send(method))
708
762
  expect(enum.in_threads.send(method, 3)).to eq(enum.send(method, 3))
@@ -711,40 +765,25 @@ describe 'in_threads' do
711
765
  end
712
766
 
713
767
  %w[include? member?].each do |method|
714
- describe "##{method}" do
768
+ describe_enum_method method do
715
769
  it 'returns same result' do
716
- expect(enum.in_threads.send(method, items[10])).
717
- to eq(enum.send(method, items[10]))
770
+ expect(enum.in_threads.send(method, enum.to_a[10])).to eq(enum.send(method, enum.to_a[10]))
718
771
  end
719
772
  end
720
773
  end
721
774
 
722
- describe_enum_method 'each_with_object' do
723
- let(:runner){ proc{ |o, h| h[o.value] = true } }
775
+ describe_enum_method :each_with_object do
776
+ let(:block){ proc{ |o, h| h[o] = true } }
724
777
 
725
778
  it 'returns same result' do
726
- expect(enum.in_threads.each_with_object({}, &runner)).
727
- to eq(enum.each_with_object({}, &runner))
728
- end
729
-
730
- it 'raises exception in outer thread' do
731
- check_test_exception(enum) do |threaded|
732
- threaded.each_with_object({}){ fail TestException }
733
- end
779
+ expect(enum.in_threads.each_with_object({}, &block)).to eq(enum.each_with_object({}, &block))
734
780
  end
735
781
  end
736
782
 
737
783
  %w[chunk slice_before slice_after].each do |method|
738
784
  describe_enum_method method do
739
785
  it 'returns same result' do
740
- expect(enum.in_threads.send(method, &:check?).to_a).
741
- to eq(enum.send(method, &:check?).to_a)
742
- end
743
-
744
- it 'raises exception in outer thread' do
745
- check_test_exception(enum) do |threaded|
746
- threaded.send(method){ fail TestException }.to_a
747
- end
786
+ expect(enum.in_threads.send(method, &:odd?).to_a).to eq(enum.send(method, &:odd?).to_a)
748
787
  end
749
788
  end
750
789
  end