Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow time to be frozen #55

Merged
merged 1 commit into from
Jun 23, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@

### Changed

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

### Fixed
- Fixed warnings about keyword arguments in Ruby 2.7
- The official explanation of keyword arguments in Ruby 2.7 can be found [HERE](https://www.ruby-lang.org/en/news/2019/12/12/separation-of-positional-and-keyword-arguments-in-ruby-3-0/)
Expand Down Expand Up @@ -1125,3 +1130,4 @@ impending major rubocop release
[igas]: https://github.com/igas
[oieioi]: https://github.com/oieioi
[anuj-ssharma]: https://github.com/anuj-ssharma
[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
27 changes: 7 additions & 20 deletions lib/site_prism/waiter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,20 @@

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

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

sleep(0.05)
break if timer.done?

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