Created
March 12, 2020 17:02
-
-
Save aks/b553a8aec32330944d6ed5990c0273b1 to your computer and use it in GitHub Desktop.
Ruby Wait class for waiting for things to happen or not happen
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# frozen_string_literal: true | |
# | |
# Wait.until { expr } -- waits until expr is true | |
# Wait.while { expr } -- waits while expr is true | |
# | |
# Each iteration contains a `sleep(sleep_time)`, where the default sleep time is | |
# Wait::DEFAULT_SLEEP_TIME (0.1 seconds). | |
# | |
# The sleep interval can be specified with the `sleep_time` argument: | |
# | |
# Wait.until(sleep_time: 0.5) { expr } | |
# | |
# waits until expr is true, sleeping 0.5 seconds on each iteration | |
# | |
# Wait.while(sleep_time: 1, max_time: 15) { expr } | |
# | |
# waits while expr is true, sleeping 1 second each iteration, until 15 seconds | |
# | |
# If the time is exceeded, a Wait::Timeout exception is raised. | |
# If `max_time` is not provided, there is no timeout. | |
# | |
# Each iteration increments a counter, which is passed along to the block as | |
# the first argument. | |
# | |
# The blocks can receive two arguments: `counter`, and `timer`, which represent | |
# the increment counter and the time waited (in seconds as a float) | |
# | |
# Wait.until { |counter, time| expr } | |
# Wait.while { |counter, time| expr } | |
# | |
# The `limit` argument sets the maximum number of iterations. Once the loop | |
# counter exceeds the limit, the waiting is stopped with a Wait::Limit exception. | |
# | |
# The default message for exceptions are: | |
# | |
# Wait::Timeout -- "The wait timeout has been exceeded." | |
# Wait::Limit -- "The wait limit has been exceeded." | |
# | |
# The "msg: STRING" argument can be given to override the default message. | |
class Wait | |
class Error < StandardError ; end | |
class Timeout < Error ; end | |
class Limit < Error ; end | |
DEFAULT_SLEEP_TIME = 0.1 # how many seconds to sleep each iteration | |
def self.until(**args) | |
new(**args).until { |counter, time| yield(counter, time) } | |
end | |
def self.while(**args) | |
new(**args).while { |counter, time| yield(counter, time) } | |
end | |
VALID_KEYWORDS = %i[max_time sleep_time limit msg].freeze | |
attr_reader :max_time, :limit, :msg | |
def initialize(**args) | |
check_args(args) | |
@max_time = args.delete(:max_time) | |
@sleep_time = args.delete(:sleep_time) | |
@limit = args.delete(:limit) | |
@msg = args.delete(:msg) | |
end | |
private | |
def check_args(args) | |
extra_keys = (args.keys - VALID_KEYWORDS).map(&:to_s) | |
if extra_keys.size.nonzero? | |
who_called = caller_locations[1].label | |
raise ArgumentError, "#{who_called}: invalid keyword argument(s): #{extra_keys.join(', ')}" | |
end | |
end | |
public | |
def while | |
run { |counter, time| !yield(counter, time) } | |
end | |
def until | |
run { |counter, time| yield(counter, time) } | |
end | |
private | |
def sleep_time | |
@sleep_time ||= DEFAULT_SLEEP_TIME | |
end | |
def run | |
counter = 0 | |
time_start = Time.now | |
result = nil | |
while true | |
time_waited = (Time.now - time_start).to_f | |
check_limit(counter) if limit | |
check_time(time_waited) if max_time | |
return result if (result = yield(counter, time_waited)) | |
sleep(sleep_time) | |
counter += 1 | |
end | |
end | |
def check_limit(counter) | |
raise(Limit, msg || "Wait limit has been exceeded.") if counter > limit | |
end | |
def check_time(time_waited) | |
raise(Timeout, msg || "Wait time has been exceeded.") if time_waited > max_time | |
end | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# frozen_string_literal: true | |
require 'spec_helper' | |
require_relative '../../app/lib/wait' | |
RSpec.describe 'Wait' do | |
context 'class methods' do | |
subject { Wait } | |
it { is_expected.to respond_to(:while, :until) } | |
end | |
describe '.until' do | |
context 'when the condition is true right away' do | |
it 'returns right away, without sleeping' do | |
expect_any_instance_of(Wait).to_not receive(:sleep) | |
Wait.until { |c, t| c == 0 } | |
end | |
end | |
context 'when the condition is false initially' do | |
it 'loops until the condition is true' do | |
expect_any_instance_of(Wait).to receive(:sleep).exactly(2).times | |
Wait.until { |c, t| c >= 2 } | |
end | |
it 'keeps track of the time spent looping' do | |
Wait.until { |c, t| (@time = t) > 0.5 } | |
expect(@time).to be_a(Float) | |
end | |
end | |
context 'with limit' do | |
context 'with the condition is positive before the limit is exceeded' do | |
subject { Wait.until(limit: 5) { |c, t| c >= 4 && c } } | |
it 'returns the last value' do | |
expect(subject).to eq 4 | |
end | |
it 'does not throw an exception' do | |
expect { subject }.to_not raise_error | |
end | |
end | |
context 'when the limit is reached first' do | |
subject { Wait.until(limit: 5) { |c, t| c >= 8 && c } } | |
it 'raises a Limit error' do | |
expect { subject }.to raise_error(Wait::Limit, /limit has been exceeded/) | |
end | |
context 'with a custom message' do | |
subject { Wait.until(limit: 5, msg: 'Ruh-roh!') { |c, t| c >= 8 && c } } | |
it 'raises the error with the custom message' do | |
expect { subject }.to raise_error(Wait::Limit, /Ruh-roh!/) | |
end | |
end | |
end | |
end | |
context 'with max_time' do | |
context 'when the condition is true before the max_time is reached' do | |
subject { Wait.until(max_time: 4.0) { |c, t| (@time = t) > 2.0 && t } } | |
it 'returns the last value' do | |
expect(subject).to eq @time | |
end | |
end | |
context 'when the max_time is reached before the condition is true' do | |
subject { Wait.until(max_time: 1.0) { |c, t| (@time = t) > 2.0 } } | |
it 'raises a Timeout error' do | |
expect { subject }.to raise_error(Wait::Timeout, /Wait time has been exceeded/) | |
end | |
end | |
context 'with a custom message' do | |
subject { Wait.until(max_time: 1.0, msg: 'Ruh-roh!') { |c, t| t > 2.0 } } | |
it 'raises the error with the custom message' do | |
expect { subject }.to raise_error(Wait::Timeout, /Ruh-roh!/) | |
end | |
end | |
end | |
context 'with both limit and max_time' do | |
context 'when the condition is true first' do | |
subject { Wait.until(max_time: 10.0, limit: 10) { |c, t| c > 5 && t < 1.0 && c } } | |
it 'returns the last result' do | |
expect(subject).to eq 6 | |
end | |
end | |
context 'when the limit is exceeded first' do | |
subject { Wait.until(max_time: 10.0, limit: 10) { |c, t| c > 100 } } | |
it 'raises the Limit error' do | |
expect { subject }.to raise_error(Wait::Limit) | |
end | |
end | |
context 'when the max_time is exceeded first' do | |
subject { Wait.until(max_time: 1.0, limit: 10) { |c, t| t > 2.0 } } | |
it 'raises the Timeout error' do | |
expect { subject }.to raise_error(Wait::Timeout) | |
end | |
end | |
end | |
end | |
describe '.while' do | |
subject { Wait.while(**args) { condition } } | |
# TODO | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment