diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-01-09 09:07:51 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-01-09 09:07:51 +0000 |
commit | 5afd8575506372dd64c238203bd05b4826f3ae2e (patch) | |
tree | e167192fdc7d73fcc1aa5bd33b535b813120ec37 /qa | |
parent | 8bda404e2919234c299f088b7d8d04f8449125de (diff) | |
download | gitlab-ce-5afd8575506372dd64c238203bd05b4826f3ae2e.tar.gz |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'qa')
-rw-r--r-- | qa/Gemfile | 1 | ||||
-rw-r--r-- | qa/Gemfile.lock | 2 | ||||
-rw-r--r-- | qa/qa.rb | 3 | ||||
-rw-r--r-- | qa/qa/page/base.rb | 10 | ||||
-rw-r--r-- | qa/qa/resource/events/base.rb | 7 | ||||
-rw-r--r-- | qa/qa/support/repeater.rb | 65 | ||||
-rw-r--r-- | qa/qa/support/retrier.rb | 74 | ||||
-rw-r--r-- | qa/qa/support/waiter.rb | 43 | ||||
-rw-r--r-- | qa/qa/vendor/github/page/login.rb | 2 | ||||
-rw-r--r-- | qa/qa/vendor/jenkins/page/configure.rb | 2 | ||||
-rw-r--r-- | qa/qa/vendor/jenkins/page/login.rb | 2 | ||||
-rw-r--r-- | qa/spec/page/base_spec.rb | 8 | ||||
-rw-r--r-- | qa/spec/page/logging_spec.rb | 8 | ||||
-rw-r--r-- | qa/spec/resource/events/project_spec.rb | 1 | ||||
-rw-r--r-- | qa/spec/support/repeater_spec.rb | 385 | ||||
-rw-r--r-- | qa/spec/support/retrier_spec.rb | 126 | ||||
-rw-r--r-- | qa/spec/support/waiter_spec.rb | 42 |
17 files changed, 706 insertions, 75 deletions
diff --git a/qa/Gemfile b/qa/Gemfile index 3575ecf13e9..58118340f24 100644 --- a/qa/Gemfile +++ b/qa/Gemfile @@ -19,4 +19,5 @@ group :test do gem 'pry-byebug', '~> 3.5.1', platform: :mri gem "ruby-debug-ide", "~> 0.7.0" gem "debase", "~> 0.2.4.1" + gem 'timecop', '~> 0.9.1' end diff --git a/qa/Gemfile.lock b/qa/Gemfile.lock index 25c7703ef52..6d48a9449a5 100644 --- a/qa/Gemfile.lock +++ b/qa/Gemfile.lock @@ -99,6 +99,7 @@ GEM childprocess (>= 0.5, < 4.0) rubyzip (>= 1.2.2) thread_safe (0.3.6) + timecop (0.9.1) tzinfo (1.2.5) thread_safe (~> 0.1) unf (0.1.4) @@ -128,6 +129,7 @@ DEPENDENCIES rspec_junit_formatter (~> 0.4.1) ruby-debug-ide (~> 0.7.0) selenium-webdriver (~> 3.12) + timecop (~> 0.9.1) BUNDLED WITH 1.17.3 @@ -488,8 +488,9 @@ module QA end autoload :Api, 'qa/support/api' autoload :Dates, 'qa/support/dates' - autoload :Waiter, 'qa/support/waiter' + autoload :Repeater, 'qa/support/repeater' autoload :Retrier, 'qa/support/retrier' + autoload :Waiter, 'qa/support/waiter' autoload :WaitForRequests, 'qa/support/wait_for_requests' end end diff --git a/qa/qa/page/base.rb b/qa/qa/page/base.rb index 2c04fb53440..13f0e1e1994 100644 --- a/qa/qa/page/base.rb +++ b/qa/qa/page/base.rb @@ -26,20 +26,20 @@ module QA wait_for_requests end - def wait(max: 60, interval: 0.1, reload: true) - QA::Support::Waiter.wait(max: max, interval: interval) do + def wait(max: 60, interval: 0.1, reload: true, raise_on_failure: false) + Support::Waiter.wait_until(max_duration: max, sleep_interval: interval, raise_on_failure: raise_on_failure) do yield || (reload && refresh && false) end end - def retry_until(max_attempts: 3, reload: false, sleep_interval: 0) - QA::Support::Retrier.retry_until(max_attempts: max_attempts, reload_page: (reload && self), sleep_interval: sleep_interval) do + def retry_until(max_attempts: 3, reload: false, sleep_interval: 0, raise_on_failure: false) + Support::Retrier.retry_until(max_attempts: max_attempts, reload_page: (reload && self), sleep_interval: sleep_interval, raise_on_failure: raise_on_failure) do yield end end def retry_on_exception(max_attempts: 3, reload: false, sleep_interval: 0.5) - QA::Support::Retrier.retry_on_exception(max_attempts: max_attempts, reload_page: (reload && self), sleep_interval: sleep_interval) do + Support::Retrier.retry_on_exception(max_attempts: max_attempts, reload_page: (reload && self), sleep_interval: sleep_interval) do yield end end diff --git a/qa/qa/resource/events/base.rb b/qa/qa/resource/events/base.rb index b50b620b143..f98a54a6f57 100644 --- a/qa/qa/resource/events/base.rb +++ b/qa/qa/resource/events/base.rb @@ -4,6 +4,7 @@ module QA module Resource module Events MAX_WAIT = 10 + RAISE_ON_FAILURE = true EventNotFoundError = Class.new(RuntimeError) @@ -21,7 +22,7 @@ module QA end def wait_for_event - event_found = QA::Support::Waiter.wait(max: max_wait) do + event_found = Support::Waiter.wait_until(max_duration: max_wait, raise_on_failure: raise_on_failure) do yield end @@ -31,6 +32,10 @@ module QA def max_wait MAX_WAIT end + + def raise_on_failure + RAISE_ON_FAILURE + end end end end diff --git a/qa/qa/support/repeater.rb b/qa/qa/support/repeater.rb new file mode 100644 index 00000000000..53d72f2f410 --- /dev/null +++ b/qa/qa/support/repeater.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +require 'active_support/inflector' + +module QA + module Support + module Repeater + DEFAULT_MAX_WAIT_TIME = 60 + + RetriesExceededError = Class.new(RuntimeError) + WaitExceededError = Class.new(RuntimeError) + + def repeat_until(max_attempts: nil, max_duration: nil, reload_page: nil, sleep_interval: 0, raise_on_failure: true, retry_on_exception: false) + attempts = 0 + start = Time.now + + begin + while remaining_attempts?(attempts, max_attempts) && remaining_time?(start, max_duration) + QA::Runtime::Logger.debug("Attempt number #{attempts + 1}") if max_attempts + + result = yield + return result if result + + sleep_and_reload_if_needed(sleep_interval, reload_page) + attempts += 1 + end + rescue StandardError, RSpec::Expectations::ExpectationNotMetError + raise unless retry_on_exception + + attempts += 1 + if remaining_attempts?(attempts, max_attempts) && remaining_time?(start, max_duration) + sleep_and_reload_if_needed(sleep_interval, reload_page) + + retry + else + raise + end + end + + if raise_on_failure + raise RetriesExceededError, "Retry condition not met after #{max_attempts} #{'attempt'.pluralize(max_attempts)}" unless remaining_attempts?(attempts, max_attempts) + + raise WaitExceededError, "Wait condition not met after #{max_duration} #{'second'.pluralize(max_duration)}" + end + + false + end + + private + + def sleep_and_reload_if_needed(sleep_interval, reload_page) + sleep(sleep_interval) + reload_page.refresh if reload_page + end + + def remaining_attempts?(attempts, max_attempts) + max_attempts ? attempts < max_attempts : true + end + + def remaining_time?(start, max_duration) + max_duration ? Time.now - start < max_duration : true + end + end + end +end diff --git a/qa/qa/support/retrier.rb b/qa/qa/support/retrier.rb index 3b02cb4855b..7b548e95453 100644 --- a/qa/qa/support/retrier.rb +++ b/qa/qa/support/retrier.rb @@ -3,49 +3,61 @@ module QA module Support module Retrier + extend Repeater + module_function def retry_on_exception(max_attempts: 3, reload_page: nil, sleep_interval: 0.5) - QA::Runtime::Logger.debug("with retry_on_exception: max_attempts #{max_attempts}; sleep_interval #{sleep_interval}") - - attempts = 0 + QA::Runtime::Logger.debug( + <<~MSG.tr("\n", ' ') + with retry_on_exception: max_attempts: #{max_attempts}; + reload_page: #{reload_page}; + sleep_interval: #{sleep_interval} + MSG + ) - begin - QA::Runtime::Logger.debug("Attempt number #{attempts + 1}") - yield - rescue StandardError, RSpec::Expectations::ExpectationNotMetError - sleep sleep_interval - reload_page.refresh if reload_page - attempts += 1 + result = nil + repeat_until( + max_attempts: max_attempts, + reload_page: reload_page, + sleep_interval: sleep_interval, + retry_on_exception: true + ) do + result = yield - retry if attempts < max_attempts - QA::Runtime::Logger.debug("Raising exception after #{max_attempts} attempts") - raise + # This method doesn't care what the return value of the block is. + # We set it to `true` so that it doesn't repeat if there's no exception + true end - end - - def retry_until(max_attempts: 3, reload_page: nil, sleep_interval: 0, exit_on_failure: false) - QA::Runtime::Logger.debug("with retry_until: max_attempts #{max_attempts}; sleep_interval #{sleep_interval}; reload_page:#{reload_page}") - attempts = 0 + QA::Runtime::Logger.debug("ended retry_on_exception") - while attempts < max_attempts - QA::Runtime::Logger.debug("Attempt number #{attempts + 1}") - result = yield - return result if result + result + end - sleep sleep_interval + def retry_until(max_attempts: nil, max_duration: nil, reload_page: nil, sleep_interval: 0, raise_on_failure: false, retry_on_exception: false) + # For backwards-compatibility + max_attempts = 3 if max_attempts.nil? && max_duration.nil? - reload_page.refresh if reload_page + start_msg ||= ["with retry_until:"] + start_msg << "max_attempts: #{max_attempts};" if max_attempts + start_msg << "max_duration: #{max_duration};" if max_duration + start_msg << "reload_page: #{reload_page}; sleep_interval: #{sleep_interval}; raise_on_failure: #{raise_on_failure}; retry_on_exception: #{retry_on_exception}" + QA::Runtime::Logger.debug(start_msg.join(' ')) - attempts += 1 - end - - if exit_on_failure - QA::Runtime::Logger.debug("Raising exception after #{max_attempts} attempts") - raise + result = nil + repeat_until( + max_attempts: max_attempts, + max_duration: max_duration, + reload_page: reload_page, + sleep_interval: sleep_interval, + raise_on_failure: raise_on_failure, + retry_on_exception: retry_on_exception + ) do + result = yield end + QA::Runtime::Logger.debug("ended retry_until") - false + result end end end diff --git a/qa/qa/support/waiter.rb b/qa/qa/support/waiter.rb index fdcf2d7e157..73ca0182464 100644 --- a/qa/qa/support/waiter.rb +++ b/qa/qa/support/waiter.rb @@ -3,30 +3,39 @@ module QA module Support module Waiter - DEFAULT_MAX_WAIT_TIME = 60 + extend Repeater module_function - def wait(max: DEFAULT_MAX_WAIT_TIME, interval: 0.1) - QA::Runtime::Logger.debug("with wait: max #{max}; interval #{interval}") - start = Time.now + def wait(max: singleton_class::DEFAULT_MAX_WAIT_TIME, interval: 0.1) + wait_until(max_duration: max, sleep_interval: interval, raise_on_failure: false) do + yield + end + end - while Time.now - start < max - result = yield - if result - log_end(Time.now - start) - return result - end + def wait_until(max_duration: singleton_class::DEFAULT_MAX_WAIT_TIME, reload_page: nil, sleep_interval: 0.1, raise_on_failure: false, retry_on_exception: false) + QA::Runtime::Logger.debug( + <<~MSG.tr("\n", ' ') + with wait_until: max_duration: #{max_duration}; + reload_page: #{reload_page}; + sleep_interval: #{sleep_interval}; + raise_on_failure: #{raise_on_failure} + MSG + ) - sleep(interval) + result = nil + self.repeat_until( + max_duration: max_duration, + reload_page: reload_page, + sleep_interval: sleep_interval, + raise_on_failure: raise_on_failure, + retry_on_exception: retry_on_exception + ) do + result = yield end - log_end(Time.now - start) - - false - end + QA::Runtime::Logger.debug("ended wait_until") - def self.log_end(duration) - QA::Runtime::Logger.debug("ended wait after #{duration} seconds") + result end end end diff --git a/qa/qa/vendor/github/page/login.rb b/qa/qa/vendor/github/page/login.rb index e581edcb7c7..8dd79148043 100644 --- a/qa/qa/vendor/github/page/login.rb +++ b/qa/qa/vendor/github/page/login.rb @@ -12,7 +12,7 @@ module QA fill_in 'password', with: QA::Runtime::Env.github_password click_on 'Sign in' - Support::Retrier.retry_until(exit_on_failure: true, sleep_interval: 35) do + Support::Retrier.retry_until(raise_on_failure: true, sleep_interval: 35) do otp = OnePassword::CLI.new.otp fill_in 'otp', with: otp diff --git a/qa/qa/vendor/jenkins/page/configure.rb b/qa/qa/vendor/jenkins/page/configure.rb index 8851a2564fd..da59060152d 100644 --- a/qa/qa/vendor/jenkins/page/configure.rb +++ b/qa/qa/vendor/jenkins/page/configure.rb @@ -18,7 +18,7 @@ module QA dropdown_element = find('.setting-name', text: "Credentials").find(:xpath, "..").find('select') - QA::Support::Retrier.retry_until(exit_on_failure: true) do + QA::Support::Retrier.retry_until(raise_on_failure: true) do dropdown_element.select "GitLab API token (#{token_description})" dropdown_element.value != '' end diff --git a/qa/qa/vendor/jenkins/page/login.rb b/qa/qa/vendor/jenkins/page/login.rb index 7b3558b25e2..b18c02b5a44 100644 --- a/qa/qa/vendor/jenkins/page/login.rb +++ b/qa/qa/vendor/jenkins/page/login.rb @@ -14,7 +14,7 @@ module QA def visit! super - QA::Support::Retrier.retry_until(sleep_interval: 3, reload_page: page, max_attempts: 20, exit_on_failure: true) do + QA::Support::Retrier.retry_until(sleep_interval: 3, reload_page: page, max_attempts: 20, raise_on_failure: true) do page.has_text? 'Welcome to Jenkins!' end end diff --git a/qa/spec/page/base_spec.rb b/qa/spec/page/base_spec.rb index 9e3f143ea5b..e157eb6ac3e 100644 --- a/qa/spec/page/base_spec.rb +++ b/qa/spec/page/base_spec.rb @@ -69,11 +69,11 @@ describe QA::Page::Base do it 'does not refresh' do expect(subject).not_to receive(:refresh) - subject.wait(max: 0.01) { true } + subject.wait(max: 0.01, raise_on_failure: false) { true } end it 'returns true' do - expect(subject.wait(max: 0.1) { true }).to be_truthy + expect(subject.wait(max: 0.1, raise_on_failure: false) { true }).to be_truthy end end @@ -81,13 +81,13 @@ describe QA::Page::Base do it 'refreshes' do expect(subject).to receive(:refresh).at_least(:once) - subject.wait(max: 0.01) { false } + subject.wait(max: 0.01, raise_on_failure: false) { false } end it 'returns false' do allow(subject).to receive(:refresh) - expect(subject.wait(max: 0.01) { false }).to be_falsey + expect(subject.wait(max: 0.01, raise_on_failure: false) { false }).to be_falsey end end end diff --git a/qa/spec/page/logging_spec.rb b/qa/spec/page/logging_spec.rb index fb89bcd3ab4..0a394e1c38f 100644 --- a/qa/spec/page/logging_spec.rb +++ b/qa/spec/page/logging_spec.rb @@ -31,18 +31,18 @@ describe QA::Support::Page::Logging do expect { subject.wait(max: 0) {} } .to output(/next wait uses reload: true/).to_stdout_from_any_process expect { subject.wait(max: 0) {} } - .to output(/with wait/).to_stdout_from_any_process + .to output(/with wait_until/).to_stdout_from_any_process expect { subject.wait(max: 0) {} } - .to output(/ended wait after .* seconds$/).to_stdout_from_any_process + .to output(/ended wait_until$/).to_stdout_from_any_process end it 'logs wait with reload false' do expect { subject.wait(max: 0, reload: false) {} } .to output(/next wait uses reload: false/).to_stdout_from_any_process expect { subject.wait(max: 0, reload: false) {} } - .to output(/with wait/).to_stdout_from_any_process + .to output(/with wait_until/).to_stdout_from_any_process expect { subject.wait(max: 0, reload: false) {} } - .to output(/ended wait after .* seconds$/).to_stdout_from_any_process + .to output(/ended wait_until$/).to_stdout_from_any_process end it 'logs scroll_to' do diff --git a/qa/spec/resource/events/project_spec.rb b/qa/spec/resource/events/project_spec.rb index b3efdb518f3..dd544ec7ac8 100644 --- a/qa/spec/resource/events/project_spec.rb +++ b/qa/spec/resource/events/project_spec.rb @@ -33,6 +33,7 @@ describe QA::Resource::Events::Project do before do allow(subject).to receive(:max_wait).and_return(0.01) + allow(subject).to receive(:raise_on_failure).and_return(false) allow(subject).to receive(:parse_body).and_return(all_events) end diff --git a/qa/spec/support/repeater_spec.rb b/qa/spec/support/repeater_spec.rb new file mode 100644 index 00000000000..20dca6608f6 --- /dev/null +++ b/qa/spec/support/repeater_spec.rb @@ -0,0 +1,385 @@ +# frozen_string_literal: true + +require 'logger' +require 'timecop' +require 'active_support/core_ext/integer/time' + +describe QA::Support::Repeater do + before do + logger = ::Logger.new $stdout + logger.level = ::Logger::DEBUG + QA::Runtime::Logger.logger = logger + end + + subject do + Module.new do + extend QA::Support::Repeater + end + end + + let(:time_start) { Time.now } + let(:return_value) { "test passed" } + + describe '.repeat_until' do + context 'when raise_on_failure is not provided (default: true)' do + context 'when retry_on_exception is not provided (default: false)' do + context 'when max_duration is provided' do + context 'when max duration is reached' do + it 'raises an exception' do + expect do + Timecop.freeze do + subject.repeat_until(max_duration: 1) do + Timecop.travel(2) + false + end + end + end.to raise_error(QA::Support::Repeater::WaitExceededError, "Wait condition not met after 1 second") + end + + it 'ignores attempts' do + loop_counter = 0 + + expect( + Timecop.freeze do + subject.repeat_until(max_duration: 1) do + loop_counter += 1 + + if loop_counter > 3 + Timecop.travel(1) + return_value + else + false + end + end + end + ).to eq(return_value) + expect(loop_counter).to eq(4) + end + end + + context 'when max duration is not reached' do + it 'returns value from block' do + Timecop.freeze(time_start) do + expect( + subject.repeat_until(max_duration: 1) do + return_value + end + ).to eq(return_value) + end + end + end + end + + context 'when max_attempts is provided' do + context 'when max_attempts is reached' do + it 'raises an exception' do + expect do + Timecop.freeze do + subject.repeat_until(max_attempts: 1) do + false + end + end + end.to raise_error(QA::Support::Repeater::RetriesExceededError, "Retry condition not met after 1 attempt") + end + + it 'ignores duration' do + loop_counter = 0 + + expect( + Timecop.freeze do + subject.repeat_until(max_attempts: 2) do + loop_counter += 1 + Timecop.travel(1.year) + + if loop_counter > 1 + return_value + else + false + end + end + end + ).to eq(return_value) + expect(loop_counter).to eq(2) + end + end + + context 'when max_attempts is not reached' do + it 'returns value from block' do + expect( + Timecop.freeze do + subject.repeat_until(max_attempts: 1) do + return_value + end + end + ).to eq(return_value) + end + end + end + + context 'when both max_attempts and max_duration are provided' do + context 'when max_attempts is reached first' do + it 'raises an exception' do + loop_counter = 0 + expect do + Timecop.freeze do + subject.repeat_until(max_attempts: 1, max_duration: 2) do + loop_counter += 1 + Timecop.travel(time_start + loop_counter) + false + end + end + end.to raise_error(QA::Support::Repeater::RetriesExceededError, "Retry condition not met after 1 attempt") + end + end + + context 'when max_duration is reached first' do + it 'raises an exception' do + loop_counter = 0 + expect do + Timecop.freeze do + subject.repeat_until(max_attempts: 2, max_duration: 1) do + loop_counter += 1 + Timecop.travel(time_start + loop_counter) + false + end + end + end.to raise_error(QA::Support::Repeater::WaitExceededError, "Wait condition not met after 1 second") + end + end + end + end + + context 'when retry_on_exception is true' do + context 'when max duration is reached' do + it 'raises an exception' do + Timecop.freeze do + expect do + subject.repeat_until(max_duration: 1, retry_on_exception: true) do + Timecop.travel(2) + + raise "this should be raised" + end + end.to raise_error(RuntimeError, "this should be raised") + end + end + + it 'does not raise an exception until max_duration is reached' do + loop_counter = 0 + + Timecop.freeze(time_start) do + expect do + subject.repeat_until(max_duration: 2, retry_on_exception: true) do + loop_counter += 1 + Timecop.travel(time_start + loop_counter) + + raise "this should be raised" + end + end.to raise_error(RuntimeError, "this should be raised") + end + expect(loop_counter).to eq(2) + end + end + + context 'when max duration is not reached' do + it 'returns value from block' do + loop_counter = 0 + + Timecop.freeze(time_start) do + expect( + subject.repeat_until(max_duration: 3, retry_on_exception: true) do + loop_counter += 1 + Timecop.travel(time_start + loop_counter) + + raise "this should not be raised" if loop_counter == 1 + + return_value + end + ).to eq(return_value) + end + expect(loop_counter).to eq(2) + end + end + + context 'when both max_attempts and max_duration are provided' do + context 'when max_attempts is reached first' do + it 'raises an exception' do + loop_counter = 0 + expect do + Timecop.freeze do + subject.repeat_until(max_attempts: 1, max_duration: 2, retry_on_exception: true) do + loop_counter += 1 + Timecop.travel(time_start + loop_counter) + false + end + end + end.to raise_error(QA::Support::Repeater::RetriesExceededError, "Retry condition not met after 1 attempt") + end + end + + context 'when max_duration is reached first' do + it 'raises an exception' do + loop_counter = 0 + expect do + Timecop.freeze do + subject.repeat_until(max_attempts: 2, max_duration: 1, retry_on_exception: true) do + loop_counter += 1 + Timecop.travel(time_start + loop_counter) + false + end + end + end.to raise_error(QA::Support::Repeater::WaitExceededError, "Wait condition not met after 1 second") + end + end + end + end + end + + context 'when raise_on_failure is false' do + context 'when retry_on_exception is not provided (default: false)' do + context 'when max duration is reached' do + def test_wait + Timecop.freeze do + subject.repeat_until(max_duration: 1, raise_on_failure: false) do + Timecop.travel(2) + return_value + end + end + end + + it 'does not raise an exception' do + expect { test_wait }.not_to raise_error + end + + it 'returns the value from the block' do + expect(test_wait).to eq(return_value) + end + end + + context 'when max duration is not reached' do + it 'returns the value from the block' do + Timecop.freeze do + expect( + subject.repeat_until(max_duration: 1, raise_on_failure: false) do + return_value + end + ).to eq(return_value) + end + end + + it 'raises an exception' do + Timecop.freeze do + expect do + subject.repeat_until(max_duration: 1, raise_on_failure: false) do + raise "this should be raised" + end + end.to raise_error(RuntimeError, "this should be raised") + end + end + end + + context 'when both max_attempts and max_duration are provided' do + shared_examples 'repeat until' do |max_attempts:, max_duration:| + it "returns when #{max_attempts < max_duration ? 'max_attempts' : 'max_duration'} is reached" do + loop_counter = 0 + + expect( + Timecop.freeze do + subject.repeat_until(max_attempts: max_attempts, max_duration: max_duration, raise_on_failure: false) do + loop_counter += 1 + Timecop.travel(time_start + loop_counter) + false + end + end + ).to eq(false) + expect(loop_counter).to eq(1) + end + end + + context 'when max_attempts is reached first' do + it_behaves_like 'repeat until', max_attempts: 1, max_duration: 2 + end + + context 'when max_duration is reached first' do + it_behaves_like 'repeat until', max_attempts: 2, max_duration: 1 + end + end + end + + context 'when retry_on_exception is true' do + context 'when max duration is reached' do + def test_wait + Timecop.freeze do + subject.repeat_until(max_duration: 1, raise_on_failure: false, retry_on_exception: true) do + Timecop.travel(2) + return_value + end + end + end + + it 'does not raise an exception' do + expect { test_wait }.not_to raise_error + end + + it 'returns the value from the block' do + expect(test_wait).to eq(return_value) + end + end + + context 'when max duration is not reached' do + before do + @loop_counter = 0 + end + + def test_wait_with_counter + Timecop.freeze(time_start) do + subject.repeat_until(max_duration: 3, raise_on_failure: false, retry_on_exception: true) do + @loop_counter += 1 + Timecop.travel(time_start + @loop_counter) + + raise "this should not be raised" if @loop_counter == 1 + + return_value + end + end + end + + it 'does not raise an exception' do + expect { test_wait_with_counter }.not_to raise_error + end + + it 'returns the value from the block' do + expect(test_wait_with_counter).to eq(return_value) + expect(@loop_counter).to eq(2) + end + end + + context 'when both max_attempts and max_duration are provided' do + shared_examples 'repeat until' do |max_attempts:, max_duration:| + it "returns when #{max_attempts < max_duration ? 'max_attempts' : 'max_duration'} is reached" do + loop_counter = 0 + + expect( + Timecop.freeze do + subject.repeat_until(max_attempts: max_attempts, max_duration: max_duration, raise_on_failure: false, retry_on_exception: true) do + loop_counter += 1 + Timecop.travel(time_start + loop_counter) + false + end + end + ).to eq(false) + expect(loop_counter).to eq(1) + end + end + + context 'when max_attempts is reached first' do + it_behaves_like 'repeat until', max_attempts: 1, max_duration: 2 + end + + context 'when max_duration is reached first' do + it_behaves_like 'repeat until', max_attempts: 2, max_duration: 1 + end + end + end + end + end +end diff --git a/qa/spec/support/retrier_spec.rb b/qa/spec/support/retrier_spec.rb new file mode 100644 index 00000000000..fbe66a680f9 --- /dev/null +++ b/qa/spec/support/retrier_spec.rb @@ -0,0 +1,126 @@ +# frozen_string_literal: true + +require 'logger' +require 'timecop' + +describe QA::Support::Retrier do + before do + logger = ::Logger.new $stdout + logger.level = ::Logger::DEBUG + QA::Runtime::Logger.logger = logger + end + + describe '.retry_until' do + context 'when the condition is true' do + it 'logs max attempts (3 by default)' do + expect { subject.retry_until { true } } + .to output(/with retry_until: max_attempts: 3; reload_page: ; sleep_interval: 0; raise_on_failure: false; retry_on_exception: false/).to_stdout_from_any_process + end + + it 'logs max duration' do + expect { subject.retry_until(max_duration: 1) { true } } + .to output(/with retry_until: max_duration: 1; reload_page: ; sleep_interval: 0; raise_on_failure: false; retry_on_exception: false/).to_stdout_from_any_process + end + + it 'logs the end' do + expect { subject.retry_until { true } } + .to output(/ended retry_until$/).to_stdout_from_any_process + end + end + + context 'when the condition is false' do + it 'logs the start' do + expect { subject.retry_until(max_duration: 0) { false } } + .to output(/with retry_until: max_duration: 0; reload_page: ; sleep_interval: 0; raise_on_failure: false; retry_on_exception: false/).to_stdout_from_any_process + end + + it 'logs the end' do + expect { subject.retry_until(max_duration: 0) { false } } + .to output(/ended retry_until$/).to_stdout_from_any_process + end + end + + context 'when max_duration and max_attempts are nil' do + it 'sets max attempts to 3 by default' do + expect(subject).to receive(:repeat_until).with(hash_including(max_attempts: 3)) + + subject.retry_until + end + end + + it 'sets sleep_interval to 0 by default' do + expect(subject).to receive(:repeat_until).with(hash_including(sleep_interval: 0)) + + subject.retry_until + end + + it 'sets raise_on_failure to false by default' do + expect(subject).to receive(:repeat_until).with(hash_including(raise_on_failure: false)) + + subject.retry_until + end + + it 'sets retry_on_exception to false by default' do + expect(subject).to receive(:repeat_until).with(hash_including(retry_on_exception: false)) + + subject.retry_until + end + end + + describe '.retry_on_exception' do + context 'when the condition is true' do + it 'logs max_attempts, reload_page, and sleep_interval parameters' do + expect { subject.retry_on_exception(max_attempts: 1, reload_page: nil, sleep_interval: 0) { true } } + .to output(/with retry_on_exception: max_attempts: 1; reload_page: ; sleep_interval: 0/).to_stdout_from_any_process + end + + it 'logs the end' do + expect { subject.retry_on_exception(max_attempts: 1, reload_page: nil, sleep_interval: 0) { true } } + .to output(/ended retry_on_exception$/).to_stdout_from_any_process + end + end + + context 'when the condition is false' do + it 'logs the start' do + expect { subject.retry_on_exception(max_attempts: 1, reload_page: nil, sleep_interval: 0) { false } } + .to output(/with retry_on_exception: max_attempts: 1; reload_page: ; sleep_interval: 0/).to_stdout_from_any_process + end + + it 'logs the end' do + expect { subject.retry_on_exception(max_attempts: 1, reload_page: nil, sleep_interval: 0) { false } } + .to output(/ended retry_on_exception$/).to_stdout_from_any_process + end + end + + it 'does not repeat if no exception is raised' do + loop_counter = 0 + return_value = "test passed" + + expect( + subject.retry_on_exception(max_attempts: 2) do + loop_counter += 1 + return_value + end + ).to eq(return_value) + expect(loop_counter).to eq(1) + end + + it 'sets retry_on_exception to true' do + expect(subject).to receive(:repeat_until).with(hash_including(retry_on_exception: true)) + + subject.retry_on_exception + end + + it 'sets max_attempts to 3 by default' do + expect(subject).to receive(:repeat_until).with(hash_including(max_attempts: 3)) + + subject.retry_on_exception + end + + it 'sets sleep_interval to 0.5 by default' do + expect(subject).to receive(:repeat_until).with(hash_including(sleep_interval: 0.5)) + + subject.retry_on_exception + end + end +end diff --git a/qa/spec/support/waiter_spec.rb b/qa/spec/support/waiter_spec.rb index 8283b65e1be..06e404c862a 100644 --- a/qa/spec/support/waiter_spec.rb +++ b/qa/spec/support/waiter_spec.rb @@ -9,29 +9,53 @@ describe QA::Support::Waiter do QA::Runtime::Logger.logger = logger end - describe '.wait' do + describe '.wait_until' do context 'when the condition is true' do it 'logs the start' do - expect { subject.wait(max: 0) {} } - .to output(/with wait: max 0; interval 0.1/).to_stdout_from_any_process + expect { subject.wait_until(max_duration: 0, raise_on_failure: false) { true } } + .to output(/with wait_until: max_duration: 0; reload_page: ; sleep_interval: 0.1/).to_stdout_from_any_process end it 'logs the end' do - expect { subject.wait(max: 0) {} } - .to output(/ended wait after .* seconds$/).to_stdout_from_any_process + expect { subject.wait_until(max_duration: 0, raise_on_failure: false) { true } } + .to output(/ended wait_until$/).to_stdout_from_any_process end end context 'when the condition is false' do it 'logs the start' do - expect { subject.wait(max: 0) { false } } - .to output(/with wait: max 0; interval 0.1/).to_stdout_from_any_process + expect { subject.wait_until(max_duration: 0, raise_on_failure: false) { false } } + .to output(/with wait_until: max_duration: 0; reload_page: ; sleep_interval: 0.1/).to_stdout_from_any_process end it 'logs the end' do - expect { subject.wait(max: 0) { false } } - .to output(/ended wait after .* seconds$/).to_stdout_from_any_process + expect { subject.wait_until(max_duration: 0, raise_on_failure: false) { false } } + .to output(/ended wait_until$/).to_stdout_from_any_process end end + + it 'sets max_duration to 60 by default' do + expect(subject).to receive(:repeat_until).with(hash_including(max_duration: 60)) + + subject.wait_until + end + + it 'sets sleep_interval to 0.1 by default' do + expect(subject).to receive(:repeat_until).with(hash_including(sleep_interval: 0.1)) + + subject.wait_until + end + + it 'sets raise_on_failure to false by default' do + expect(subject).to receive(:repeat_until).with(hash_including(raise_on_failure: false)) + + subject.wait_until + end + + it 'sets retry_on_exception to false by default' do + expect(subject).to receive(:repeat_until).with(hash_including(retry_on_exception: false)) + + subject.wait_until + end end end |