summaryrefslogtreecommitdiff
path: root/qa
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-01-09 09:07:51 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-01-09 09:07:51 +0000
commit5afd8575506372dd64c238203bd05b4826f3ae2e (patch)
treee167192fdc7d73fcc1aa5bd33b535b813120ec37 /qa
parent8bda404e2919234c299f088b7d8d04f8449125de (diff)
downloadgitlab-ce-5afd8575506372dd64c238203bd05b4826f3ae2e.tar.gz
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'qa')
-rw-r--r--qa/Gemfile1
-rw-r--r--qa/Gemfile.lock2
-rw-r--r--qa/qa.rb3
-rw-r--r--qa/qa/page/base.rb10
-rw-r--r--qa/qa/resource/events/base.rb7
-rw-r--r--qa/qa/support/repeater.rb65
-rw-r--r--qa/qa/support/retrier.rb74
-rw-r--r--qa/qa/support/waiter.rb43
-rw-r--r--qa/qa/vendor/github/page/login.rb2
-rw-r--r--qa/qa/vendor/jenkins/page/configure.rb2
-rw-r--r--qa/qa/vendor/jenkins/page/login.rb2
-rw-r--r--qa/spec/page/base_spec.rb8
-rw-r--r--qa/spec/page/logging_spec.rb8
-rw-r--r--qa/spec/resource/events/project_spec.rb1
-rw-r--r--qa/spec/support/repeater_spec.rb385
-rw-r--r--qa/spec/support/retrier_spec.rb126
-rw-r--r--qa/spec/support/waiter_spec.rb42
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
diff --git a/qa/qa.rb b/qa/qa.rb
index ce0488fdc81..a0ce6caa3a9 100644
--- a/qa/qa.rb
+++ b/qa/qa.rb
@@ -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