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 CHANGED
@@ -3,3 +3,5 @@ pkg/*
3
3
  .bundle
4
4
  doc
5
5
  .yardoc
6
+ Gemfile.lock
7
+ tags
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
- Examples
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
- The `FirePoll` module may be mixed into your class; this makes it a little faster to type the method name.
47
- class TestHelper
48
- include FirePoll
49
- def helper_method
50
- poll do
51
- ...
52
- end
53
- end
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
- `FirePoll.poll`'s implementation isn't partcilarly accurate with respect to time. The method will run your block (number of seconds * 10) times. It sleeps for a tenth of a second between attempts. Since it doesn't keep track of time, if your timing needs require accuracy, you'll need to look elsewhere.
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. `FirePoll.poll` meets our need nicely.
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
- * © 2011 [Atomic Object](http://www.atomicobject.com/)
102
+ * © 2012 [Atomic Object](http://www.atomicobject.com/)
77
103
  * More Atomic Object [open source](http://www.atomicobject.com/pages/Software+Commons) projects
@@ -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
- # @yieldreturn false if polling should continue
7
- # @yieldreturn true if polling is complete
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=2.0)
12
- (seconds * 10).to_i.times do
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 0.1
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
@@ -1,3 +1,3 @@
1
1
  module FirePoll
2
- VERSION = "1.0.1"
2
+ VERSION = "1.2.0"
3
3
  end
@@ -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
- class TestFirePollMixin < Test::Unit::TestCase
65
- include FirePoll
66
-
67
- def test_can_be_mixed_in
68
- assert_nothing_raised do
69
- poll { true }
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
- date: 2011-12-16 00:00:00 Z
16
- dependencies:
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: &id001 !ruby/object:Gem::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: *id001
28
- - !ruby/object:Gem::Dependency
26
+ version_requirements: *2156420820
27
+ - !ruby/object:Gem::Dependency
29
28
  name: rake
30
- requirement: &id002 !ruby/object:Gem::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: *id002
39
- - !ruby/object:Gem::Dependency
37
+ version_requirements: *2156419160
38
+ - !ruby/object:Gem::Dependency
40
39
  name: yard
41
- requirement: &id003 !ruby/object:Gem::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: *id003
50
- - !ruby/object:Gem::Dependency
48
+ version_requirements: *2156416240
49
+ - !ruby/object:Gem::Dependency
51
50
  name: bluecloth
52
- requirement: &id004 !ruby/object:Gem::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: *id004
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
- homepage: ""
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
- hash: 1299515667354431829
97
- segments:
89
+ requirements:
90
+ - - ! '>='
91
+ - !ruby/object:Gem::Version
92
+ version: '0'
93
+ segments:
98
94
  - 0
99
- version: "0"
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
- hash: 1299515667354431829
106
- segments:
98
+ requirements:
99
+ - - ! '>='
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ segments:
107
103
  - 0
108
- version: "0"
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