restify 3.0.0.rc2 → 3.0.0.rc3

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.
@@ -4,6 +4,26 @@ require 'spec_helper'
4
4
 
5
5
  describe Restify::Promise do
6
6
  let(:promise) { described_class.new }
7
+ let(:threads) { [] }
8
+
9
+ after do
10
+ threads.each(&:kill).each(&:join)
11
+ end
12
+
13
+ def pending_promise
14
+ writer = nil
15
+ promise = described_class.create {|w| writer = w }
16
+ [promise, writer]
17
+ end
18
+
19
+ # Run the block in another thread, and return once the thread blocks,
20
+ # e.g. waiting on a promise.
21
+ def blocking(&)
22
+ thread = Thread.new(&)
23
+ threads << thread
24
+ Thread.pass until thread.stop?
25
+ thread
26
+ end
7
27
 
8
28
  describe 'factory methods' do
9
29
  describe '#fulfilled' do
@@ -118,24 +138,21 @@ describe Restify::Promise do
118
138
  end
119
139
  end
120
140
 
121
- context 'when fulfilling the promise asynchronously' do
122
- subject(:promise) do
123
- described_class.create do |writer|
124
- Thread.new do
125
- sleep 0.1
126
- writer.fulfill 42
127
- end
128
- end
129
- end
130
-
141
+ context 'when fulfilling the promise later' do
131
142
  it 'returns a pending promise' do
143
+ promise, = pending_promise
144
+
132
145
  expect(promise.fulfilled?).to be false
133
146
  expect(promise.rejected?).to be false
134
147
  expect(promise.pending?).to be true
135
148
  end
136
149
 
137
150
  it 'waits for the fulfillment value' do
138
- expect(promise.value!).to eq 42
151
+ promise, writer = pending_promise
152
+ waiter = blocking { promise.value! }
153
+
154
+ writer.fulfill 42
155
+ expect(waiter.value).to eq 42
139
156
  end
140
157
  end
141
158
  end
@@ -160,6 +177,23 @@ describe Restify::Promise do
160
177
  end
161
178
  end
162
179
 
180
+ context 'with a task raising an exception' do
181
+ let(:task) { proc { raise ArgumentError.new('kaboom') } }
182
+
183
+ it 'rejects the promise with it' do
184
+ expect { result }.to raise_error ArgumentError, 'kaboom'
185
+ end
186
+ end
187
+
188
+ # No exception must escape from a task, not even a non-StandardError.
189
+ context 'with a task raising a non-StandardError' do
190
+ let(:task) { proc { raise NotImplementedError.new('kaboom') } }
191
+
192
+ it 'rejects the promise with it' do
193
+ expect { result }.to raise_error NotImplementedError, 'kaboom'
194
+ end
195
+ end
196
+
163
197
  context 'with dependencies, but no task' do
164
198
  let(:dependencies) do
165
199
  [
@@ -215,7 +249,7 @@ describe Restify::Promise do
215
249
 
216
250
  describe '#wait' do
217
251
  it 'can time out' do
218
- expect { promise.wait(0.1) }.to raise_error Timeout::Error
252
+ expect { promise.wait(0.01) }.to raise_error Timeout::Error
219
253
  end
220
254
 
221
255
  context 'with a driver' do
@@ -240,19 +274,16 @@ describe Restify::Promise do
240
274
 
241
275
  it 'waits as usual when the driver declines' do
242
276
  allow(driver).to receive(:drive).and_return(false)
277
+ waiter = blocking { promise.value! }
243
278
 
244
- Thread.new do
245
- sleep 0.05
246
- writer.fulfill 42
247
- end
248
-
249
- expect(promise.value!).to eq 42
279
+ writer.fulfill 42
280
+ expect(waiter.value).to eq 42
250
281
  end
251
282
 
252
283
  it 'times out when the driver returns without completing it' do
253
284
  allow(driver).to receive(:drive).and_return(true)
254
285
 
255
- expect { promise.wait(0.1) }.to raise_error Timeout::Error
286
+ expect { promise.wait(0.01) }.to raise_error Timeout::Error
256
287
  end
257
288
 
258
289
  it 'does not call the driver for a complete promise' do
@@ -264,15 +295,181 @@ describe Restify::Promise do
264
295
  end
265
296
  end
266
297
 
298
+ describe 'timeouts' do
299
+ it 'uses the default timeout' do
300
+ previous = Restify::Timeout.default_timeout
301
+ Restify::Timeout.default_timeout = 0.01
302
+
303
+ expect { promise.wait }.to raise_error Restify::Timeout::Error
304
+ ensure
305
+ Restify::Timeout.default_timeout = previous
306
+ end
307
+
308
+ it 'does not reject the promise' do
309
+ dependency, writer = pending_promise
310
+ chained = dependency.then {|v| v + 1 }
311
+
312
+ expect { chained.value!(0.01) }.to raise_error Restify::Timeout::Error
313
+ expect(chained).to be_pending
314
+
315
+ writer.fulfill 1
316
+ expect(chained.value!).to eq 2
317
+ end
318
+
319
+ it 'does not run the task again after a timeout' do
320
+ calls = 0
321
+ inner, writer = pending_promise
322
+ chained = described_class.fulfilled(1).then do
323
+ calls += 1
324
+ inner
325
+ end
326
+
327
+ expect { chained.value!(0.01) }.to raise_error Restify::Timeout::Error
328
+
329
+ writer.fulfill 2
330
+ expect(chained.value!).to eq 2
331
+ expect(calls).to eq 1
332
+ end
333
+
334
+ it 'times out while another thread runs the task' do
335
+ dependency, writer = pending_promise
336
+ chained = dependency.then {|v| v + 1 }
337
+ running = blocking { chained.value! }
338
+
339
+ expect { chained.value!(0.01) }.to raise_error Restify::Timeout::Error
340
+ expect(running).to be_alive
341
+
342
+ writer.fulfill 1
343
+ expect(running.value).to eq 2
344
+ end
345
+
346
+ # Giving up after a timeout releases the claim the same way.
347
+ it 'lets a waiting thread take over when the running thread is killed' do
348
+ dependency, writer = pending_promise
349
+ chained = dependency.then {|v| v + 1 }
350
+ running = blocking { chained.value! }
351
+ waiting = blocking { chained.value! }
352
+
353
+ running.kill.join
354
+
355
+ writer.fulfill 1
356
+ expect(waiting.value).to eq 2
357
+ end
358
+
359
+ it 'runs the task once for concurrent waiters' do
360
+ dependency, writer = pending_promise
361
+ calls = Queue.new
362
+ chained = dependency.then {|v| calls << v }
363
+ waiters = Array.new(5) { blocking { chained.value! } }
364
+
365
+ writer.fulfill 1
366
+ waiters.each(&:join)
367
+ expect(calls.size).to eq 1
368
+ end
369
+ end
370
+
371
+ describe '#then' do
372
+ it 'waits on a promise returned from the task' do
373
+ chained = described_class.fulfilled(1).then do |v|
374
+ described_class.fulfilled(v + 1)
375
+ end
376
+
377
+ expect(chained.value!).to eq 2
378
+ end
379
+
380
+ it 'rejects with the reason of a rejected dependency' do
381
+ chained = described_class.rejected(ArgumentError.new('nope')).then { 42 }
382
+
383
+ expect { chained.value! }.to raise_error ArgumentError, 'nope'
384
+ end
385
+
386
+ it 'rejects with the reason of a promise returned from the task' do
387
+ chained = described_class.fulfilled(1).then do
388
+ described_class.rejected(ArgumentError.new('nope'))
389
+ end
390
+
391
+ expect { chained.value! }.to raise_error ArgumentError, 'nope'
392
+ end
393
+
394
+ it 'runs lazily' do
395
+ calls = 0
396
+ chained = described_class.fulfilled(1).then { calls += 1 }
397
+
398
+ expect(calls).to eq 0
399
+ chained.value!
400
+ expect(calls).to eq 1
401
+ end
402
+ end
403
+
404
+ describe '#add_observer' do
405
+ let(:writers) { [] }
406
+ let(:promise) { described_class.create {|w| writers << w } }
407
+ let(:writer) { writers.first }
408
+
409
+ before { promise }
410
+
411
+ it 'is called when fulfilled' do
412
+ calls = []
413
+ promise.add_observer {|value, reason| calls << [value, reason] }
414
+
415
+ expect(calls).to eq []
416
+ writer.fulfill 42
417
+ expect(calls).to eq [[42, nil]]
418
+ end
419
+
420
+ it 'is called when rejected' do
421
+ error = ArgumentError.new
422
+ calls = []
423
+ promise.add_observer {|value, reason| calls << [value, reason] }
424
+
425
+ writer.reject error
426
+ expect(calls).to eq [[nil, error]]
427
+ end
428
+
429
+ it 'is called right away when already complete' do
430
+ calls = []
431
+ writer.fulfill 42
432
+
433
+ promise.add_observer {|value, _| calls << value }
434
+ expect(calls).to eq [42]
435
+ end
436
+
437
+ it 'calls later observers when one raises' do
438
+ calls = []
439
+ promise.add_observer { raise 'kaboom' }
440
+ promise.add_observer {|value, _| calls << value }
441
+
442
+ expect { writer.fulfill 42 }.not_to raise_error
443
+ expect(calls).to eq [42]
444
+ end
445
+ end
446
+
447
+ describe 'Writer' do
448
+ let(:writers) { [] }
449
+ let(:promise) { described_class.create {|w| writers << w } }
450
+ let(:writer) { writers.first }
451
+
452
+ before { promise }
453
+
454
+ it 'cannot complete a promise twice' do
455
+ writer.fulfill 1
456
+
457
+ expect { writer.fulfill 2 }.to raise_error Restify::Promise::AlreadyCompleteError
458
+ expect { writer.reject ArgumentError }.to raise_error Restify::Promise::AlreadyCompleteError
459
+ expect { writer.set { 2 } }.to raise_error Restify::Promise::AlreadyCompleteError
460
+ expect(promise.value!).to eq 1
461
+ end
462
+ end
463
+
267
464
  describe '#value' do
268
465
  it 'can time out' do
269
- expect { promise.value(0.1) }.to raise_error Timeout::Error
466
+ expect { promise.value(0.01) }.to raise_error Timeout::Error
270
467
  end
271
468
  end
272
469
 
273
470
  describe '#value!' do
274
471
  it 'can time out' do
275
- expect { promise.value!(0.1) }.to raise_error Timeout::Error
472
+ expect { promise.value!(0.01) }.to raise_error Timeout::Error
276
473
  end
277
474
  end
278
475
  end
@@ -28,6 +28,29 @@ describe Restify::Relation do
28
28
 
29
29
  it { is_expected.to be_a Addressable::URI }
30
30
 
31
+ context 'without template variables' do
32
+ let(:pattern) { '/resource?a=1' }
33
+ let(:params) { {} }
34
+
35
+ it { expect(expanded.to_s).to eq 'http://test.host/resource?a=1' }
36
+
37
+ it 'reuses a frozen URI' do
38
+ expect(expanded).to be_frozen
39
+ expect(relation.expand({})).to be expanded
40
+ end
41
+
42
+ context 'with query parameters' do
43
+ let(:params) { {b: 2} }
44
+
45
+ it { expect(expanded.to_s).to eq 'http://test.host/resource?a=1&b=2' }
46
+
47
+ it 'does not change the reused URI' do
48
+ expanded
49
+ expect(relation.expand({}).to_s).to eq 'http://test.host/resource?a=1'
50
+ end
51
+ end
52
+ end
53
+
31
54
  context 'with nil' do
32
55
  let(:params) { {id: nil} }
33
56
 
@@ -5,61 +5,60 @@ require 'spec_helper'
5
5
  describe Restify::Timeout do
6
6
  let(:timer) { described_class.new(0.2) }
7
7
 
8
- describe '#timeout!' do
9
- context 'before having timed out' do
10
- it 'do nothing' do
11
- expect { timer.timeout! }.not_to raise_error
12
- end
8
+ describe '#remaining' do
9
+ it 'returns the seconds left' do
10
+ expect(timer.remaining).to be_within(0.05).of(0.2)
13
11
  end
14
12
 
15
- context 'after having timed out' do
16
- it 'calls given block' do
17
- expect { timer.timeout! }.not_to raise_error
18
- sleep timer.send(:wait_interval)
19
- expect { timer.timeout! }.to raise_error Restify::Timeout::Error
20
- end
13
+ it 'is not positive after having timed out' do
14
+ expired = described_class.new(0.01)
15
+ sleep expired.remaining
16
+
17
+ expect(expired.remaining).not_to be_positive
21
18
  end
22
19
  end
23
20
 
24
- describe '#remaining' do
25
- it 'returns the seconds left' do
26
- expect(timer.remaining).to be_within(0.05).of(0.2)
21
+ describe '#exception' do
22
+ it 'names the target and duration' do
23
+ error = described_class.new(0.2, :target).exception
24
+
25
+ expect(error).to be_a Restify::Timeout::Error
26
+ expect(error.target).to eq :target
27
+ expect(error.duration).to eq 0.2
28
+ expect(error.message).to eq 'Operation on target timed out after 0.2s'
27
29
  end
28
30
 
29
- it 'is not positive after having timed out' do
30
- sleep timer.remaining
31
- expect(timer.remaining).not_to be_positive
31
+ it 'names the duration without a target' do
32
+ expect(timer.exception.message).to eq 'Operation timed out after 0.2s'
32
33
  end
33
34
  end
34
35
 
35
- describe '#wait_on!' do
36
- it 'calls block on IVar timeout' do
37
- expect do
38
- timer.wait_on!(Concurrent::IVar.new)
39
- end.to raise_error Restify::Timeout::Error
36
+ describe '.new' do
37
+ it 'returns a given timeout' do
38
+ expect(described_class.new(timer)).to be timer
39
+ end
40
+
41
+ it 'uses the default timeout for nil' do
42
+ expect(described_class.new(nil).duration).to eq described_class.default_timeout
43
+ end
44
+
45
+ it 'accepts numeric strings' do
46
+ expect(described_class.new('0.2').duration).to eq 0.2
40
47
  end
41
48
 
42
- it 'calls block on Promise timeout' do
43
- expect do
44
- timer.wait_on!(Restify::Promise.new)
45
- end.to raise_error Restify::Timeout::Error
49
+ it 'rejects infinite values' do
50
+ expect { described_class.new(Float::INFINITY) }.to raise_error ArgumentError, /must be finite/
46
51
  end
47
52
 
48
- it 'does nothing on successful IVar' do
49
- expect do
50
- Concurrent::IVar.new.tap do |ivar|
51
- Thread.new { ivar.set :success }
52
- expect(timer.wait_on!(ivar)).to eq :success
53
- end
54
- end.not_to raise_error
53
+ it 'rejects non-numeric values' do
54
+ expect { described_class.new('soon') }.to raise_error ArgumentError, /must be a number/
55
+ expect { described_class.new(Object.new) }.to raise_error ArgumentError, /must be a number/
55
56
  end
56
57
 
57
- it 'does nothing on successful Promise' do
58
- expect do
59
- Restify::Promise.fulfilled(:success).tap do |p|
60
- expect(timer.wait_on!(p)).to eq :success
61
- end
62
- end.not_to raise_error
58
+ it 'rejects non-positive values' do
59
+ expect { described_class.new(0) }.to raise_error ArgumentError, /must be > 0/
60
+ expect { described_class.new(-1) }.to raise_error ArgumentError, /must be > 0/
61
+ expect { described_class.new(Float::NAN) }.to raise_error ArgumentError, /must be > 0/
63
62
  end
64
63
  end
65
64
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: restify
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.0.rc2
4
+ version: 3.0.0.rc3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jan Graichen
@@ -23,20 +23,6 @@ dependencies:
23
23
  - - "~>"
24
24
  - !ruby/object:Gem::Version
25
25
  version: '2.3'
26
- - !ruby/object:Gem::Dependency
27
- name: concurrent-ruby
28
- requirement: !ruby/object:Gem::Requirement
29
- requirements:
30
- - - "~>"
31
- - !ruby/object:Gem::Version
32
- version: '1.0'
33
- type: :runtime
34
- prerelease: false
35
- version_requirements: !ruby/object:Gem::Requirement
36
- requirements:
37
- - - "~>"
38
- - !ruby/object:Gem::Version
39
- version: '1.0'
40
26
  - !ruby/object:Gem::Dependency
41
27
  name: ethon
42
28
  requirement: !ruby/object:Gem::Requirement
@@ -135,6 +121,10 @@ files:
135
121
  - lib/restify.rb
136
122
  - lib/restify/adapter/base.rb
137
123
  - lib/restify/adapter/ethon.rb
124
+ - lib/restify/adapter/ethon/easy.rb
125
+ - lib/restify/adapter/ethon/event_loop.rb
126
+ - lib/restify/adapter/ethon/options.rb
127
+ - lib/restify/adapter/ethon/pool.rb
138
128
  - lib/restify/adapter/loop_lock.rb
139
129
  - lib/restify/adapter/telemetry.rb
140
130
  - lib/restify/adapter/typhoeus.rb
@@ -158,6 +148,10 @@ files:
158
148
  - lib/restify/version.rb
159
149
  - lib/restify/webmock.rb
160
150
  - spec/restify/adapter/base_spec.rb
151
+ - spec/restify/adapter/ethon/easy_spec.rb
152
+ - spec/restify/adapter/ethon/event_loop_spec.rb
153
+ - spec/restify/adapter/ethon/options_spec.rb
154
+ - spec/restify/adapter/ethon/pool_spec.rb
161
155
  - spec/restify/adapter/ethon_spec.rb
162
156
  - spec/restify/adapter/loop_lock_spec.rb
163
157
  - spec/restify/adapter/telemetry_spec.rb