fire_poll 1.0.1 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +2 -0
- data/README.md +39 -13
- data/lib/fire_poll.rb +45 -5
- data/lib/fire_poll/version.rb +1 -1
- data/test/test_fire_poll.rb +15 -7
- data/test/test_fire_poll_mixin.rb +19 -0
- data/test/test_patiently.rb +89 -0
- metadata +51 -54
data/.gitignore
CHANGED
data/README.md
CHANGED
@@ -2,8 +2,10 @@ Description
|
|
2
2
|
===========
|
3
3
|
`FirePoll.poll` is a method for knowing when something is ready. When your block yields true, execution continues. When your block yields false, poll keeps trying until it gives up and raises an error.
|
4
4
|
|
5
|
-
|
6
|
-
|
5
|
+
`FirePoll.patiently` extends this idea to letting your assertion(s) achieve success after a few tries, if necessary.
|
6
|
+
|
7
|
+
Examples: poll
|
8
|
+
--------------
|
7
9
|
I'm writing a system test for a web application. My test simulates uploading a large file, which isn't instantaneous. I need to know when the file has finished uploading so I can start making assertions.
|
8
10
|
def wait_for_file(filename)
|
9
11
|
FirePoll.poll do
|
@@ -33,6 +35,24 @@ I just fired up a fake web service to respond to my client application. I want t
|
|
33
35
|
end
|
34
36
|
end
|
35
37
|
|
38
|
+
Example: patiently
|
39
|
+
------------------
|
40
|
+
I'm writing tests for my web app which uses a bunch of crazy Ajax to fetch data from a service and populate a table... one row at a time.
|
41
|
+
In real life it takes just a moment to complete, but sometimes one or two of the rows hangs for a second before continuing.
|
42
|
+
|
43
|
+
it "loads the tasks asynchronously and fills the table" do
|
44
|
+
go_to_task_list_page
|
45
|
+
|
46
|
+
patiently do
|
47
|
+
read_task_table_row(1).should == [ "Ride bike", "Done" ]
|
48
|
+
read_task_table_row(2).should == [ "Write code", "Done" ]
|
49
|
+
read_task_table_row(3).should == [ "Go to The Meanwhile", "Todo" ]
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
This test clearly shows what you're interested in, without getting tripped up by delayed Ajax results, but without adding unneeded synchronization or sleep code.
|
54
|
+
|
55
|
+
|
36
56
|
Usage
|
37
57
|
-----
|
38
58
|
Pass a block to `FirePoll.poll`. Return `true` when your need is met. Return `false` when it isn't. `poll` will raise an exception after too many failed attempts.
|
@@ -43,23 +63,29 @@ The `poll` method takes two optional parameters: a specific message to raise on
|
|
43
63
|
FirePoll.poll("waited for too long!", 7) { ... } # raises an error after seven seconds with a specific error message
|
44
64
|
FirePoll.poll(nil, 88) { ... } # raises an error after eighty-eight seconds with the generic error message
|
45
65
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
66
|
+
`FirePoll.patiently` is similar, but instead focuses on error-free execution of arbitrary code or tests. If the passed block runs without raising an error, execution proceeds normally. If an error is raised, the block is rerun after a brief delay, until the block can be run without exceptions. If exceptions continue to raise, `patiently` gives up after a bit (default 5 seconds) by re-raising the most recent exception raised by the block.
|
67
|
+
|
68
|
+
The `FirePoll` module may be mixed into your class via `include` for nicer reading.
|
69
|
+
FirePoll.poll { ... } # returns immedialtely if no errors, or as soon as errors stop
|
70
|
+
FirePoll.poll(10) { ... } # increase patience to 10 seconds
|
71
|
+
FirePoll.poll(20, 3) { ... } # increase patience to 20 seconds, and delay for 3 seconds before retry
|
72
|
+
|
73
|
+
RSpec
|
74
|
+
-----
|
75
|
+
We tend to include the FirePoll module up-front for all our specs:
|
76
|
+
|
77
|
+
RSpec.configure do |config|
|
78
|
+
config.include FirePoll
|
79
|
+
...
|
54
80
|
end
|
55
81
|
|
56
82
|
Implementation
|
57
83
|
--------------
|
58
|
-
`
|
84
|
+
UPDATE v1.2.0 - `poll` and `patiently` are both wall-clock sensitive now, meaning they will not poll longer than their allotted time. This means if your blocks spend significant time determining truth or success, these methods no longer suffer from the multiplicative effects of up-front loop-count calculation.
|
59
85
|
|
60
86
|
Motivation
|
61
87
|
----------
|
62
|
-
We frequently need to wait for something to happen - usually in tests. And we usually don't have any strict time requirements - as long as something happens in _about_ [x] seconds, we're happy. `
|
88
|
+
We frequently need to wait for something to happen - usually in tests. And we usually don't have any strict time requirements - as long as something happens in _about_ [x] seconds, we're happy. `poll` and `patiently` are cover a lot of ground quickly and cleanly.
|
63
89
|
|
64
90
|
On a related note, `Timer::Timeout` is known to be [busted](http://ph7spot.com/musings/system-timer) and [unreliable](http://blog.headius.com/2008/02/rubys-threadraise-threadkill-timeoutrb.html). `FirePoll.poll` doesn't employ any threads or timers, so we don't worry about whether it will work or not.
|
65
91
|
|
@@ -73,5 +99,5 @@ Authors
|
|
73
99
|
* Matt Fletcher (fletcher@atomicobject.com)
|
74
100
|
* David Crosby (crosby@atomicobject.com)
|
75
101
|
* Micah Alles (alles@atomicobject.com)
|
76
|
-
* ©
|
102
|
+
* © 2012 [Atomic Object](http://www.atomicobject.com/)
|
77
103
|
* More Atomic Object [open source](http://www.atomicobject.com/pages/Software+Commons) projects
|
data/lib/fire_poll.rb
CHANGED
@@ -3,20 +3,60 @@ module FirePoll
|
|
3
3
|
# @param [String] msg a custom message raised when polling fails
|
4
4
|
# @param [Numeric] seconds number of seconds to poll
|
5
5
|
# @yield a block that determines whether polling should continue
|
6
|
-
# @
|
7
|
-
# @
|
6
|
+
# @yield return false if polling should continue
|
7
|
+
# @yield return true if polling is complete
|
8
8
|
# @raise [RuntimeError] when polling fails
|
9
9
|
# @return the return value of the passed block
|
10
10
|
# @since 1.0.0
|
11
|
-
def poll(msg=nil, seconds=
|
12
|
-
|
11
|
+
def poll(msg=nil, seconds=nil)
|
12
|
+
seconds ||= 2.0 # 5 seconds overall patience
|
13
|
+
give_up_at = Time.now + seconds # pick a time to stop being patient
|
14
|
+
delay = 0.1 # wait a tenth of a second before re-attempting
|
15
|
+
failure = nil # record the most recent failure
|
16
|
+
|
17
|
+
while Time.now < give_up_at do
|
13
18
|
result = yield
|
14
19
|
return result if result
|
15
|
-
sleep
|
20
|
+
sleep delay
|
16
21
|
end
|
17
22
|
msg ||= "polling failed after #{seconds} seconds"
|
18
23
|
raise msg
|
19
24
|
end
|
20
25
|
|
21
26
|
module_function :poll
|
27
|
+
|
28
|
+
#
|
29
|
+
# Runs a block of code and returns the value.
|
30
|
+
# IF ANYTHING raises in the block due to test failure or error,
|
31
|
+
# the exception will be held, a small delay, then re-try the block.
|
32
|
+
# This patience endures for 5 seconds by default, before the most
|
33
|
+
# recent reason for failure gets re-raised.
|
34
|
+
#
|
35
|
+
# @param [Numeric] seconds Wall-clock number of seconds to be patient, default is 5 seconds
|
36
|
+
# @param [Numeric] delay Seconds to hesitate after encountering a failure, default is 0.1 seconds
|
37
|
+
# @yield a block that will be run, and if it raises an error, re-run until success, or patience runs out
|
38
|
+
# @raise [Exception] the most recent Exception that caused the loop to retry before giving up.
|
39
|
+
# @return the value of the passed block
|
40
|
+
# @since 1.2.0
|
41
|
+
#
|
42
|
+
def patiently(seconds=nil, delay=nil)
|
43
|
+
seconds ||= 5 # 5 seconds overall patience
|
44
|
+
give_up_at = Time.now + seconds # pick a time to stop being patient
|
45
|
+
delay ||= 0.1 # wait a tenth of a second before re-attempting
|
46
|
+
failure = nil # record the most recent failure
|
47
|
+
|
48
|
+
while Time.now < give_up_at do
|
49
|
+
begin
|
50
|
+
return yield
|
51
|
+
rescue Exception => e
|
52
|
+
failure = e
|
53
|
+
sleep delay # avoid spinning like crazy
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
if failure
|
58
|
+
raise failure # if we never got satisfaction, tell the world
|
59
|
+
end
|
60
|
+
end
|
61
|
+
module_function :patiently
|
22
62
|
end
|
data/lib/fire_poll/version.rb
CHANGED
data/test/test_fire_poll.rb
CHANGED
@@ -59,14 +59,22 @@ class FirePollTest < Test::Unit::TestCase
|
|
59
59
|
result = FirePoll.poll { "this is the result of the block" }
|
60
60
|
assert_equal "this is the result of the block", result
|
61
61
|
end
|
62
|
-
end
|
63
62
|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
poll
|
63
|
+
def test_should_pay_attention_to_actual_time_elapsed_when_deciding_whether_to_continue_polling
|
64
|
+
start = Time.now
|
65
|
+
call_count = 0
|
66
|
+
begin
|
67
|
+
# If something inside the block consumes significant time, don't get caught in a repeater
|
68
|
+
FirePoll.poll("woops", 0.5) do
|
69
|
+
call_count += 1
|
70
|
+
sleep 0.35 # more than half
|
71
|
+
false
|
72
|
+
end
|
73
|
+
raise "Didn't expect #poll to return! call_count=#{call_count}"
|
74
|
+
rescue => e
|
75
|
+
assert_equal "woops", e.message
|
76
|
+
assert_equal 2, call_count, "Delaying should have cut the iterations down to 2"
|
70
77
|
end
|
71
78
|
end
|
72
79
|
end
|
80
|
+
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require "test/unit"
|
2
|
+
require "fire_poll"
|
3
|
+
|
4
|
+
class TestFirePollMixin < Test::Unit::TestCase
|
5
|
+
include FirePoll
|
6
|
+
|
7
|
+
def test_poll_can_be_mixed_in
|
8
|
+
assert_nothing_raised do
|
9
|
+
poll { true }
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def test_patiently_can_be_mixed_in
|
14
|
+
assert_nothing_raised do
|
15
|
+
a = patiently { true }
|
16
|
+
assert_equal a, true
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require "test/unit"
|
2
|
+
require "fire_poll"
|
3
|
+
|
4
|
+
class PatientlyTest < Test::Unit::TestCase
|
5
|
+
def test_should_run_block_and_return_value
|
6
|
+
result = FirePoll.patiently do "hi there" end
|
7
|
+
assert_equal "hi there", result
|
8
|
+
end
|
9
|
+
|
10
|
+
def test_should_not_invoke_the_block_more_than_once_if_success
|
11
|
+
call_count = 0
|
12
|
+
result = FirePoll.patiently do
|
13
|
+
call_count += 1
|
14
|
+
"hi again"
|
15
|
+
end
|
16
|
+
assert_equal "hi again", result
|
17
|
+
end
|
18
|
+
|
19
|
+
def test_should_invoke_multiple_times_until_exceptions_cease
|
20
|
+
call_count = 0
|
21
|
+
result = FirePoll.patiently do
|
22
|
+
call_count += 1
|
23
|
+
raise "minor fail" if call_count < 3
|
24
|
+
"ok done"
|
25
|
+
end
|
26
|
+
assert_equal result, "ok done"
|
27
|
+
assert_equal 3, call_count
|
28
|
+
end
|
29
|
+
|
30
|
+
def test_should_invoke_multiple_times_then_raise_last_exception_if_errors_never_cease
|
31
|
+
call_count = 0
|
32
|
+
begin
|
33
|
+
FirePoll.patiently(0.5) do
|
34
|
+
call_count += 1
|
35
|
+
raise "persistent fail"
|
36
|
+
end
|
37
|
+
raise "Didn't expect patiently to return! call_count: #{call_count}"
|
38
|
+
rescue => e
|
39
|
+
assert_equal "persistent fail", e.message
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def test_should_delay_slightly_before_reinvocation
|
44
|
+
ticks = []
|
45
|
+
begin
|
46
|
+
FirePoll.patiently(0.5) do
|
47
|
+
ticks << Time.now
|
48
|
+
raise "more persistent fail"
|
49
|
+
end
|
50
|
+
raise "Didn't expect patiently to return!"
|
51
|
+
rescue
|
52
|
+
ticks.each_cons(2).with_index do |times,i|
|
53
|
+
before,after = times
|
54
|
+
assert ((after-before) > 0.005), "time between reinvocations should be bigger than 50 ms (checking gap #{i})"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def test_should_default_to_5_seconds_max_patience_if_no_timeout_specified
|
60
|
+
start = Time.now
|
61
|
+
begin
|
62
|
+
FirePoll.patiently do
|
63
|
+
raise "big fail"
|
64
|
+
end
|
65
|
+
raise "Didn't expect patiently to return!"
|
66
|
+
rescue
|
67
|
+
span = Time.now-start
|
68
|
+
diff = (5 - span).abs
|
69
|
+
assert diff < 0.05, "Expected about 5 seconds to pass before giving up, got #{span} (diff of #{diff})"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def test_should_pay_attention_to_actual_time_elapsed_when_deciding_patience_is_up
|
74
|
+
start = Time.now
|
75
|
+
call_count = 0
|
76
|
+
begin
|
77
|
+
# If something inside the block consumes significant time, don't get caught in a repeater
|
78
|
+
FirePoll.patiently(0.5) do
|
79
|
+
call_count += 1
|
80
|
+
sleep 0.35 # more than half
|
81
|
+
raise "sleepy fail"
|
82
|
+
end
|
83
|
+
raise "Didn't expect patiently to return! call_count=#{call_count}"
|
84
|
+
rescue
|
85
|
+
assert_equal 2, call_count, "Delaying should have cut the iterations down to 2"
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
metadata
CHANGED
@@ -1,75 +1,71 @@
|
|
1
|
-
--- !ruby/object:Gem::Specification
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
2
|
name: fire_poll
|
3
|
-
version: !ruby/object:Gem::Version
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.2.0
|
4
5
|
prerelease:
|
5
|
-
version: 1.0.1
|
6
6
|
platform: ruby
|
7
|
-
authors:
|
7
|
+
authors:
|
8
8
|
- Matt Fletcher
|
9
9
|
- David Crosby
|
10
10
|
- Micah Alles
|
11
11
|
autorequire:
|
12
12
|
bindir: bin
|
13
13
|
cert_chain: []
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
- !ruby/object:Gem::Dependency
|
14
|
+
date: 2012-03-22 00:00:00.000000000Z
|
15
|
+
dependencies:
|
16
|
+
- !ruby/object:Gem::Dependency
|
18
17
|
name: bundler
|
19
|
-
requirement: &
|
18
|
+
requirement: &2156420820 !ruby/object:Gem::Requirement
|
20
19
|
none: false
|
21
|
-
requirements:
|
22
|
-
- -
|
23
|
-
- !ruby/object:Gem::Version
|
20
|
+
requirements:
|
21
|
+
- - ! '>='
|
22
|
+
- !ruby/object:Gem::Version
|
24
23
|
version: 1.0.0
|
25
24
|
type: :development
|
26
25
|
prerelease: false
|
27
|
-
version_requirements: *
|
28
|
-
- !ruby/object:Gem::Dependency
|
26
|
+
version_requirements: *2156420820
|
27
|
+
- !ruby/object:Gem::Dependency
|
29
28
|
name: rake
|
30
|
-
requirement: &
|
29
|
+
requirement: &2156419160 !ruby/object:Gem::Requirement
|
31
30
|
none: false
|
32
|
-
requirements:
|
33
|
-
- -
|
34
|
-
- !ruby/object:Gem::Version
|
31
|
+
requirements:
|
32
|
+
- - ! '>='
|
33
|
+
- !ruby/object:Gem::Version
|
35
34
|
version: 0.8.0
|
36
35
|
type: :development
|
37
36
|
prerelease: false
|
38
|
-
version_requirements: *
|
39
|
-
- !ruby/object:Gem::Dependency
|
37
|
+
version_requirements: *2156419160
|
38
|
+
- !ruby/object:Gem::Dependency
|
40
39
|
name: yard
|
41
|
-
requirement: &
|
40
|
+
requirement: &2156416240 !ruby/object:Gem::Requirement
|
42
41
|
none: false
|
43
|
-
requirements:
|
42
|
+
requirements:
|
44
43
|
- - ~>
|
45
|
-
- !ruby/object:Gem::Version
|
44
|
+
- !ruby/object:Gem::Version
|
46
45
|
version: 0.6.4
|
47
46
|
type: :development
|
48
47
|
prerelease: false
|
49
|
-
version_requirements: *
|
50
|
-
- !ruby/object:Gem::Dependency
|
48
|
+
version_requirements: *2156416240
|
49
|
+
- !ruby/object:Gem::Dependency
|
51
50
|
name: bluecloth
|
52
|
-
requirement: &
|
51
|
+
requirement: &2156414060 !ruby/object:Gem::Requirement
|
53
52
|
none: false
|
54
|
-
requirements:
|
53
|
+
requirements:
|
55
54
|
- - ~>
|
56
|
-
- !ruby/object:Gem::Version
|
55
|
+
- !ruby/object:Gem::Version
|
57
56
|
version: 2.0.11
|
58
57
|
type: :development
|
59
58
|
prerelease: false
|
60
|
-
version_requirements: *
|
59
|
+
version_requirements: *2156414060
|
61
60
|
description: Simple, brute-force method for knowing when something is ready
|
62
|
-
email:
|
61
|
+
email:
|
63
62
|
- fletcher@atomicobject.com
|
64
63
|
- crosby@atomicobject.com
|
65
64
|
- alles@atomicobject.com
|
66
65
|
executables: []
|
67
|
-
|
68
66
|
extensions: []
|
69
|
-
|
70
67
|
extra_rdoc_files: []
|
71
|
-
|
72
|
-
files:
|
68
|
+
files:
|
73
69
|
- .gitignore
|
74
70
|
- .yardopts
|
75
71
|
- Gemfile
|
@@ -80,38 +76,39 @@ files:
|
|
80
76
|
- lib/fire_poll.rb
|
81
77
|
- lib/fire_poll/version.rb
|
82
78
|
- test/test_fire_poll.rb
|
83
|
-
|
79
|
+
- test/test_fire_poll_mixin.rb
|
80
|
+
- test/test_patiently.rb
|
81
|
+
homepage: ''
|
84
82
|
licenses: []
|
85
|
-
|
86
83
|
post_install_message:
|
87
84
|
rdoc_options: []
|
88
|
-
|
89
|
-
require_paths:
|
85
|
+
require_paths:
|
90
86
|
- lib
|
91
|
-
required_ruby_version: !ruby/object:Gem::Requirement
|
87
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
92
88
|
none: false
|
93
|
-
requirements:
|
94
|
-
- -
|
95
|
-
- !ruby/object:Gem::Version
|
96
|
-
|
97
|
-
segments:
|
89
|
+
requirements:
|
90
|
+
- - ! '>='
|
91
|
+
- !ruby/object:Gem::Version
|
92
|
+
version: '0'
|
93
|
+
segments:
|
98
94
|
- 0
|
99
|
-
|
100
|
-
required_rubygems_version: !ruby/object:Gem::Requirement
|
95
|
+
hash: -2132279313955691794
|
96
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
101
97
|
none: false
|
102
|
-
requirements:
|
103
|
-
- -
|
104
|
-
- !ruby/object:Gem::Version
|
105
|
-
|
106
|
-
segments:
|
98
|
+
requirements:
|
99
|
+
- - ! '>='
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '0'
|
102
|
+
segments:
|
107
103
|
- 0
|
108
|
-
|
104
|
+
hash: -2132279313955691794
|
109
105
|
requirements: []
|
110
|
-
|
111
106
|
rubyforge_project: fire_poll
|
112
107
|
rubygems_version: 1.8.10
|
113
108
|
signing_key:
|
114
109
|
specification_version: 3
|
115
110
|
summary: Simple, brute-force method for knowing when something is ready
|
116
|
-
test_files:
|
111
|
+
test_files:
|
117
112
|
- test/test_fire_poll.rb
|
113
|
+
- test/test_fire_poll_mixin.rb
|
114
|
+
- test/test_patiently.rb
|