in_threads 1.4.0 → 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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