Skip to content

Commit

Permalink
Allow time to be frozen
Browse files Browse the repository at this point in the history
  • Loading branch information
sos4nt committed Jun 4, 2020
1 parent 34a1ec9 commit f15d8e5
Show file tree
Hide file tree
Showing 7 changed files with 179 additions and 39 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@

### Changed

- Refined SitePrism's `Waiter.wait_until_true` logic ([sos4nt])
- SitePrism can now be used with `Timecop.freeze` and Rails' `travel_to`
- `FrozenInTimeError` was removed as it is no longer needed

### Fixed

## [3.4.2] - 2020-01-30
Expand Down Expand Up @@ -1108,3 +1112,4 @@ impending major rubocop release
[dkniffin]: https://github.com/dkniffin
[hoffi]: https://github.com/hoffi
[igas]: https://github.com/igas
[sos4nt]: https://github.com/sos4nt
1 change: 1 addition & 0 deletions lib/site_prism.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ module SitePrism
autoload :Page, 'site_prism/page'
autoload :Section, 'site_prism/section'
autoload :Waiter, 'site_prism/waiter'
autoload :Timer, 'site_prism/timer'

class << self
attr_reader :use_all_there_gem
Expand Down
8 changes: 0 additions & 8 deletions lib/site_prism/error.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,6 @@ class InvalidUrlMatcherError < PageLoadError; end
# Formerly known as `NoSelectorForElement`
class InvalidElementError < SitePrismError; end

# A tool like Timecop is being used to "freeze time" by overriding Time.now
# and similar methods. In this case, our waiter functions won't work, because
# Time.now does not change.
# If you encounter this issue, check that you are not doing Timecop.freeze without
# an accompanying Timecop.return.
# Also check out Timecop.safe_mode https://github.com/travisjeffery/timecop#timecopsafe_mode
class FrozenInTimeError < SitePrismError; end

# The condition that was being evaluated inside the block did not evaluate
# to true within the time limit
# Formerly known as `TimeoutException`
Expand Down
47 changes: 47 additions & 0 deletions lib/site_prism/timer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# frozen_string_literal: true

module SitePrism
class Timer
attr_reader :wait_time

def self.run(wait_time, &block)
new(wait_time).run(&block)
end

def initialize(wait_time)
@wait_time = wait_time
@done = false
end

def done?
@done == true
end

def run
start
yield self
ensure
stop
end

def start
stop
return if wait_time.zero?

@done = false
@thread = Thread.start do
sleep wait_time
@done = true
end
end

def stop
if @thread
@thread.kill
@thread.join
@thread = nil
end
@done = true
end
end
end
25 changes: 5 additions & 20 deletions lib/site_prism/waiter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,18 @@

module SitePrism
class Waiter
class << self
def wait_until_true(wait_time = Capybara.default_max_wait_time)
start_time = Time.now
SLEEP_DURATION = 0.05

def self.wait_until_true(wait_time = Capybara.default_max_wait_time)
Timer.run(wait_time) do |timer|
loop do
return true if yield
break if Time.now - start_time > wait_time
break if timer.done?

sleep(0.05)

check_for_time_stopped!(start_time)
sleep(SLEEP_DURATION)
end

raise SitePrism::TimeoutError, "Timed out after #{wait_time}s."
end

private

def check_for_time_stopped!(start_time)
return unless start_time == Time.now

raise(
SitePrism::FrozenInTimeError,
'Time appears to be frozen. For more info, see ' \
'https://github.com/site-prism/site_prism/blob/master/lib/site_prism/error.rb'
)
end
end
end
end
121 changes: 121 additions & 0 deletions spec/site_prism/timer_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# frozen_string_literal: true

describe SitePrism::Timer do
let(:wait_time) { 0.1 }

describe '#initialize' do
subject(:timer) { described_class.new(wait_time) }

it 'sets the wait time' do
expect(timer.wait_time).to eq(0.1)
end

it 'initially marks the timer as not done' do
expect(timer).not_to be_done
end
end

describe '.run' do
subject(:timer) { described_class.new(wait_time) }

it 'yields the timer to the block' do
yielded_value = nil
timer.run { |t| yielded_value = t }

expect(yielded_value).to eq(timer)
end

it 'starts the timer within the block and stops it afterwards' do
states = []
states << timer.done?
timer.run { |t| states << t.done? }
states << timer.done?

expect(states).to contain_exactly(false, false, true)
end

context 'with an exception within the block' do
it 'sets the state to done without rescuing the exception' do
expect { timer.run { raise 'test error' } }
.to raise_error('test error')
.and change(timer, :done?).from(false).to(true)
end
end
end

describe '#start' do
subject(:timer) { described_class.new(wait_time) }

after do
timer.stop
end

it 'starts the timer thread' do
expect(Thread).to receive(:start)

timer.start
end

it 'initially marks the timer as not done' do
timer.start

expect(timer).not_to be_done
end

it 'marks the timer as done after the specified wait time' do
timer.start

expect { sleep(0.15) }.to change(timer, :done?).from(false).to(true)
end

context 'with a wait time of 0' do
let(:wait_time) { 0 }

it 'does not start the timer thread' do
expect(Thread).not_to receive(:start)

timer.start
end

it 'immediately marks the timer as done' do
timer.start

expect(timer).to be_done
end
end
end

describe '#stop' do
subject(:timer) { described_class.new(wait_time) }

after do
timer.stop
end

it 'stops the timer thread' do
thread = timer.start
expect { timer.stop }.to change(thread, :alive?).from(true).to(false)
end

it 'marks the timer as done' do
timer.start
timer.stop
expect(timer).to be_done
end

context 'with a wait time of 0' do
let(:wait_time) { 0 }

it 'does not fail because of the missing timer thread' do
timer.start
expect { timer.stop }.not_to raise_error
end

it 'marks the timer as done' do
timer.start
timer.stop
expect(timer).to be_done
end
end
end
end
11 changes: 0 additions & 11 deletions spec/site_prism/waiter_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,5 @@
expect(duration).to be_within(0.1).of(timeout)
end
end

context 'when time is frozen' do
before do
allow(Time).to receive(:now).and_return(Time.new(2019, 4, 25))
end

it 'throws a FrozenInTimeError exception' do
expect { described_class.wait_until_true { false } }
.to raise_error(SitePrism::FrozenInTimeError)
end
end
end
end

0 comments on commit f15d8e5

Please sign in to comment.