Skip to content

Instantly share code, notes, and snippets.

@aks
Created March 12, 2020 17:02
Show Gist options
  • Save aks/b553a8aec32330944d6ed5990c0273b1 to your computer and use it in GitHub Desktop.
Save aks/b553a8aec32330944d6ed5990c0273b1 to your computer and use it in GitHub Desktop.
Ruby Wait class for waiting for things to happen or not happen
# 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
# 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