patiently 1.0.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f738cd510ed2b9ff9b43ba29a3a72201f3c6a85332ff47af71f171bd7478d3ac
4
+ data.tar.gz: 6d5d64fbe90a9b14be3bc00480d53e79823441d4b194c14adcac7cc75ef5266b
5
+ SHA512:
6
+ metadata.gz: 2c85cbcda0754060cd80be043067a61fe21b8a4cd70a27722f3ddb433bb82cb7d96e158e0d273ce3c30aa96ddc0fc89fbae86ba1999a468c0e088093f26d753b
7
+ data.tar.gz: fe576c8eb91123d2c6de6b5dd1519ab0f3cdaa2fc3607bbe0899482ccf0fbb5aa5ad0b0e4da3e1657c6f118546a318f48774afa68c6d9c9ddf65d7f7bfac1b5f
@@ -0,0 +1,35 @@
1
+ ---
2
+ name: Tests
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+ pull_request:
8
+ branches:
9
+ - main
10
+ workflow_dispatch: {}
11
+ jobs:
12
+ test:
13
+ runs-on: ubuntu-latest
14
+ timeout-minutes: 5
15
+ continue-on-error: "${{ matrix.experimental || false }}"
16
+ strategy:
17
+ fail-fast: false
18
+ matrix:
19
+ ruby:
20
+ - "2.7"
21
+ - "3.0"
22
+ - "3.2"
23
+ - "3.4"
24
+ include:
25
+ - ruby: "head"
26
+ experimental: true
27
+ steps:
28
+ - uses: actions/checkout@v4
29
+ - name: Install Ruby
30
+ uses: ruby/setup-ruby@v1
31
+ with:
32
+ ruby-version: "${{ matrix.ruby }}"
33
+ bundler-cache: true
34
+ - name: Run tests
35
+ run: bundle exec rake spec
data/.gitignore ADDED
@@ -0,0 +1,12 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ .idea
10
+
11
+ # rspec failure tracking
12
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.7.8
data/AGENTS.md ADDED
@@ -0,0 +1,67 @@
1
+ # AGENTS.md
2
+
3
+ Conventions for working on the `patiently` gem. Read this before changing code,
4
+ dependencies, or CI.
5
+
6
+ ## What this gem is
7
+
8
+ `patiently` retries a block until it stops raising (`patiently`) or returns
9
+ truthy (`patiently_until`, alias `patiently_wait_until`). It was extracted from a
10
+ `spec/support/patiently.rb` helper that used to be copy-pasted into many test
11
+ suites.
12
+
13
+ ## Hard rules
14
+
15
+ - **No runtime dependencies.** Do not add RSpec, Capybara, Rails or
16
+ ActiveSupport as a runtime dependency. The gem must work in any Ruby project.
17
+ RSpec/Capybara are dev-only (for the self-tests).
18
+ - **Ruby >= 2.7.** Keep the code compatible with 2.7 syntax. `required_ruby_version`
19
+ in the gemspec is the source of truth.
20
+ - **Ask before committing** (repo-wide preference).
21
+
22
+ ## Ruby versions & the lockfile
23
+
24
+ - `.ruby-version` pins **2.7.8** (our support floor). Locally you can use any
25
+ Ruby (`rbenv shell 2.7.8`, or just your default 3.x) — both are expected to pass.
26
+ - CI (`.github/workflows/test.yml`) runs a **matrix** across 2.7 / 3.0 / 3.2 / 3.4
27
+ and `head`. We do NOT use gemika/appraisals — one `Gemfile`, one `Gemfile.lock`.
28
+ - `Gemfile.lock` **is committed** and was generated under **Ruby 2.7.8 with
29
+ bundler 2.1.4** so `BUNDLED WITH` stays readable on every matrix Ruby. If you
30
+ change dependencies, regenerate the lock under the oldest supported Ruby:
31
+ `RBENV_VERSION=2.7.8 bundle _2.1.4_ install` — otherwise a newer bundler pin
32
+ breaks the 2.7 CI job.
33
+
34
+ ## Running tests
35
+
36
+ ```bash
37
+ bundle exec rspec # or: bundle exec rake spec (the CI entrypoint)
38
+ ```
39
+
40
+ Testing notes:
41
+ - Time-freeze detection is tested by **stubbing the private `monotonic_time`**
42
+ helper, not by mocking `Time`/`Process.clock_gettime` globally.
43
+ - Some examples wait **real** (sub-second) time on purpose; that is acceptable
44
+ here. Keep any real waits well under a second and stub `sleep` where timing
45
+ isn't the thing under test.
46
+ - Keep the suite dependency-light: no embedded Rails app, no nested RSpec runs.
47
+
48
+ ## Public API & semantics (don't break without a version bump)
49
+
50
+ - Mix in with `include Patiently::Helpers`.
51
+ - Config lives on `Patiently.config` (`timeout`, `retry_intervals`,
52
+ `min_retries`, `max_retries`) or `Patiently.configure { |c| ... }`.
53
+ - `min_retries` / `max_retries` count **re-attempts** (invocations after the
54
+ first). `min_retries = 1` reproduces the original "try at least twice"
55
+ behavior. `max_retries = nil` means unlimited.
56
+ - `retry_intervals` is a backoff array; the last element is reused once exhausted.
57
+ - Errors: `Patiently::Error` (base) and `Patiently::FrozenInTime`. Do NOT reach
58
+ for Capybara's error classes.
59
+ - Optional RSpec glue is `require "patiently/rspec"` — keep it a thin
60
+ `config.include(Patiently::Helpers, type: :feature)` and nothing more.
61
+
62
+ ## Packaging (house style, mirrors capybara-lockstep)
63
+
64
+ The gemspec ships **every tracked file except `spec/`, `test/`, `features/`** —
65
+ so `Gemfile`, `Gemfile.lock`, `.github/`, the gemspec and `bin/` are included in
66
+ the published `.gem` on purpose. `.gitignore` keeps IDE/build cruft (`.idea`,
67
+ `tmp/`, `pkg/`, `.rspec_status`) out of git and therefore out of the gem.
data/CHANGELOG.md ADDED
@@ -0,0 +1,18 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ ## Unreleased
6
+
7
+ ### Added
8
+
9
+ - Initial extraction of the `patiently` / `patiently_until` test helpers into a
10
+ standalone, dependency-free gem.
11
+ - `include Patiently::Helpers` to gain the `patiently` and `patiently_until`
12
+ helpers (`patiently_wait_until` is kept as an alias).
13
+ - Configurable defaults via `Patiently.config`: `timeout`, `retry_intervals`
14
+ (with backoff), `min_retries` and `max_retries`.
15
+ - Per-call timeout argument for both `patiently` and `patiently_until`.
16
+ - `Patiently::FrozenInTime` error raised when the monotonic clock does not
17
+ advance between retries (mocked/frozen time).
18
+ - Optional RSpec integration via `require "patiently/rspec"`.
data/Gemfile ADDED
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in patiently.gemspec
6
+ gemspec
7
+
8
+ gem "rake", "~> 13.0"
9
+ gem "rspec", "~> 3.0"
data/Gemfile.lock ADDED
@@ -0,0 +1,34 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ patiently (1.0.0)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ diff-lcs (1.6.2)
10
+ rake (13.4.2)
11
+ rspec (3.13.2)
12
+ rspec-core (~> 3.13.0)
13
+ rspec-expectations (~> 3.13.0)
14
+ rspec-mocks (~> 3.13.0)
15
+ rspec-core (3.13.6)
16
+ rspec-support (~> 3.13.0)
17
+ rspec-expectations (3.13.5)
18
+ diff-lcs (>= 1.2.0, < 2.0)
19
+ rspec-support (~> 3.13.0)
20
+ rspec-mocks (3.13.8)
21
+ diff-lcs (>= 1.2.0, < 2.0)
22
+ rspec-support (~> 3.13.0)
23
+ rspec-support (3.13.7)
24
+
25
+ PLATFORMS
26
+ ruby
27
+
28
+ DEPENDENCIES
29
+ patiently!
30
+ rake (~> 13.0)
31
+ rspec (~> 3.0)
32
+
33
+ BUNDLED WITH
34
+ 2.1.4
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Henning Koch
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,187 @@
1
+ <p>
2
+ <a href="https://makandra.de/">
3
+ <picture>
4
+ <source media="(prefers-color-scheme: light)" srcset="media/makandra-with-bottom-margin.light.svg">
5
+ <source media="(prefers-color-scheme: dark)" srcset="media/makandra-with-bottom-margin.dark.svg">
6
+ <img align="right" width="25%" alt="makandra" src="media/makandra-with-bottom-margin.light.svg">
7
+ </picture>
8
+ </a>
9
+
10
+ <picture>
11
+ <source media="(prefers-color-scheme: light)" srcset="media/logo.light.shapes.svg">
12
+ <source media="(prefers-color-scheme: dark)" srcset="media/logo.dark.shapes.svg">
13
+ <img width="200" alt="patiently" role="heading" aria-level="1" src="media/logo.light.shapes.svg">
14
+ </picture>
15
+ </p>
16
+
17
+ `patiently` retries a block of code until it stops raising an exception (or
18
+ returns a truthy value). It is most useful in tests that need to wait for an
19
+ eventually-consistent condition — a background job to finish, an AJAX request to
20
+ update the DOM, a file to appear — without sprinkling `sleep` calls everywhere.
21
+
22
+ It has **no runtime dependencies**: it does not require RSpec, Capybara, Rails or
23
+ ActiveSupport.
24
+
25
+ ## Installation
26
+
27
+ Add it to your `Gemfile` and run `bundle install`:
28
+
29
+ ```ruby
30
+ gem "patiently"
31
+ ```
32
+
33
+ Patiently does not depend on RSpec, but if you use it, wire the helpers into your
34
+ specs. Either require the bundled integration, which includes the helpers into
35
+ feature and system specs:
36
+
37
+ ```ruby
38
+ # spec/spec_helper.rb (or rails_helper.rb)
39
+ require "patiently/rspec"
40
+ ```
41
+
42
+ or include the helpers yourself with whatever scope you like:
43
+
44
+ ```ruby
45
+ RSpec.configure do |config|
46
+ config.include(Patiently::Helpers, type: :feature)
47
+ config.include(Patiently::Helpers, type: :system)
48
+ end
49
+ ```
50
+
51
+ ## Usage
52
+
53
+ Mix the helpers into any class:
54
+
55
+ ```ruby
56
+ include Patiently::Helpers
57
+ ```
58
+
59
+ This gives you two methods.
60
+
61
+ ### `patiently`
62
+
63
+ Runs the block and, if it raises, keeps retrying until it succeeds or the
64
+ patience window is exhausted. On success it returns the block's value; on failure
65
+ it re-raises the block's last exception.
66
+
67
+ ```ruby
68
+ patiently do
69
+ expect(page).to have_content("Saved!")
70
+ end
71
+ ```
72
+
73
+ `patiently` retries on **any** exception (including non-`StandardError`s such as
74
+ RSpec's `ExpectationNotMetError`), which is what makes it work with test
75
+ assertions.
76
+
77
+ You can pass a custom timeout (in seconds) as the first argument, overriding the
78
+ global default:
79
+
80
+ ```ruby
81
+ patiently(10) do
82
+ expect(page).to have_content("Slow import finished")
83
+ end
84
+ ```
85
+
86
+ ### `patiently_until`
87
+
88
+ Retries the block until it returns a truthy value, then returns `true`. If the
89
+ block is still falsey when the window elapses, it returns `false`. A *real*
90
+ exception raised inside the block still propagates.
91
+
92
+ ```ruby
93
+ patiently_until { user.reload.confirmed? } # => true / false
94
+ ```
95
+
96
+ This is handy for building custom matchers that retry internally while keeping
97
+ their own failure message. It also accepts a custom timeout:
98
+
99
+ ```ruby
100
+ patiently_until(10) { import.reload.done? }
101
+ ```
102
+
103
+ `patiently_wait_until` is available as an alias for `patiently_until`.
104
+
105
+ ### Nested blocks
106
+
107
+ When you call `patiently` (or `patiently_until`) while already inside a
108
+ `patiently` block on the same thread, the inner block simply runs once — only the
109
+ **outermost** block is retried. This prevents an inner retry loop from repeatedly
110
+ re-running expensive setup and lets the outer block drive the timing.
111
+
112
+ ## Configuration
113
+
114
+ Configure global defaults via `Patiently.config`:
115
+
116
+ ```ruby
117
+ Patiently.config.timeout = 5 # seconds before giving up
118
+ Patiently.config.retry_intervals = [0.05] # sleep durations between retries
119
+ Patiently.config.min_retries = 1 # always retry at least this often
120
+ Patiently.config.max_retries = nil # cap on retries (nil = unlimited)
121
+ ```
122
+
123
+ Or use a block:
124
+
125
+ ```ruby
126
+ Patiently.configure do |config|
127
+ config.timeout = 10
128
+ end
129
+ ```
130
+
131
+ | Option | Default | Meaning |
132
+ | ----------------- | -------- | ------- |
133
+ | `timeout` | `5` | How long (in seconds) to keep retrying before giving up. |
134
+ | `retry_intervals` | `[0.05]` | Sleep durations (seconds) between retries. See "Backoff" below. |
135
+ | `min_retries` | `1` | The minimum number of *retries* performed before giving up, even if the timeout has already elapsed. |
136
+ | `max_retries` | `nil` | The maximum number of *retries* before giving up, regardless of the timeout. `nil` means unlimited. |
137
+
138
+ Both `min_retries` and `max_retries` count **retries** — re-invocations *after*
139
+ the first call. So `min_retries = 1` means the block runs at least twice before
140
+ `patiently` is allowed to give up.
141
+
142
+ `patiently` gives up (and re-raises the block's exception) when either:
143
+
144
+ - the timeout has elapsed **and** at least `min_retries` retries have happened, or
145
+ - `max_retries` retries have happened (when `max_retries` is set).
146
+
147
+ ### Backoff
148
+
149
+ `retry_intervals` may be an array, used as a backoff schedule. The value at index
150
+ N is the sleep before the (N+1)-th retry; once the array is exhausted, its last
151
+ element is reused for all further retries:
152
+
153
+ ```ruby
154
+ Patiently.config.retry_intervals = [0.05, 0.05, 0.05, 0.1]
155
+ # sleeps 0.05, 0.05, 0.05, then 0.1, 0.1, 0.1, ...
156
+ ```
157
+
158
+ ## Frozen time detection
159
+
160
+ If the monotonic clock does not advance between retries — which usually means time
161
+ has been mocked or frozen (Timecop, Rails' `travel`/`freeze_time`, …) —
162
+ `patiently` would loop forever. Instead it raises `Patiently::FrozenInTime` so you
163
+ notice and travel time explicitly.
164
+
165
+ ## Errors
166
+
167
+ - `Patiently::Error` — base class for all errors raised by this gem.
168
+ - `Patiently::FrozenInTime` — raised when time appears to be frozen (see above).
169
+
170
+ ## Development
171
+
172
+ After checking out the repo, run `bin/setup` to install dependencies, then run
173
+ `rake spec` to run the tests. You can also run `bin/console` for an interactive
174
+ prompt.
175
+
176
+ To release a new version, update the version number in
177
+ `lib/patiently/version.rb`, then run `bundle exec rake release`.
178
+
179
+ ## Contributing
180
+
181
+ Bug reports and pull requests are welcome on GitHub at
182
+ https://github.com/makandra/patiently.
183
+
184
+ ## License
185
+
186
+ The gem is available as open source under the terms of the
187
+ [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
data/bin/console ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "patiently"
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ require "irb"
11
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Patiently
4
+ # Holds the global defaults for {Patiently::Helpers#patiently}. Access the
5
+ # shared instance via +Patiently.config+.
6
+ class Configuration
7
+ DEFAULT_TIMEOUT = 5
8
+ DEFAULT_RETRY_INTERVALS = [0.05].freeze
9
+ DEFAULT_MIN_RETRIES = 1
10
+ DEFAULT_MAX_RETRIES = nil # nil = unlimited
11
+
12
+ attr_writer :timeout, :retry_intervals, :min_retries, :max_retries
13
+
14
+ def initialize
15
+ reset!
16
+ end
17
+
18
+ # How long (in seconds) `patiently` keeps retrying before giving up.
19
+ def timeout
20
+ @timeout.nil? ? DEFAULT_TIMEOUT : @timeout
21
+ end
22
+
23
+ # An array of sleep durations (in seconds) used between retries. The value
24
+ # at index N is used before the (N+1)-th retry; once the array is exhausted
25
+ # its last element is reused for all further retries. This allows a backoff,
26
+ # e.g. `[0.05, 0.05, 0.05, 0.1]`. A bare number is accepted and wrapped in
27
+ # an array.
28
+ def retry_intervals
29
+ intervals = @retry_intervals.nil? ? DEFAULT_RETRY_INTERVALS : @retry_intervals
30
+ Array(intervals)
31
+ end
32
+
33
+ # The minimum number of *retries* (re-invocations after the first call)
34
+ # `patiently` performs before it is allowed to give up, even if the timeout
35
+ # has already elapsed. `1` reproduces the historical "try at least twice"
36
+ # behavior.
37
+ def min_retries
38
+ @min_retries.nil? ? DEFAULT_MIN_RETRIES : @min_retries
39
+ end
40
+
41
+ # The maximum number of *retries* `patiently` performs before giving up,
42
+ # regardless of the timeout. `nil` means unlimited.
43
+ def max_retries
44
+ @max_retries
45
+ end
46
+
47
+ # Returns the sleep duration for the given (zero-based) retry index.
48
+ def retry_interval(retry_index)
49
+ intervals = retry_intervals
50
+ intervals[retry_index] || intervals.last
51
+ end
52
+
53
+ # Restores all defaults. Handy in test suites.
54
+ def reset!
55
+ @timeout = nil
56
+ @retry_intervals = nil
57
+ @min_retries = nil
58
+ @max_retries = nil
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Patiently
4
+ # Base class for all errors raised by Patiently.
5
+ class Error < StandardError; end
6
+
7
+ # Raised when the (monotonic) clock does not advance between retries, which
8
+ # usually means time has been mocked/frozen (e.g. via Timecop or Rails'
9
+ # `travel`/`freeze_time`). Retrying against a frozen clock would loop forever,
10
+ # so we fail loudly instead.
11
+ class FrozenInTime < Error; end
12
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Patiently
4
+ # Mix this into a class (or configure RSpec to include it) to gain the
5
+ # {#patiently} and {#patiently_until} helpers:
6
+ #
7
+ # include Patiently::Helpers
8
+ module Helpers
9
+ # Thread-local flag marking that we are already inside a `patiently` block.
10
+ # Nested blocks are run once and let the *outer* block do the retrying.
11
+ IN_PATIENTLY = :patiently_in_progress
12
+
13
+ # Internal sentinel used by {#patiently_until} to signal "not truthy yet".
14
+ RetryUntilTrue = Class.new(StandardError)
15
+
16
+ # Runs `block`, retrying it whenever it raises, until it succeeds or the
17
+ # patience window (timeout + min/max retries) is exhausted. Returns the
18
+ # block's return value on success; re-raises the last exception on failure.
19
+ #
20
+ # When called while another `patiently` block is already running on the
21
+ # current thread, the block is simply run once: only the outermost block is
22
+ # retried.
23
+ #
24
+ # @param timeout [Numeric, nil] overrides +Patiently.config.timeout+
25
+ # rubocop:disable Lint/RescueException
26
+ def patiently(timeout = nil, &block)
27
+ return block.call if Thread.current[IN_PATIENTLY]
28
+
29
+ config = Patiently.config
30
+ timeout ||= config.timeout
31
+ started_at = monotonic_time
32
+ retries = 0
33
+
34
+ begin
35
+ Thread.current[IN_PATIENTLY] = true
36
+ attempt_started_at = monotonic_time
37
+ block.call
38
+ rescue Exception => e
39
+ elapsed = attempt_started_at - started_at
40
+ timed_out = elapsed > timeout && retries >= config.min_retries
41
+ retries_exhausted = config.max_retries && retries >= config.max_retries
42
+ raise e if timed_out || retries_exhausted
43
+
44
+ sleep(config.retry_interval(retries))
45
+
46
+ if monotonic_time == started_at
47
+ raise Patiently::FrozenInTime, "time appears to be frozen, consider time travelling instead"
48
+ end
49
+
50
+ retries += 1
51
+ retry
52
+ ensure
53
+ Thread.current[IN_PATIENTLY] = false
54
+ end
55
+ end
56
+ # rubocop:enable Lint/RescueException
57
+
58
+ # Retries `block` until it returns a truthy value or the patience window
59
+ # elapses, then returns the final result as a boolean.
60
+ #
61
+ # Because `patiently` only retries on a *raise*, we raise an internal
62
+ # sentinel while the block is falsey and translate a timed-out sentinel back
63
+ # into `false`. A real exception inside the block is not the sentinel, so it
64
+ # still propagates.
65
+ #
66
+ # @param timeout [Numeric, nil] overrides +Patiently.config.timeout+
67
+ def patiently_until(timeout = nil, &block)
68
+ patiently(timeout) { block.call || raise(RetryUntilTrue) }
69
+ true
70
+ rescue RetryUntilTrue
71
+ false
72
+ end
73
+ alias patiently_wait_until patiently_until
74
+
75
+ private
76
+
77
+ def monotonic_time
78
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../patiently"
4
+
5
+ # Convenience integration for RSpec: makes `patiently` and `patiently_until`
6
+ # available in feature and system specs (both drive a browser and need retries).
7
+ # If you want a different scope, skip this file and call
8
+ # `config.include(Patiently::Helpers, ...)` yourself.
9
+ RSpec.configure do |config|
10
+ config.include(Patiently::Helpers, type: :feature)
11
+ config.include(Patiently::Helpers, type: :system)
12
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Patiently
4
+ VERSION = "1.0.0"
5
+ end
data/lib/patiently.rb ADDED
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "patiently/version"
4
+ require_relative "patiently/errors"
5
+ require_relative "patiently/configuration"
6
+ require_relative "patiently/helpers"
7
+
8
+ module Patiently
9
+ class << self
10
+ # The global configuration object. See Patiently::Configuration.
11
+ def config
12
+ @config ||= Configuration.new
13
+ end
14
+
15
+ # Yields the configuration object for block-style setup:
16
+ #
17
+ # Patiently.configure do |config|
18
+ # config.timeout = 10
19
+ # end
20
+ def configure
21
+ yield config
22
+ end
23
+ end
24
+ end