From e90ce22c85d1a768596f470491fdb13b334a2056 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Schu=CC=88=C3=9Fler?= Date: Tue, 26 May 2020 13:03:37 +0200 Subject: [PATCH] Allow time to be frozen --- CHANGELOG.md | 6 ++ lib/site_prism.rb | 1 + lib/site_prism/error.rb | 8 --- lib/site_prism/timer.rb | 47 +++++++++++++ lib/site_prism/waiter.rb | 27 ++------ spec/site_prism/timer_spec.rb | 121 +++++++++++++++++++++++++++++++++ spec/site_prism/waiter_spec.rb | 11 --- 7 files changed, 182 insertions(+), 39 deletions(-) create mode 100644 lib/site_prism/timer.rb create mode 100644 spec/site_prism/timer_spec.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 97abd421..76b0fe1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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/) @@ -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 diff --git a/lib/site_prism.rb b/lib/site_prism.rb index ea7a27c4..ec66b381 100644 --- a/lib/site_prism.rb +++ b/lib/site_prism.rb @@ -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 diff --git a/lib/site_prism/error.rb b/lib/site_prism/error.rb index dd37501a..25a57f58 100644 --- a/lib/site_prism/error.rb +++ b/lib/site_prism/error.rb @@ -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` diff --git a/lib/site_prism/timer.rb b/lib/site_prism/timer.rb new file mode 100644 index 00000000..371f7a3b --- /dev/null +++ b/lib/site_prism/timer.rb @@ -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 diff --git a/lib/site_prism/waiter.rb b/lib/site_prism/waiter.rb index 6c9c7c59..56938bc6 100644 --- a/lib/site_prism/waiter.rb +++ b/lib/site_prism/waiter.rb @@ -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 diff --git a/spec/site_prism/timer_spec.rb b/spec/site_prism/timer_spec.rb new file mode 100644 index 00000000..fe28bef8 --- /dev/null +++ b/spec/site_prism/timer_spec.rb @@ -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 diff --git a/spec/site_prism/waiter_spec.rb b/spec/site_prism/waiter_spec.rb index 128c925d..5f491945 100644 --- a/spec/site_prism/waiter_spec.rb +++ b/spec/site_prism/waiter_spec.rb @@ -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