diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-01-21 17:21:10 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-01-21 17:21:10 +0300 |
commit | cb0d23c455b73486fd1015f8ca9479b5b7e3585d (patch) | |
tree | d7dc129a407fd74266d2dc561bebf24665197c2f /qa | |
parent | c3e911be175c0aabfea1eb030f9e0ef23f5f3887 (diff) |
Add latest changes from gitlab-org/gitlab@12-7-stable-ee
Diffstat (limited to 'qa')
113 files changed, 1458 insertions, 473 deletions
diff --git a/qa/Dockerfile b/qa/Dockerfile index e4b860b08b2..126d9fbc591 100644 --- a/qa/Dockerfile +++ b/qa/Dockerfile @@ -39,6 +39,12 @@ RUN wget -q https://chromedriver.storage.googleapis.com/$(wget -q -O - https://c RUN unzip chromedriver_linux64.zip -d /usr/local/bin ## +# Install K3d local cluster support +# https://github.com/rancher/k3d +# +RUN curl -s https://raw.githubusercontent.com/rancher/k3d/master/install.sh | TAG=v1.3.4 bash + +## # Install gcloud and kubectl CLI used in Auto DevOps test to create K8s # clusters # 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/README.md b/qa/README.md index 332e5c8170f..1bfa83cadf1 100644 --- a/qa/README.md +++ b/qa/README.md @@ -30,7 +30,7 @@ and corresponding views / partials / selectors in CE / EE. Whenever `qa:selectors` job fails in your merge request, you are supposed to fix [page objects](../doc/development/testing_guide/end_to_end/page_objects.md). You should also trigger end-to-end tests -using `package-and-qa-manual` manual action, to test if everything works fine. +using `package-and-qa` manual action, to test if everything works fine. ## How can I use it? diff --git a/qa/load/artillery.yml b/qa/load/artillery.yml deleted file mode 100644 index 17d253ec480..00000000000 --- a/qa/load/artillery.yml +++ /dev/null @@ -1,25 +0,0 @@ -config: - target: "{{ $processEnvironment.HOST_URL }}" - http: - pool: 10 # All HTTP requests from all virtual users will be sent over the same <pool> connections. - # This also means that there is a limit on the number of requests sent per second. - phases: - - duration: 30 - arrivalRate: 10 - name: "Warm up" - - duration: 90 - arrivalRate: 10 - rampTo: 100 - name: "Gradual ramp up" - - duration: 90 - arrivalRate: 100 - name: "Sustained max load" -scenarios: - - name: "Visit large issue url" - flow: - - get: - url: "{{ $processEnvironment.LARGE_ISSUE_URL }}" - - name: "Visit large MR url" - flow: - - get: - url: "{{ $processEnvironment.LARGE_MR_URL }}" @@ -37,6 +37,7 @@ module QA autoload :MailHog, 'qa/runtime/mail_hog' autoload :IPAddress, 'qa/runtime/ip_address' autoload :Search, 'qa/runtime/search' + autoload :ApplicationSettings, 'qa/runtime/application_settings' module API autoload :Client, 'qa/runtime/api/client' @@ -487,8 +488,10 @@ 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/fixtures/auto_devops_rack/Dockerfile b/qa/qa/fixtures/auto_devops_rack/Dockerfile index 1f59c23ea88..6ab2795dd40 100644 --- a/qa/qa/fixtures/auto_devops_rack/Dockerfile +++ b/qa/qa/fixtures/auto_devops_rack/Dockerfile @@ -1,4 +1,4 @@ -FROM ruby:2.6.3-alpine +FROM ruby:2.6.5-alpine ADD ./ /app/ WORKDIR /app ENV RACK_ENV production diff --git a/qa/qa/page/base.rb b/qa/qa/page/base.rb index dcba4fc8544..a4c44f78ad4 100644 --- a/qa/qa/page/base.rb +++ b/qa/qa/page/base.rb @@ -8,6 +8,7 @@ module QA prepend Support::Page::Logging if Runtime::Env.debug? include Capybara::DSL include Scenario::Actable + include Support::WaitForRequests extend Validatable extend SingleForwardable @@ -21,27 +22,31 @@ module QA def refresh page.refresh + + wait_for_requests end - def wait(max: 60, interval: 0.1, reload: true) - QA::Support::Waiter.wait(max: max, interval: interval) do + def wait_until(max_duration: 60, sleep_interval: 0.1, reload: true, raise_on_failure: false) + Support::Waiter.wait_until(max_duration: max_duration, sleep_interval: sleep_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 def scroll_to(selector, text: nil) + wait_for_requests + page.execute_script <<~JS var elements = Array.from(document.querySelectorAll('#{selector}')); var text = '#{text}'; @@ -66,7 +71,7 @@ module QA xhr.send(); JS - return false unless wait(interval: 0.5, max: 60, reload: false) do + return false unless wait_until(sleep_interval: 0.5, max_duration: 60, reload: false) do page.evaluate_script('xhr.readyState == XMLHttpRequest.DONE') end @@ -74,6 +79,8 @@ module QA end def find_element(name, **kwargs) + wait_for_requests + find(element_selector_css(name), kwargs) end @@ -82,6 +89,12 @@ module QA end def all_elements(name, **kwargs) + if kwargs.keys.none? { |key| [:minimum, :maximum, :count, :between].include?(key) } + raise ArgumentError, "Please use :minimum, :maximum, :count, or :between so that all is more reliable" + end + + wait_for_requests + all(element_selector_css(name), **kwargs) end @@ -102,8 +115,8 @@ module QA end # replace with (..., page = self.class) - def click_element(name, page = nil, text: nil) - find_element(name, text: text).click + def click_element(name, page = nil, text: nil, wait: Capybara.default_max_wait_time) + find_element(name, text: text, wait: wait).click page.validate_elements_present! if page end @@ -119,33 +132,48 @@ module QA element.select value end + def has_active_element?(name, **kwargs) + has_element?(name, class: 'active', **kwargs) + end + def has_element?(name, **kwargs) - wait = kwargs[:wait] ? kwargs[:wait] && kwargs.delete(:wait) : Capybara.default_max_wait_time - text = kwargs[:text] ? kwargs[:text] && kwargs.delete(:text) : nil + wait_for_requests - has_css?(element_selector_css(name, kwargs), text: text, wait: wait) + wait = kwargs.delete(:wait) || Capybara.default_max_wait_time + text = kwargs.delete(:text) + klass = kwargs.delete(:class) + + has_css?(element_selector_css(name, kwargs), text: text, wait: wait, class: klass) end def has_no_element?(name, **kwargs) - wait = kwargs[:wait] ? kwargs[:wait] && kwargs.delete(:wait) : Capybara.default_max_wait_time - text = kwargs[:text] ? kwargs[:text] && kwargs.delete(:text) : nil + wait_for_requests + + wait = kwargs.delete(:wait) || Capybara.default_max_wait_time + text = kwargs.delete(:text) has_no_css?(element_selector_css(name, kwargs), wait: wait, text: text) end def has_text?(text, wait: Capybara.default_max_wait_time) + wait_for_requests + page.has_text?(text, wait: wait) end - def has_no_text?(text) - page.has_no_text? text + def has_no_text?(text, wait: Capybara.default_max_wait_time) + wait_for_requests + + page.has_no_text?(text, wait: wait) end def has_normalized_ws_text?(text, wait: Capybara.default_max_wait_time) - page.has_text?(text.gsub(/\s+/, " "), wait: wait) + has_text?(text.gsub(/\s+/, " "), wait: wait) end def finished_loading? + wait_for_requests + # The number of selectors should be able to be reduced after # migration to the new spinner is complete. # https://gitlab.com/groups/gitlab-org/-/epics/956 @@ -153,6 +181,8 @@ module QA end def finished_loading_block? + wait_for_requests + has_no_css?('.fa-spinner.block-loading', wait: Capybara.default_max_wait_time) end @@ -161,7 +191,7 @@ module QA # This loop gives time for the img tags to be rendered and for # images to start loading. previous_total_images = 0 - wait(interval: 1) do + wait_until(sleep_interval: 1) do current_total_images = all("img").size result = previous_total_images == current_total_images previous_total_images = current_total_images @@ -220,10 +250,14 @@ module QA end def click_link_with_text(text) + wait_for_requests + click_link text end def click_body + wait_for_requests + find('body').click end diff --git a/qa/qa/page/component/ci_badge_link.rb b/qa/qa/page/component/ci_badge_link.rb index aad8dc1d3df..d3e44fd867d 100644 --- a/qa/qa/page/component/ci_badge_link.rb +++ b/qa/qa/page/component/ci_badge_link.rb @@ -26,7 +26,7 @@ module QA private def completed?(timeout: 60) - wait(reload: false, max: timeout) do + wait_until(reload: false, max_duration: timeout) do COMPLETED_STATUSES.include?(status_badge) end end diff --git a/qa/qa/page/component/clone_panel.rb b/qa/qa/page/component/clone_panel.rb index b80877f5ecd..fbe19e5802b 100644 --- a/qa/qa/page/component/clone_panel.rb +++ b/qa/qa/page/component/clone_panel.rb @@ -24,7 +24,7 @@ module QA private def repository_clone_location(kind) - wait(reload: false) do + wait_until(reload: false) do click_element :clone_dropdown within_element :clone_options do diff --git a/qa/qa/page/component/dropdown_filter.rb b/qa/qa/page/component/dropdown_filter.rb index e896c382779..a39a04a668d 100644 --- a/qa/qa/page/component/dropdown_filter.rb +++ b/qa/qa/page/component/dropdown_filter.rb @@ -5,9 +5,7 @@ module QA module Component module DropdownFilter def filter_and_select(item) - wait(reload: false) do - page.has_css?('.dropdown-input-field') - end + page.has_css?('.dropdown-input-field', wait: QA::Support::Repeater::DEFAULT_MAX_WAIT_TIME) find('.dropdown-input-field').set(item) click_link item diff --git a/qa/qa/page/component/dropzone.rb b/qa/qa/page/component/dropzone.rb index 757111f240b..2efb96a02bc 100644 --- a/qa/qa/page/component/dropzone.rb +++ b/qa/qa/page/component/dropzone.rb @@ -23,7 +23,7 @@ module QA page.attach_file(attachment, class: 'dz-hidden-input', make_visible: field_style) # Wait for link to be appended to dropzone text - page.wait(reload: false) do + page.wait_until(reload: false) do page.find("#{container} textarea").value.match(filename) end end diff --git a/qa/qa/page/component/groups_filter.rb b/qa/qa/page/component/groups_filter.rb index cc50bb439b4..7eb1257db71 100644 --- a/qa/qa/page/component/groups_filter.rb +++ b/qa/qa/page/component/groups_filter.rb @@ -23,9 +23,7 @@ module QA # Since we submitted after filtering, the presence of # groups_list_tree_container means we have the complete filtered list # of groups - wait(reload: false) do - page.has_css?(element_selector_css(:groups_list_tree_container)) - end + has_element?(:groups_list_tree_container, wait: QA::Support::Repeater::DEFAULT_MAX_WAIT_TIME) # If there are no groups we'll know immediately because we filtered the list return false if page.has_text?('No groups or projects matched your search', wait: 0) diff --git a/qa/qa/page/component/issuable/common.rb b/qa/qa/page/component/issuable/common.rb index 9ecc8f73bdb..1155d4da036 100644 --- a/qa/qa/page/component/issuable/common.rb +++ b/qa/qa/page/component/issuable/common.rb @@ -23,11 +23,6 @@ module QA element :save_button element :delete_button end - - base.view 'app/assets/javascripts/issue_show/components/edit_actions.vue' do - element :save_button - element :delete_button - end end end end diff --git a/qa/qa/page/component/legacy_clone_panel.rb b/qa/qa/page/component/legacy_clone_panel.rb index e495cf4ef04..7b4b30623a6 100644 --- a/qa/qa/page/component/legacy_clone_panel.rb +++ b/qa/qa/page/component/legacy_clone_panel.rb @@ -30,7 +30,7 @@ module QA private def choose_repository_clone(kind, detect_text) - wait(reload: false) do + wait_until(reload: false) do click_element :clone_dropdown page.within('.clone-options-dropdown') do diff --git a/qa/qa/page/component/note.rb b/qa/qa/page/component/note.rb index c85fa690d6c..3e8ed9069ce 100644 --- a/qa/qa/page/component/note.rb +++ b/qa/qa/page/component/note.rb @@ -45,17 +45,17 @@ module QA click_element :comment_button end - def toggle_comments - all_elements(:toggle_comments_button).last.click + def toggle_comments(position) + all_elements(:toggle_comments_button, minimum: position)[position - 1].click end - def type_reply_to_discussion(reply_text) - all_elements(:discussion_reply_tab).last.click + def type_reply_to_discussion(position, reply_text) + all_elements(:discussion_reply_tab, minimum: position)[position - 1].click fill_element :reply_input, reply_text end - def reply_to_discussion(reply_text) - type_reply_to_discussion(reply_text) + def reply_to_discussion(position, reply_text) + type_reply_to_discussion(position, reply_text) click_element :reply_comment_button end diff --git a/qa/qa/page/file/shared/commit_button.rb b/qa/qa/page/file/shared/commit_button.rb index 559b4c6ceea..9ea4f4e7818 100644 --- a/qa/qa/page/file/shared/commit_button.rb +++ b/qa/qa/page/file/shared/commit_button.rb @@ -14,7 +14,7 @@ module QA def commit_changes click_element(:commit_button) - wait(reload: false, max: 60) do + wait_until(reload: false, max_duration: 60) do finished_loading? end end diff --git a/qa/qa/page/group/settings/general.rb b/qa/qa/page/group/settings/general.rb index efc8bbd7482..4a30403fda8 100644 --- a/qa/qa/page/group/settings/general.rb +++ b/qa/qa/page/group/settings/general.rb @@ -94,6 +94,18 @@ module QA select_element(:project_creation_level_dropdown, value) click_element :save_permissions_changes_button end + + def toggle_request_access + expand_section :permission_lfs_2fa_section + + if find_element(:request_access_checkbox).checked? + uncheck_element :request_access_checkbox + else + check_element :request_access_checkbox + end + + click_element :save_permissions_changes_button + end end end end diff --git a/qa/qa/page/group/show.rb b/qa/qa/page/group/show.rb index e1f319da134..7639def98b7 100644 --- a/qa/qa/page/group/show.rb +++ b/qa/qa/page/group/show.rb @@ -58,7 +58,7 @@ module QA QA::Support::Retrier.retry_on_exception(sleep_interval: 1.0) do within_element(:new_project_or_subgroup_dropdown) do # May need to click again because it is possible to click the button quicker than the JS is bound - wait(reload: false) do + wait_until(reload: false) do click_element :new_project_or_subgroup_dropdown_toggle has_element?(kind) diff --git a/qa/qa/page/group/sub_menus/members.rb b/qa/qa/page/group/sub_menus/members.rb index c8b3f5bb422..33c4caaddcb 100644 --- a/qa/qa/page/group/sub_menus/members.rb +++ b/qa/qa/page/group/sub_menus/members.rb @@ -7,12 +7,9 @@ module QA class Members < Page::Base include Page::Component::UsersSelect - view 'app/views/groups/group_members/_new_group_member.html.haml' do - element :add_to_group_button - end - - view 'app/helpers/groups/group_members_helper.rb' do + view 'app/views/shared/members/_invite_member.html.haml' do element :member_select_field + element :invite_member_button end view 'app/views/shared/members/_member.html.haml' do @@ -24,7 +21,7 @@ module QA def add_member(username) select_user :member_select_field, username - click_element :add_to_group_button + click_element :invite_member_button end def update_access_level(username, access_level) diff --git a/qa/qa/page/layout/performance_bar.rb b/qa/qa/page/layout/performance_bar.rb index 79e4d3edce0..4e144e67f12 100644 --- a/qa/qa/page/layout/performance_bar.rb +++ b/qa/qa/page/layout/performance_bar.rb @@ -9,25 +9,31 @@ module QA end view 'app/assets/javascripts/performance_bar/components/detailed_metric.vue' do - element :performance_bar_detailed_metric + element :detailed_metric_content end view 'app/assets/javascripts/performance_bar/components/request_selector.vue' do - element :performance_bar_request + element :request_dropdown_option + element :request_dropdown end def has_performance_bar? has_element?(:performance_bar) end - def has_detailed_metrics? - all_elements(:performance_bar_detailed_metric).all? do |metric| - metric.has_text?(%r{\d+}) + def has_detailed_metrics?(count) + retry_until(sleep_interval: 1) do + all_elements(:detailed_metric_content, count: count).all? do |metric| + metric.has_text?(%r{\d+}) + end end end def has_request_for?(path) - has_element?(:performance_bar_request, text: path) + click_element(:request_dropdown) + retry_until(sleep_interval: 1) do + has_element?(:request_dropdown_option, text: path) + end end end end diff --git a/qa/qa/page/main/menu.rb b/qa/qa/page/main/menu.rb index 5f4b3946e6a..8ad30632fa1 100644 --- a/qa/qa/page/main/menu.rb +++ b/qa/qa/page/main/menu.rb @@ -76,8 +76,14 @@ module QA end def sign_out - within_user_menu do - click_element :sign_out_link + retry_until do + break true unless signed_in? + + within_user_menu do + click_element :sign_out_link + end + + has_no_element?(:user_avatar) end end diff --git a/qa/qa/page/merge_request/show.rb b/qa/qa/page/merge_request/show.rb index c3645a6a755..ad5b3c97cb9 100644 --- a/qa/qa/page/merge_request/show.rb +++ b/qa/qa/page/merge_request/show.rb @@ -6,6 +6,10 @@ module QA class Show < Page::Base include Page::Component::Note + view 'app/assets/javascripts/mr_tabs_popover/components/popover.vue' do + element :dismiss_popover_button + end + view 'app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue' do element :dropdown_toggle element :download_email_patches @@ -29,6 +33,10 @@ module QA element :merged_status_content end + view 'app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue' do + element :merge_request_error_content + end + view 'app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue' do element :mr_rebase_button element :no_fast_forward_message, 'Fast-forward merge is not possible' # rubocop:disable QA/ElementWithPattern @@ -38,6 +46,10 @@ module QA element :squash_checkbox end + view 'app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue' do + element :skeleton_note + end + view 'app/views/projects/merge_requests/show.html.haml' do element :notes_tab element :diffs_tab @@ -61,32 +73,34 @@ module QA end def add_comment_to_diff(text) - wait(interval: 5) do + wait_until(sleep_interval: 5) do has_text?("No newline at end of file") end - all_elements(:new_diff_line).first.hover - click_element :diff_comment - fill_element :reply_input, text + all_elements(:new_diff_line, minimum: 1).first.hover + click_element(:diff_comment) + fill_element(:reply_input, text) end def click_discussions_tab - click_element :notes_tab + click_element(:notes_tab) - finished_loading? + wait_for_loading end def click_diffs_tab - click_element :diffs_tab + click_element(:diffs_tab) - finished_loading? + wait_for_loading + + click_element(:dismiss_popover_button) if has_element?(:dismiss_popover_button) end def click_pipeline_link - click_element :pipeline_link + click_element(:pipeline_link) end def edit! - click_element :edit_button + click_element(:edit_button) end def fast_forward_possible? @@ -126,12 +140,12 @@ module QA def mark_to_squash # The squash checkbox is disabled on load - wait do + wait_until do has_element?(:squash_checkbox) end # The squash checkbox is enabled via JS - wait(reload: false) do + wait_until(reload: false) do !find_element(:squash_checkbox).disabled? end @@ -150,30 +164,30 @@ module QA def ready_to_merge? # The merge button is disabled on load - wait do + wait_until do has_element?(:merge_button) end # The merge button is enabled via JS - wait(reload: false) do + wait_until(reload: false) do !find_element(:merge_button).disabled? end end def rebase! # The rebase button is disabled on load - wait do + wait_until do has_element?(:mr_rebase_button) end # The rebase button is enabled via JS - wait(reload: false) do + wait_until(reload: false) do !find_element(:mr_rebase_button).disabled? end click_element :mr_rebase_button - success = wait do + success = wait_until do has_text?('Fast-forward merge without a merge commit') end @@ -193,6 +207,16 @@ module QA click_element :dropdown_toggle visit_link_in_element(:download_plain_diff) end + + def wait_for_merge_request_error_message + wait_until(max_duration: 30, reload: false) do + has_element?(:merge_request_error_content) + end + end + + def wait_for_loading + finished_loading? && has_no_element?(:skeleton_note) + end end end end diff --git a/qa/qa/page/project/branches/show.rb b/qa/qa/page/project/branches/show.rb index 480fc7d78cb..63021df30f6 100644 --- a/qa/qa/page/project/branches/show.rb +++ b/qa/qa/page/project/branches/show.rb @@ -29,7 +29,7 @@ module QA end def has_no_branch?(branch_name, reload: false) - wait(reload: reload) do + wait_until(reload: reload) do within_element(:all_branches) do has_no_element?(:branch_name, text: branch_name) end diff --git a/qa/qa/page/project/import/github.rb b/qa/qa/page/project/import/github.rb index cc0c4e1e835..b533e0096a8 100644 --- a/qa/qa/page/project/import/github.rb +++ b/qa/qa/page/project/import/github.rb @@ -8,8 +8,8 @@ module QA include Page::Component::Select2 view 'app/views/import/github/new.html.haml' do - element :personal_access_token_field, 'text_field_tag :personal_access_token' # rubocop:disable QA/ElementWithPattern - element :authenticate_button, "submit_tag _('Authenticate')" # rubocop:disable QA/ElementWithPattern + element :personal_access_token_field + element :authenticate_button end view 'app/assets/javascripts/import_projects/components/provider_repo_table_row.vue' do @@ -20,11 +20,9 @@ module QA end def add_personal_access_token(personal_access_token) - fill_in 'personal_access_token', with: personal_access_token - end - - def list_repos - click_button 'List your GitHub repositories' + fill_element(:personal_access_token_field, personal_access_token) + click_element(:authenticate_button) + finished_loading? end def import!(full_path, name) @@ -37,7 +35,7 @@ module QA private def within_repo_path(full_path) - wait(reload: false) do + wait_until(reload: false) do has_element?(:project_import_row, text: full_path) end @@ -69,7 +67,7 @@ module QA end def wait_for_success - wait(max: 60, interval: 1.0, reload: false) do + wait_until(max_duration: 60, sleep_interval: 1.0, reload: false) do page.has_content?('Done', wait: 1.0) end end diff --git a/qa/qa/page/project/issue/index.rb b/qa/qa/page/project/issue/index.rb index a6ccee4353b..b5ad63ab8de 100644 --- a/qa/qa/page/project/issue/index.rb +++ b/qa/qa/page/project/issue/index.rb @@ -10,6 +10,7 @@ module QA end view 'app/views/projects/issues/_issue.html.haml' do + element :issue element :issue_link, 'link_to issue.title' # rubocop:disable QA/ElementWithPattern end @@ -21,10 +22,6 @@ module QA element :closed_issues_link end - def assignee_link_count - all_elements(:assignee_link).count - end - def avatar_counter find_element(:avatar_counter) end @@ -37,8 +34,12 @@ module QA click_element :closed_issues_link end + def has_assignee_link_count?(count) + all_elements(:assignee_link, count: count) + end + def has_issue?(issue) - has_element? :issue, issue_title: issue.to_s + has_element? :issue, issue_title: issue.title end end end diff --git a/qa/qa/page/project/issue/show.rb b/qa/qa/page/project/issue/show.rb index 1ef711d459e..a1e1bb4bc98 100644 --- a/qa/qa/page/project/issue/show.rb +++ b/qa/qa/page/project/issue/show.rb @@ -56,12 +56,6 @@ module QA element :new_note_form, 'attr: :note' # rubocop:disable QA/ElementWithPattern end - def avatar_image_count - wait_assignees_block_finish_loading do - all_elements(:avatar_image).count - end - end - def click_milestone_link click_element(:milestone_link) end @@ -88,12 +82,16 @@ module QA click_element :comment_button end - def has_comment?(comment_text) - wait(reload: false) do - has_element?(:noteable_note_item, text: comment_text) + def has_avatar_image_count?(count) + wait_assignees_block_finish_loading do + all_elements(:avatar_image, count: count) end end + def has_comment?(comment_text) + has_element?(:noteable_note_item, text: comment_text, wait: QA::Support::Repeater::DEFAULT_MAX_WAIT_TIME) + end + def more_assignees_link find_element(:more_assignees_link) end @@ -155,7 +153,7 @@ module QA def wait_assignees_block_finish_loading within_element(:assignee_block) do - wait(reload: false, max: 10, interval: 1) do + wait_until(reload: false, max_duration: 10, sleep_interval: 1) do finished_loading_block? yield end diff --git a/qa/qa/page/project/job/show.rb b/qa/qa/page/project/job/show.rb index a1a5b3c296e..07dea3449f1 100644 --- a/qa/qa/page/project/job/show.rb +++ b/qa/qa/page/project/job/show.rb @@ -5,8 +5,8 @@ module QA::Page class Show < QA::Page::Base include Component::CiBadgeLink - view 'app/assets/javascripts/jobs/components/job_log.vue' do - element :build_trace + view 'app/assets/javascripts/jobs/components/log/log.vue' do + element :job_log_content end view 'app/assets/javascripts/jobs/components/stages_dropdown.vue' do @@ -24,8 +24,8 @@ module QA::Page def output(wait: 5) result = '' - wait(reload: false, max: wait, interval: 1) do - result = find_element(:build_trace).text + wait_until(reload: false, max_duration: wait, sleep_interval: 1) do + result = find_element(:job_log_content).text result.include?('Job') end @@ -36,8 +36,8 @@ module QA::Page private def loaded?(wait: 60) - wait(reload: true, max: wait, interval: 1) do - has_element?(:build_trace, wait: 1) + wait_until(reload: true, max_duration: wait, sleep_interval: 1) do + has_element?(:job_log_content, wait: 1) end end end diff --git a/qa/qa/page/project/operations/environments/index.rb b/qa/qa/page/project/operations/environments/index.rb index 610a34385b1..6b46fa4985a 100644 --- a/qa/qa/page/project/operations/environments/index.rb +++ b/qa/qa/page/project/operations/environments/index.rb @@ -11,9 +11,7 @@ module QA end def click_environment_link(environment_name) - wait(reload: false) do - find(element_selector_css(:environment_link), text: environment_name).click - end + click_element(:environment_link, text: environment_name) end end end diff --git a/qa/qa/page/project/operations/kubernetes/index.rb b/qa/qa/page/project/operations/kubernetes/index.rb index de54319596d..84b58e9ea5b 100644 --- a/qa/qa/page/project/operations/kubernetes/index.rb +++ b/qa/qa/page/project/operations/kubernetes/index.rb @@ -13,6 +13,10 @@ module QA def add_kubernetes_cluster click_on 'Add Kubernetes cluster' end + + def has_cluster?(cluster) + has_element?(:cluster, cluster_name: cluster.to_s) + end end end end diff --git a/qa/qa/page/project/operations/kubernetes/show.rb b/qa/qa/page/project/operations/kubernetes/show.rb index fa276f15b8a..3d3eebdbec9 100644 --- a/qa/qa/page/project/operations/kubernetes/show.rb +++ b/qa/qa/page/project/operations/kubernetes/show.rb @@ -6,12 +6,6 @@ module QA module Operations module Kubernetes class Show < Page::Base - view 'app/assets/javascripts/clusters/components/application_row.vue' do - element :application_row, 'js-cluster-application-row-${this.id}' # rubocop:disable QA/ElementWithPattern - element :install_button, "__('Install')" # rubocop:disable QA/ElementWithPattern - element :installed_button, "__('Installed')" # rubocop:disable QA/ElementWithPattern - end - view 'app/assets/javascripts/clusters/components/applications.vue' do element :ingress_ip_address, 'id="ingress-endpoint"' # rubocop:disable QA/ElementWithPattern end @@ -22,15 +16,21 @@ module QA end def install!(application_name) - within(".js-cluster-application-row-#{application_name}") do - page.has_button?('Install', wait: 30) - click_on 'Install' + within_element(application_name) do + has_element?(:install_button, application: application_name, wait: 30) + click_on 'Install' # TODO replace with click_element end end def await_installed(application_name) - within(".js-cluster-application-row-#{application_name}") do - page.has_text?(/Installed|Uninstall/, wait: 300) + within_element(application_name) do + has_element?(:uninstall_button, application: application_name, wait: 300) + end + end + + def has_application_installed?(application_name) + within_element(application_name) do + has_element?(:uninstall_button, application: application_name, wait: 300) end end diff --git a/qa/qa/page/project/pipeline/index.rb b/qa/qa/page/project/pipeline/index.rb index 269d4dfc411..684ad4a59d5 100644 --- a/qa/qa/page/project/pipeline/index.rb +++ b/qa/qa/page/project/pipeline/index.rb @@ -18,7 +18,7 @@ module QA::Page end def wait_for_latest_pipeline_success - wait(reload: false, max: 300) do + wait_until(reload: false, max_duration: 300) do within_element_by_index(:pipeline_commit_status, 0) do has_text?('passed') end diff --git a/qa/qa/page/project/pipeline/show.rb b/qa/qa/page/project/pipeline/show.rb index fd29c5eacdc..45fffbf6000 100644 --- a/qa/qa/page/project/pipeline/show.rb +++ b/qa/qa/page/project/pipeline/show.rb @@ -67,13 +67,7 @@ module QA::Page end def click_on_first_job - css = '.js-pipeline-graph-job-link' - - wait(reload: false) do - has_css?(css) - end - - first(css).click + first('.js-pipeline-graph-job-link', wait: QA::Support::Repeater::DEFAULT_MAX_WAIT_TIME).click end end end diff --git a/qa/qa/page/project/settings/ci_variables.rb b/qa/qa/page/project/settings/ci_variables.rb index 3621e618bf2..64a182e5b3a 100644 --- a/qa/qa/page/project/settings/ci_variables.rb +++ b/qa/qa/page/project/settings/ci_variables.rb @@ -20,13 +20,13 @@ module QA end def fill_variable(key, value, masked) - keys = all_elements(:ci_variable_input_key) + keys = all_elements(:ci_variable_input_key, minimum: 1) index = keys.size - 1 # After we fill the key, JS would generate another field so # we need to use the same index to find the corresponding one. keys[index].set(key) - node = all_elements(:ci_variable_input_value)[index] + node = all_elements(:ci_variable_input_value, count: keys.size + 1)[index] # Simply run `node.set(value)` is too slow for long text here, # so we need to run JavaScript directly to set the value. @@ -34,7 +34,7 @@ module QA # https://github.com/teamcapybara/capybara/blob/679548cea10773d45e32808f4d964377cfe5e892/lib/capybara/selenium/node.rb#L217 execute_script("arguments[0].value = #{value.to_json}", node) - masked_node = all_elements(:variable_masked)[index] + masked_node = all_elements(:variable_masked, count: keys.size + 1)[index] toggle_masked(masked_node, masked) end @@ -55,7 +55,7 @@ module QA private def toggle_masked(masked_node, masked) - wait(reload: false) do + wait_until(reload: false) do masked_node.click masked ? masked_enabled?(masked_node) : masked_disabled?(masked_node) diff --git a/qa/qa/page/project/settings/deploy_keys.rb b/qa/qa/page/project/settings/deploy_keys.rb index 602bfc64710..c330d090ce6 100644 --- a/qa/qa/page/project/settings/deploy_keys.rb +++ b/qa/qa/page/project/settings/deploy_keys.rb @@ -18,7 +18,7 @@ module QA view 'app/assets/javascripts/deploy_keys/components/key.vue' do element :key element :key_title - element :key_fingerprint + element :key_md5_fingerprint end def add_key @@ -33,17 +33,17 @@ module QA fill_in 'deploy_key_key', with: key end - def find_fingerprint(title) + def find_md5_fingerprint(title) within_project_deploy_keys do find_element(:key, text: title) - .find(element_selector_css(:key_fingerprint)).text + .find(element_selector_css(:key_md5_fingerprint)).text.delete_prefix('MD5:') end end - def has_key?(title, fingerprint) + def has_key?(title, md5_fingerprint) within_project_deploy_keys do find_element(:key, text: title) - .has_css?(element_selector_css(:key_fingerprint), text: fingerprint) + .has_css?(element_selector_css(:key_md5_fingerprint), text: "MD5:#{md5_fingerprint}") end end @@ -53,18 +53,10 @@ module QA end end - def key_fingerprint - within_project_deploy_keys do - find_element(:key_fingerprint).text - end - end - private def within_project_deploy_keys - wait(reload: false) do - has_element?(:project_deploy_keys) - end + has_element?(:project_deploy_keys, wait: QA::Support::Repeater::DEFAULT_MAX_WAIT_TIME) within_element(:project_deploy_keys) do yield diff --git a/qa/qa/page/project/settings/deploy_tokens.rb b/qa/qa/page/project/settings/deploy_tokens.rb index ad34ebc13c2..3173752d40a 100644 --- a/qa/qa/page/project/settings/deploy_tokens.rb +++ b/qa/qa/page/project/settings/deploy_tokens.rb @@ -51,9 +51,7 @@ module QA private def within_new_project_deploy_token - wait(reload: false) do - has_css?(element_selector_css(:created_deploy_token_section)) - end + has_element?(:created_deploy_token_section, wait: QA::Support::Repeater::DEFAULT_MAX_WAIT_TIME) within_element(:created_deploy_token_section) do yield diff --git a/qa/qa/page/project/settings/members.rb b/qa/qa/page/project/settings/members.rb index 2ef018fd983..fd3e0add2a6 100644 --- a/qa/qa/page/project/settings/members.rb +++ b/qa/qa/page/project/settings/members.rb @@ -8,9 +8,9 @@ module QA include Page::Component::UsersSelect include QA::Page::Component::Select2 - view 'app/views/projects/project_members/_new_project_member.html.haml' do - element :member_select_input - element :add_member_button + view 'app/views/shared/members/_invite_member.html.haml' do + element :member_select_field + element :invite_member_button end view 'app/views/projects/project_members/_team.html.haml' do @@ -21,7 +21,7 @@ module QA element :invite_group_tab end - view 'app/views/projects/project_members/_new_project_group.html.haml' do + view 'app/views/shared/members/_invite_group.html.haml' do element :group_select_field element :invite_group_button end @@ -43,8 +43,8 @@ module QA end def add_member(username) - select_user :member_select_input, username - click_element :add_member_button + select_user :member_select_field, username + click_element :invite_member_button end def remove_group(group_name) diff --git a/qa/qa/page/project/settings/mirroring_repositories.rb b/qa/qa/page/project/settings/mirroring_repositories.rb index 4afe042d9fb..517163a22f1 100644 --- a/qa/qa/page/project/settings/mirroring_repositories.rb +++ b/qa/qa/page/project/settings/mirroring_repositories.rb @@ -77,9 +77,7 @@ module QA # The host key detection process is interrupted if we navigate away # from the page before the fingerprint appears. - wait(max: 5) do - find_element(:fingerprints_list).has_text? /.*/ - end + find_element(:fingerprints_list, text: /.*/) end def mirror_repository @@ -100,7 +98,7 @@ module QA sleep 5 refresh - wait(interval: 1) do + wait_until(sleep_interval: 1) do within_element_by_index(:mirrored_repository_row, row_index) do last_update = find_element(:mirror_last_update_at_cell, wait: 0) last_update.has_text?('just now') || last_update.has_text?('seconds') @@ -117,8 +115,8 @@ module QA private def find_repository_row_index(target_url) - wait(max: 5, reload: false) do - all_elements(:mirror_repository_url_cell).index do |url| + wait_until(max_duration: 5, reload: false) do + all_elements(:mirror_repository_url_cell, minimum: 1).index do |url| # The url might be a sanitized url but the target_url won't be so # we compare just the paths instead of the full url URI.parse(url.text).path == target_url.path diff --git a/qa/qa/page/project/settings/protected_branches.rb b/qa/qa/page/project/settings/protected_branches.rb index d1d2f302013..f718311fbf2 100644 --- a/qa/qa/page/project/settings/protected_branches.rb +++ b/qa/qa/page/project/settings/protected_branches.rb @@ -46,7 +46,7 @@ module QA end def protect_branch - click_element :protect_button + click_element(:protect_button, wait: QA::Support::Repeater::DEFAULT_MAX_WAIT_TIME) end private @@ -58,10 +58,9 @@ module QA within_element(:"allowed_to_#{action}_dropdown") do click_on allowed[:roles] + allowed[:users].each { |user| click_on user.username } if allowed.key?(:users) + allowed[:groups].each { |group| click_on group.name } if allowed.key?(:groups) end - - # Click the select element again to close the dropdown - click_element :protected_branch_select end end end diff --git a/qa/qa/page/project/show.rb b/qa/qa/page/project/show.rb index 102b6144a1e..c619bd6d6a3 100644 --- a/qa/qa/page/project/show.rb +++ b/qa/qa/page/project/show.rb @@ -61,9 +61,7 @@ module QA end def wait_for_viewers_to_load - wait(reload: false) do - has_no_element?(:spinner) - end + has_no_element?(:spinner, wait: QA::Support::Repeater::DEFAULT_MAX_WAIT_TIME) end def create_first_new_file! @@ -103,7 +101,7 @@ module QA end def new_merge_request - wait(reload: true) do + wait_until(reload: true) do has_css?(element_selector_css(:create_merge_request)) end @@ -127,7 +125,7 @@ module QA end def wait_for_import - wait(reload: true) do + wait_until(reload: true) do has_css?('.tree-holder') end end diff --git a/qa/qa/page/project/web_ide/edit.rb b/qa/qa/page/project/web_ide/edit.rb index 4d26cadcdfe..73b0856b445 100644 --- a/qa/qa/page/project/web_ide/edit.rb +++ b/qa/qa/page/project/web_ide/edit.rb @@ -69,7 +69,7 @@ module QA # Wait for the modal to fade out too has_no_element?(:new_file_modal) - wait(reload: false) do + wait_until(reload: false) do within_element(:file_templates_bar) do click_element :file_template_dropdown fill_element :dropdown_filter_input, template @@ -84,35 +84,29 @@ module QA end def commit_changes - # Clicking :begin_commit_button the first time switches from the + # Clicking :begin_commit_button switches from the # edit to the commit view click_element :begin_commit_button active_element? :commit_mode_tab - # We need to click :begin_commit_button again - click_element :begin_commit_button - - # After clicking :begin_commit_button the 2nd time there is an - # animation that hides :begin_commit_button and shows :commit_button + # After clicking :begin_commit_button, there is an animation + # that hides :begin_commit_button and shows :commit_button # # Wait for the animation to complete before clicking :commit_button # otherwise the click will quietly do nothing. - wait(reload: false) do + wait_until(reload: false) do has_no_element?(:begin_commit_button) && has_element?(:commit_button) end - # At this point we're ready to commit and the button should be - # labelled "Stage & Commit" - # # Click :commit_button and keep retrying just in case part of the # animation is still in process even when the buttons have the # expected visibility. - commit_success_msg_shown = retry_until do - click_element :commit_to_current_branch_radio - click_element :commit_button + commit_success_msg_shown = retry_until(sleep_interval: 5) do + click_element(:commit_to_current_branch_radio) if has_element?(:commit_to_current_branch_radio) + click_element(:commit_button) if has_element?(:commit_button) - wait(reload: false) do + wait_until(reload: false) do has_text?('Your changes have been committed') end end diff --git a/qa/qa/page/search/results.rb b/qa/qa/page/search/results.rb index 2f99d8da784..85f1d224935 100644 --- a/qa/qa/page/search/results.rb +++ b/qa/qa/page/search/results.rb @@ -19,11 +19,11 @@ module QA::Page end def switch_to_code - click_element(:code_tab) + switch_to_tab(:code_tab) end def switch_to_projects - click_element(:projects_tab) + switch_to_tab(:projects_tab) end def has_file_in_project?(file_name, project_name) @@ -32,7 +32,7 @@ module QA::Page def has_file_with_content?(file_name, file_text) within_element_by_index(:result_item_content, 0) do - false unless has_element?(:file_title_content, text: file_name) + break false unless has_element?(:file_title_content, text: file_name) has_element?(:file_text_content, text: file_text) end @@ -41,6 +41,15 @@ module QA::Page def has_project?(project_name) has_element?(:project, project_name: project_name) end + + private + + def switch_to_tab(tab) + retry_until do + click_element(tab) + has_active_element?(tab) + end + end end end end diff --git a/qa/qa/page/settings/common.rb b/qa/qa/page/settings/common.rb index bd1070158f0..6989e8125d3 100644 --- a/qa/qa/page/settings/common.rb +++ b/qa/qa/page/settings/common.rb @@ -10,7 +10,7 @@ module QA def expand_section(element_name) within_element(element_name) do # Because it is possible to click the button before the JS toggle code is bound - wait(reload: false) do + wait_until(reload: false) do click_button 'Expand' unless has_css?('button', text: 'Collapse', wait: 1) has_content?('Collapse') diff --git a/qa/qa/page/validatable.rb b/qa/qa/page/validatable.rb index 8467d261285..3c4d9ad68aa 100644 --- a/qa/qa/page/validatable.rb +++ b/qa/qa/page/validatable.rb @@ -11,8 +11,7 @@ module QA elements.each do |element| next unless element.required? - # TODO: this wait needs to be replaced by the wait class - unless base_page.has_element?(element.name, wait: 60) + unless base_page.has_element?(element.name, wait: QA::Support::Repeater::DEFAULT_MAX_WAIT_TIME) raise Validatable::PageValidationError, "#{element.name} did not appear on #{self.name} as expected" end end diff --git a/qa/qa/resource/base.rb b/qa/qa/resource/base.rb index 3bb62703290..873ba353051 100644 --- a/qa/qa/resource/base.rb +++ b/qa/qa/resource/base.rb @@ -66,18 +66,24 @@ module QA def visit! Runtime::Logger.debug(%Q[Visiting #{self.class.name} at "#{web_url}"]) + # Just in case an async action is not yet complete + Support::WaitForRequests.wait_for_requests + Support::Retrier.retry_until do visit(web_url) - wait { current_url.include?(URI.parse(web_url).path.split('/').last || web_url) } + wait_until { current_url.include?(URI.parse(web_url).path.split('/').last || web_url) } end + + # Wait until the new page is ready for us to interact with it + Support::WaitForRequests.wait_for_requests end def populate(*attributes) attributes.each(&method(:public_send)) end - def wait(max: 60, interval: 0.1) - QA::Support::Waiter.wait(max: max, interval: interval) do + def wait_until(max_duration: 60, sleep_interval: 0.1) + QA::Support::Waiter.wait_until(max_duration: max_duration, sleep_interval: sleep_interval) do yield end end diff --git a/qa/qa/resource/deploy_key.rb b/qa/qa/resource/deploy_key.rb index 869e2a71e47..26355729dab 100644 --- a/qa/qa/resource/deploy_key.rb +++ b/qa/qa/resource/deploy_key.rb @@ -5,10 +5,10 @@ module QA class DeployKey < Base attr_accessor :title, :key - attribute :fingerprint do + attribute :md5_fingerprint do Page::Project::Settings::Repository.perform do |setting| setting.expand_deploy_keys do |key| - key.find_fingerprint(title) + key.find_md5_fingerprint(title) end 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/resource/group.rb b/qa/qa/resource/group.rb index c12e9dd146b..0824512d238 100644 --- a/qa/qa/resource/group.rb +++ b/qa/qa/resource/group.rb @@ -39,7 +39,7 @@ module QA end # Ensure that the group was actually created - group_show.wait(interval: 1) do + group_show.wait_until(sleep_interval: 1) do group_show.has_text?(path) && group_show.has_new_project_or_subgroup_dropdown? end diff --git a/qa/qa/resource/issue.rb b/qa/qa/resource/issue.rb index 3bcff6a10ac..0817a9de06f 100644 --- a/qa/qa/resource/issue.rb +++ b/qa/qa/resource/issue.rb @@ -38,10 +38,6 @@ module QA end end - def to_s - @title - end - def api_get_path "/projects/#{project.id}/issues/#{id}" end diff --git a/qa/qa/resource/merge_request.rb b/qa/qa/resource/merge_request.rb index 24fb96a20a2..6c0f4621dd9 100644 --- a/qa/qa/resource/merge_request.rb +++ b/qa/qa/resource/merge_request.rb @@ -54,7 +54,7 @@ module QA @assignee = nil @milestone = nil @labels = [] - @file_name = "added_file.txt" + @file_name = "added_file-#{SecureRandom.hex(8)}.txt" @file_content = "File Added" @target_new_branch = true @no_preparation = false diff --git a/qa/qa/resource/merge_request_from_fork.rb b/qa/qa/resource/merge_request_from_fork.rb index 6c9a096289b..9cb4e6a49ca 100644 --- a/qa/qa/resource/merge_request_from_fork.rb +++ b/qa/qa/resource/merge_request_from_fork.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require 'securerandom' + module QA module Resource class MergeRequestFromFork < MergeRequest @@ -13,7 +15,7 @@ module QA Repository::ProjectPush.fabricate! do |resource| resource.project = fork.project resource.branch_name = fork_branch - resource.file_name = 'file2.txt' + resource.file_name = "file2-#{SecureRandom.hex(8)}.txt" resource.user = fork.user end end diff --git a/qa/qa/resource/project_imported_from_github.rb b/qa/qa/resource/project_imported_from_github.rb index 3e25235e6b8..e5ecaeae139 100644 --- a/qa/qa/resource/project_imported_from_github.rb +++ b/qa/qa/resource/project_imported_from_github.rb @@ -23,7 +23,6 @@ module QA Page::Project::Import::Github.perform do |import_page| import_page.add_personal_access_token(@personal_access_token) - import_page.list_repos import_page.import!(@github_repository_path, @name) end end diff --git a/qa/qa/resource/protected_branch.rb b/qa/qa/resource/protected_branch.rb index f0cef624e0b..9c65e0e5a31 100644 --- a/qa/qa/resource/protected_branch.rb +++ b/qa/qa/resource/protected_branch.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require 'securerandom' + module QA module Resource class ProtectedBranch < Base @@ -15,7 +17,7 @@ module QA attribute :branch do Repository::ProjectPush.fabricate! do |project_push| project_push.project = project - project_push.file_name = 'new_file.md' + project_push.file_name = "new_file-#{SecureRandom.hex(8)}.md" project_push.commit_message = 'Add new file' project_push.branch_name = branch_name project_push.new_branch = true @@ -47,11 +49,6 @@ module QA page.select_branch(branch_name) page.select_allowed_to_merge(allowed_to_merge) page.select_allowed_to_push(allowed_to_push) - - page.wait(reload: false) do - !page.first('.btn-success').disabled? - end - page.protect_branch end end diff --git a/qa/qa/resource/repository/commit.rb b/qa/qa/resource/repository/commit.rb index 4b5e8535ade..e3fb5bf486d 100644 --- a/qa/qa/resource/repository/commit.rb +++ b/qa/qa/resource/repository/commit.rb @@ -11,6 +11,8 @@ module QA :file_path, :sha + attribute :short_id + attribute :project do Project.fabricate! do |resource| resource.name = 'project-with-commit' diff --git a/qa/qa/resource/repository/project_push.rb b/qa/qa/resource/repository/project_push.rb index f79bb035c46..17596601cf9 100644 --- a/qa/qa/resource/repository/project_push.rb +++ b/qa/qa/resource/repository/project_push.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require 'securerandom' + module QA module Resource module Repository @@ -15,7 +17,7 @@ module QA end def initialize - @file_name = 'file.txt' + @file_name = "file-#{SecureRandom.hex(8)}.txt" @file_content = '# This is test project' @commit_message = "This is a test commit" @branch_name = 'master' diff --git a/qa/qa/resource/repository/push.rb b/qa/qa/resource/repository/push.rb index 68674248be2..902ae9f3135 100644 --- a/qa/qa/resource/repository/push.rb +++ b/qa/qa/resource/repository/push.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require 'pathname' +require 'securerandom' module QA module Resource @@ -13,7 +14,7 @@ module QA attr_writer :remote_branch, :gpg_key_id def initialize - @file_name = 'file.txt' + @file_name = "file-#{SecureRandom.hex(8)}.txt" @file_content = '# This is test file' @commit_message = "This is a test commit" @branch_name = 'master' diff --git a/qa/qa/resource/runner.rb b/qa/qa/resource/runner.rb index d1b4e8f7d54..f1f72c9cacd 100644 --- a/qa/qa/resource/runner.rb +++ b/qa/qa/resource/runner.rb @@ -54,7 +54,7 @@ module QA @id = this_runner[:id] super - + ensure Service::DockerRun::GitlabRunner.new(name).remove! end diff --git a/qa/qa/resource/sandbox.rb b/qa/qa/resource/sandbox.rb index 6c87fcb377a..54c13071cef 100644 --- a/qa/qa/resource/sandbox.rb +++ b/qa/qa/resource/sandbox.rb @@ -41,6 +41,14 @@ module QA resource_web_url(api_get) rescue ResourceNotFoundError super + + # If the group was just created the runners token might not be + # available via the API immediately. + Support::Retrier.retry_on_exception(sleep_interval: 5) do + resource = resource_web_url(api_get) + populate(:runners_token) + resource + end end def api_get_path diff --git a/qa/qa/resource/ssh_key.rb b/qa/qa/resource/ssh_key.rb index c140cb9ca62..22bdea424ca 100644 --- a/qa/qa/resource/ssh_key.rb +++ b/qa/qa/resource/ssh_key.rb @@ -7,7 +7,7 @@ module QA attr_accessor :title - def_delegators :key, :private_key, :public_key, :fingerprint + def_delegators :key, :private_key, :public_key, :md5_fingerprint def key @key ||= Runtime::Key::RSA.new diff --git a/qa/qa/runtime/application_settings.rb b/qa/qa/runtime/application_settings.rb new file mode 100644 index 00000000000..df6323f9a48 --- /dev/null +++ b/qa/qa/runtime/application_settings.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +module QA + module Runtime + module ApplicationSettings + extend self + extend Support::Api + + APPLICATION_SETTINGS_PATH = '/application/settings' + + # Set a GitLab application setting + # Example: + # #set({ allow_local_requests_from_web_hooks_and_services: true }) + # #set(allow_local_requests_from_web_hooks_and_services: true) + # https://docs.gitlab.com/ee/api/settings.html + def set_application_settings(**application_settings) + QA::Runtime::Logger.info("Setting application settings: #{application_settings}") + r = put(Runtime::API::Request.new(api_client, APPLICATION_SETTINGS_PATH).url, **application_settings) + raise "Couldn't set application settings #{application_settings.inspect}" unless r.code == QA::Support::Api::HTTP_STATUS_OK + end + + def get_application_settings + parse_body(get(Runtime::API::Request.new(api_client, APPLICATION_SETTINGS_PATH).url)) + end + + private + + def api_client + @api_client ||= begin + return Runtime::API::Client.new(:gitlab, personal_access_token: Runtime::Env.admin_personal_access_token) if Runtime::Env.admin_personal_access_token + + user = Resource::User.fabricate_via_api! do |user| + user.username = Runtime::User.admin_username + user.password = Runtime::User.admin_password + end + + unless user.admin? + raise "Administrator access is required to set application settings. User '#{user.username}' is not an administrator." + end + + Runtime::API::Client.new(:gitlab, user: user) + end + end + end + end +end diff --git a/qa/qa/runtime/browser.rb b/qa/qa/runtime/browser.rb index 69ba90702be..340f6dc0356 100644 --- a/qa/qa/runtime/browser.rb +++ b/qa/qa/runtime/browser.rb @@ -15,6 +15,10 @@ module QA CAPYBARA_MAX_WAIT_TIME = 10 + class << self + attr_accessor :rspec_configured, :capybara_configured + end + def initialize self.class.configure! end @@ -45,11 +49,40 @@ module QA end def self.configure! + configure_rspec! + configure_capybara! + end + + def self.configure_rspec! + # We don't want to enter this infinite loop: + # Runtime::Release.perform_before_hooks -> `QA::Runtime::Browser.visit` -> configure! -> configure_rspec! -> Runtime::Release.perform_before_hooks + # So we make sure this method is called only once. + return if self.rspec_configured + + browser = self + RSpec.configure do |config| config.define_derived_metadata(file_path: %r{/qa/specs/features/}) do |metadata| metadata[:type] = :feature end + + config.before do + unless browser.rspec_configured + browser.rspec_configured = true + + ## + # Perform before hooks, which are different for CE and EE + # + Runtime::Release.perform_before_hooks + end + end end + end + + def self.configure_capybara! + return if self.capybara_configured + + self.capybara_configured = true Capybara.server_port = 9887 + ENV['TEST_ENV_NUMBER'].to_i diff --git a/qa/qa/runtime/env.rb b/qa/qa/runtime/env.rb index 184ccd3ef07..6514e41e279 100644 --- a/qa/qa/runtime/env.rb +++ b/qa/qa/runtime/env.rb @@ -248,6 +248,10 @@ module QA raise ArgumentError, "Please provide GITHUB_ACCESS_TOKEN" end + def require_admin_access_token! + admin_personal_access_token || (raise ArgumentError, "GITLAB_QA_ADMIN_ACCESS_TOKEN is required!") + end + # Returns true if there is an environment variable that indicates that # the feature is supported in the environment under test. # All features are supported by default. diff --git a/qa/qa/runtime/feature.rb b/qa/qa/runtime/feature.rb index 8c19436ee12..25fc02a887e 100644 --- a/qa/qa/runtime/feature.rb +++ b/qa/qa/runtime/feature.rb @@ -33,7 +33,7 @@ module QA is_enabled = false - QA::Support::Waiter.wait(interval: 1) do + QA::Support::Waiter.wait_until(sleep_interval: 1) do is_enabled = enabled?(key) end diff --git a/qa/qa/runtime/key/base.rb b/qa/qa/runtime/key/base.rb index 1281eceaff0..72d1673438a 100644 --- a/qa/qa/runtime/key/base.rb +++ b/qa/qa/runtime/key/base.rb @@ -4,7 +4,7 @@ module QA module Runtime module Key class Base - attr_reader :name, :bits, :private_key, :public_key, :fingerprint + attr_reader :name, :bits, :private_key, :public_key, :md5_fingerprint def initialize(name, bits) @name = name @@ -29,7 +29,7 @@ module QA def populate_key_data(path) @private_key = ::File.binread(path) @public_key = ::File.binread("#{path}.pub") - @fingerprint = + @md5_fingerprint = `ssh-keygen -l -E md5 -f #{path} | cut -d' ' -f2 | cut -d: -f2-`.chomp end end diff --git a/qa/qa/runtime/logger.rb b/qa/qa/runtime/logger.rb index 7f73f1bd01b..a70c8faf7d2 100644 --- a/qa/qa/runtime/logger.rb +++ b/qa/qa/runtime/logger.rb @@ -14,11 +14,9 @@ module QA attr_writer :logger def logger - return @logger if @logger - - @logger = ::Logger.new Runtime::Env.log_destination - @logger.level = Runtime::Env.debug? ? ::Logger::DEBUG : ::Logger::ERROR - @logger + @logger ||= ::Logger.new(Runtime::Env.log_destination).tap do |logger| + logger.level = Runtime::Env.debug? ? ::Logger::DEBUG : ::Logger::ERROR + end end end end diff --git a/qa/qa/runtime/search.rb b/qa/qa/runtime/search.rb index faa110c96e7..74402301098 100644 --- a/qa/qa/runtime/search.rb +++ b/qa/qa/runtime/search.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require 'securerandom' + module QA module Runtime module Search @@ -8,26 +10,83 @@ module QA ElasticSearchServerError = Class.new(RuntimeError) - def elasticsearch_responding? + def assert_elasticsearch_responding QA::Runtime::Logger.debug("Attempting to search via Elasticsearch...") - QA::Support::Retrier.retry_on_exception do - # We don't care about the results of the search, we just need - # any search that uses Elasticsearch, not the native search - # The Elasticsearch-only scopes are blobs, wiki_blobs, and commits. - request = Runtime::API::Request.new(api_client, "/search?scope=blobs&search=foo") - response = get(request.url) + QA::Support::Retrier.retry_on_exception(max_attempts: 3) do + search_term = SecureRandom.hex(8) + + QA::Runtime::Logger.debug("Creating commit and project including search term '#{search_term}'...") - unless response.code == singleton_class::HTTP_STATUS_OK - raise ElasticSearchServerError, "Search attempt failed. Request returned (#{response.code}): `#{response}`." + content = "Elasticsearch test commit #{search_term}" + project = Resource::Project.fabricate_via_api! do |project| + project.name = "project-to-search-#{search_term}" + end + commit = Resource::Repository::Commit.fabricate_via_api! do |commit| + commit.project = project + commit.commit_message = content + commit.add_files( + [ + { + file_path: 'test.txt', + content: content + } + ] + ) end - true + find_commit(commit, "commit*#{search_term}") + find_project(project, "to-search*#{search_term}") + end + end + + def find_code(file_name, search_term) + find_target_in_scope('blobs', search_term) do |record| + record[:filename] == file_name && record[:data].include?(search_term) end + + QA::Runtime::Logger.debug("Found file '#{file_name} containing code '#{search_term}'") + end + + def find_commit(commit, search_term) + find_target_in_scope('commits', search_term) do |record| + record[:message] == commit.commit_message + end + + QA::Runtime::Logger.debug("Found commit '#{commit.commit_message} (#{commit.short_id})' via '#{search_term}'") + end + + def find_project(project, search_term) + find_target_in_scope('projects', search_term) do |record| + record[:name] == project.name + end + + QA::Runtime::Logger.debug("Found project '#{project.name}' via '#{search_term}'") end private + def find_target_in_scope(scope, search_term) + QA::Support::Retrier.retry_until(max_attempts: 10, sleep_interval: 10, raise_on_failure: true, retry_on_exception: true) do + result = search(scope, search_term) + result && result.any? { |record| yield record } + end + end + + def search(scope, term) + QA::Runtime::Logger.debug("Search scope '#{scope}' for '#{term}'...") + request = Runtime::API::Request.new(api_client, "/search?scope=#{scope}&search=#{term}") + response = get(request.url) + + unless response.code == singleton_class::HTTP_STATUS_OK + msg = "Search attempt failed. Request returned (#{response.code}): `#{response}`." + QA::Runtime::Logger.debug(msg) + raise ElasticSearchServerError, msg + end + + parse_body(response) + end + def api_client @api_client ||= Runtime::API::Client.new(:gitlab) end diff --git a/qa/qa/scenario/template.rb b/qa/qa/scenario/template.rb index 74d4c8f8757..97373f7a059 100644 --- a/qa/qa/scenario/template.rb +++ b/qa/qa/scenario/template.rb @@ -23,11 +23,6 @@ module QA def perform(options, *args) extract_address(:gitlab_address, options, args) - ## - # Perform before hooks, which are different for CE and EE - # - Runtime::Release.perform_before_hooks - Runtime::Feature.enable(options[:enable_feature]) if options.key?(:enable_feature) Specs::Runner.perform do |specs| diff --git a/qa/qa/scenario/test/instance.rb b/qa/qa/scenario/test/instance.rb index b4098619e4e..79dad7f4619 100644 --- a/qa/qa/scenario/test/instance.rb +++ b/qa/qa/scenario/test/instance.rb @@ -20,11 +20,6 @@ module QA def self.do_perform(address, *rspec_options) Runtime::Scenario.define(:gitlab_address, address) - ## - # Perform before hooks, which are different for CE and EE - # - Runtime::Release.perform_before_hooks - Specs::Runner.perform do |specs| specs.tty = true specs.options = rspec_options if rspec_options.any? diff --git a/qa/qa/service/cluster_provider/k3d.rb b/qa/qa/service/cluster_provider/k3d.rb index 8e117c2dbd5..fe02dde607c 100644 --- a/qa/qa/service/cluster_provider/k3d.rb +++ b/qa/qa/service/cluster_provider/k3d.rb @@ -6,6 +6,8 @@ module QA class K3d < Base def validate_dependencies find_executable('k3d') || raise("You must first install `k3d` executable to run these tests.") + Runtime::Env.require_admin_access_token! + Runtime::ApplicationSettings.set_application_settings(allow_local_requests_from_web_hooks_and_services: true) end def set_credentials(admin_user) @@ -24,6 +26,7 @@ module QA def teardown ENV['KUBECONFIG'] = @old_kubeconfig shell "k3d delete --name #{cluster_name}" + Runtime::ApplicationSettings.set_application_settings(allow_local_requests_from_web_hooks_and_services: false) end # Fetch "real" certificate diff --git a/qa/qa/service/kubernetes_cluster.rb b/qa/qa/service/kubernetes_cluster.rb index 26b5f58d2d3..84196556547 100644 --- a/qa/qa/service/kubernetes_cluster.rb +++ b/qa/qa/service/kubernetes_cluster.rb @@ -39,6 +39,10 @@ module QA @provider.cluster_name end + def to_s + cluster_name + end + private def fetch_api_url diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/login_via_oauth_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/login_via_oauth_spec.rb index 4fd80c353fb..70303a30153 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/login/login_via_oauth_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/login/login_via_oauth_spec.rb @@ -1,8 +1,7 @@ # frozen_string_literal: true module QA - # Failure issue: https://gitlab.com/gitlab-org/gitlab/issues/36305 - context 'Manage', :orchestrated, :oauth, :skip do + context 'Manage', :orchestrated, :oauth, quarantine: 'https://gitlab.com/gitlab-org/gitlab/issues/196517' do describe 'OAuth login' do it 'User logs in to GitLab with GitHub OAuth' do Runtime::Browser.visit(:gitlab, Page::Main::Login) diff --git a/qa/qa/specs/features/browser_ui/1_manage/project/import_github_repo_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/project/import_github_repo_spec.rb index e0045a4d8a1..14eaf770f10 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/project/import_github_repo_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/project/import_github_repo_spec.rb @@ -21,6 +21,8 @@ module QA delete delete_project_request.url expect_status(202) + + Page::Main::Menu.perform(&:sign_out_if_signed_in) end it 'user imports a GitHub repo' do diff --git a/qa/qa/specs/features/browser_ui/2_plan/email/trigger_email_notification_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/email/trigger_email_notification_spec.rb index 90290b4f2a0..eecf485a518 100644 --- a/qa/qa/specs/features/browser_ui/2_plan/email/trigger_email_notification_spec.rb +++ b/qa/qa/specs/features/browser_ui/2_plan/email/trigger_email_notification_spec.rb @@ -22,8 +22,15 @@ module QA expect(page).to have_content(/@#{user.username}(\n| )?Given access/) - # Wait for Action Mailer to deliver messages - mailhog_json = Support::Retrier.retry_until(sleep_interval: 1) do + mailhog_items = mailhog_json.dig('items') + + expect(mailhog_items).to include(an_object_satisfying { |o| /project was granted/ === o.dig('Content', 'Headers', 'Subject', 0) }) + end + + private + + def mailhog_json + Support::Retrier.retry_until(sleep_interval: 1) do Runtime::Logger.debug(%Q[retrieving "#{QA::Runtime::MailHog.api_messages_url}"]) mailhog_response = get QA::Runtime::MailHog.api_messages_url @@ -33,10 +40,6 @@ module QA # Expect at least two invitation messages: group and project mailhog_data if mailhog_data.dig('total') >= 2 end - - # Check json result from mailhog - mailhog_items = mailhog_json.dig('items') - expect(mailhog_items).to include(an_object_satisfying { |o| /project was granted/ === o.dig('Content', 'Headers', 'Subject', 0) }) end end end diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue/check_mentions_for_xss_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue/check_mentions_for_xss_spec.rb index 494108dbefc..aa88937504e 100644 --- a/qa/qa/specs/features/browser_ui/2_plan/issue/check_mentions_for_xss_spec.rb +++ b/qa/qa/specs/features/browser_ui/2_plan/issue/check_mentions_for_xss_spec.rb @@ -24,25 +24,19 @@ module QA project = Resource::Project.fabricate_via_api! do |resource| resource.name = 'xss-test-for-mentions-project' end - project.visit! - Page::Project::Show.perform(&:go_to_members_settings) - Page::Project::Settings::Members.perform do |members| - members.add_member(user.username) - end + Flow::Project.add_member(project: project, username: user.username) - issue = Resource::Issue.fabricate_via_api! do |issue| - issue.title = 'issue title' + Resource::Issue.fabricate_via_api! do |issue| issue.project = project - end - issue.visit! + end.visit! Page::Project::Issue::Show.perform do |show| show.select_all_activities_filter show.comment("cc-ing you here @#{user.username}") expect do - expect(show).to have_content("cc-ing you here") + expect(show).to have_comment("cc-ing you here") end.not_to raise_error # Selenium::WebDriver::Error::UnhandledAlertError end end diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue/close_issue_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue/close_issue_spec.rb index bab6b1ac5fc..2543c0091fb 100644 --- a/qa/qa/specs/features/browser_ui/2_plan/issue/close_issue_spec.rb +++ b/qa/qa/specs/features/browser_ui/2_plan/issue/close_issue_spec.rb @@ -4,12 +4,9 @@ module QA context 'Plan' do describe 'Close issue' do let(:issue) do - Resource::Issue.fabricate_via_api! do |issue| - issue.title = 'Issue to be closed via pushing a commit' - end + Resource::Issue.fabricate_via_api! end - let(:project) { issue.project } let(:issue_id) { issue.api_response[:iid] } before do @@ -27,7 +24,7 @@ module QA issue.visit! Page::Project::Issue::Show.perform do |show| - reopen_issue_button_visible = show.wait(reload: true) do + reopen_issue_button_visible = show.wait_until(reload: true) do show.has_element?(:reopen_issue_button, wait: 1.0) end expect(reopen_issue_button_visible).to be_truthy @@ -39,7 +36,7 @@ module QA push.commit_message = commit_message push.new_branch = new_branch push.file_content = commit_message - push.project = project + push.project = issue.project end end end diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue/collapse_comments_in_discussions_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue/collapse_comments_in_discussions_spec.rb index 77fcc4e9b6a..e505c0991a6 100644 --- a/qa/qa/specs/features/browser_ui/2_plan/issue/collapse_comments_in_discussions_spec.rb +++ b/qa/qa/specs/features/browser_ui/2_plan/issue/collapse_comments_in_discussions_spec.rb @@ -8,20 +8,12 @@ module QA before do Flow::Login.sign_in - issue = Resource::Issue.fabricate_via_api! do |issue| - issue.title = 'issue title' - end - - issue.visit! + Resource::Issue.fabricate_via_api!.visit! Page::Project::Issue::Show.perform do |show| - my_first_discussion = 'My first discussion' - show.select_all_activities_filter - show.start_discussion(my_first_discussion) - page.assert_text(my_first_discussion) - show.reply_to_discussion(my_first_reply) - page.assert_text(my_first_reply) + show.start_discussion('My first discussion') + show.reply_to_discussion(1, my_first_reply) end end diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue/comment_issue_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue/comment_issue_spec.rb index 77489c0ecf5..6c37e3ecbb9 100644 --- a/qa/qa/specs/features/browser_ui/2_plan/issue/comment_issue_spec.rb +++ b/qa/qa/specs/features/browser_ui/2_plan/issue/comment_issue_spec.rb @@ -6,10 +6,7 @@ module QA before do Flow::Login.sign_in - issue = Resource::Issue.fabricate_via_api! do |issue| - issue.title = 'issue title' - end - issue.visit! + Resource::Issue.fabricate_via_api!.visit! end it 'user comments on an issue and edits the comment' do diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue/create_issue_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue/create_issue_spec.rb index 254efb741b3..3b231b9930e 100644 --- a/qa/qa/specs/features/browser_ui/2_plan/issue/create_issue_spec.rb +++ b/qa/qa/specs/features/browser_ui/2_plan/issue/create_issue_spec.rb @@ -3,16 +3,12 @@ module QA context 'Plan', :smoke do describe 'Issue creation' do - let(:issue_title) { 'issue title' } - before do Flow::Login.sign_in end it 'user creates an issue' do - issue = Resource::Issue.fabricate_via_browser_ui! do |issue| - issue.title = issue_title - end + issue = Resource::Issue.fabricate_via_browser_ui! Page::Project::Menu.perform(&:click_issues) @@ -28,11 +24,7 @@ module QA end before do - issue = Resource::Issue.fabricate_via_api! do |issue| - issue.title = issue_title - end - - issue.visit! + Resource::Issue.fabricate_via_api!.visit! end it 'user comments on an issue with an attachment' do diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb index a4f6b0bb1bf..4156ba54785 100644 --- a/qa/qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb +++ b/qa/qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb @@ -3,16 +3,10 @@ module QA context 'Plan' do describe 'filter issue comments activities' do - let(:issue_title) { 'issue title' } - before do Flow::Login.sign_in - issue = Resource::Issue.fabricate_via_api! do |issue| - issue.title = issue_title - end - - issue.visit! + Resource::Issue.fabricate_via_api!.visit! end it 'user filters comments and activities in an issue' do diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue/mentions_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue/mentions_spec.rb index b1a80ad75cd..a0647df4097 100644 --- a/qa/qa/specs/features/browser_ui/2_plan/issue/mentions_spec.rb +++ b/qa/qa/specs/features/browser_ui/2_plan/issue/mentions_spec.rb @@ -12,18 +12,12 @@ module QA resource.name = 'project-to-test-mention' resource.visibility = 'private' end - project.visit! - Page::Project::Show.perform(&:go_to_members_settings) - Page::Project::Settings::Members.perform do |members| - members.add_member(@user.username) - end + project.add_member(@user) - issue = Resource::Issue.fabricate_via_api! do |issue| - issue.title = 'issue to test mention' + Resource::Issue.fabricate_via_api! do |issue| issue.project = project - end - issue.visit! + end.visit! end it 'user mentions another user in an issue' do diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/view_merge_request_diff_patch_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/view_merge_request_diff_patch_spec.rb index 0eaec61b2fa..604b6c10aee 100644 --- a/qa/qa/specs/features/browser_ui/3_create/merge_request/view_merge_request_diff_patch_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/view_merge_request_diff_patch_spec.rb @@ -4,12 +4,7 @@ module QA context 'Create' do describe 'Download merge request patch and diff' do before(:context) do - project = Resource::Project.fabricate_via_api! do |project| - project.name = 'project' - end - @merge_request = Resource::MergeRequest.fabricate_via_api! do |merge_request| - merge_request.project = project merge_request.title = 'This is a merge request' merge_request.description = '... for downloading patches and diffs' end @@ -23,7 +18,7 @@ module QA expect(page.text).to start_with('From') expect(page).to have_content('Subject: [PATCH] This is a test commit') - expect(page).to have_content('diff --git a/added_file.txt b/added_file.txt') + expect(page).to have_content("diff --git a/#{@merge_request.file_name} b/#{@merge_request.file_name}") end it 'views the merge request plain diff' do @@ -32,7 +27,7 @@ module QA @merge_request.visit! Page::MergeRequest::Show.perform(&:view_plain_diff) - expect(page.text).to start_with('diff --git a/added_file.txt b/added_file.txt') + expect(page.text).to start_with("diff --git a/#{@merge_request.file_name} b/#{@merge_request.file_name}") expect(page).to have_content('+File Added') end end diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/add_ssh_key_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/add_ssh_key_spec.rb index 474a7904fea..c3379d41ff2 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/add_ssh_key_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/add_ssh_key_spec.rb @@ -13,7 +13,7 @@ module QA end expect(page).to have_content("Title: #{key_title}") - expect(page).to have_content(key.fingerprint) + expect(page).to have_content(key.md5_fingerprint) Page::Main::Menu.perform(&:click_settings_link) Page::Profile::Menu.perform(&:click_ssh_keys) @@ -23,7 +23,7 @@ module QA end expect(page).not_to have_content("Title: #{key_title}") - expect(page).not_to have_content(key.fingerprint) + expect(page).not_to have_content(key.md5_fingerprint) end end end diff --git a/qa/qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb b/qa/qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb index 7c9db5ee496..70b571a316a 100644 --- a/qa/qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - context 'Create' do + context 'Create', quarantine: 'https://gitlab.com/gitlab-org/gitlab/issues/196034' do describe 'Web IDE file templates' do include Runtime::Fixtures diff --git a/qa/qa/specs/features/browser_ui/6_release/deploy_key/add_deploy_key_spec.rb b/qa/qa/specs/features/browser_ui/6_release/deploy_key/add_deploy_key_spec.rb index 9c964c726f1..89aba112407 100644 --- a/qa/qa/specs/features/browser_ui/6_release/deploy_key/add_deploy_key_spec.rb +++ b/qa/qa/specs/features/browser_ui/6_release/deploy_key/add_deploy_key_spec.rb @@ -15,11 +15,11 @@ module QA resource.key = deploy_key_value end - expect(deploy_key.fingerprint).to eq key.fingerprint + expect(deploy_key.md5_fingerprint).to eq key.md5_fingerprint Page::Project::Settings::Repository.perform do |setting| setting.expand_deploy_keys do |keys| - expect(keys).to have_key(deploy_key_title, key.fingerprint) + expect(keys).to have_key(deploy_key_title, key.md5_fingerprint) end end end diff --git a/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb b/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb index 3badaa983cb..0ca49bd080b 100644 --- a/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb +++ b/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb @@ -3,13 +3,9 @@ require 'digest/sha1' module QA - context 'Release', :docker do + context 'Release', :docker, quarantine: 'https://gitlab.com/gitlab-org/gitlab/issues/196047' do describe 'Git clone using a deploy key' do before do - # Handle WIP Job Logs flag - https://gitlab.com/gitlab-org/gitlab/issues/31162 - @job_log_json_flag_enabled = Runtime::Feature.enabled?('job_log_json') - Runtime::Feature.disable('job_log_json') if @job_log_json_flag_enabled - Flow::Login.sign_in @runner_name = "qa-runner-#{Time.now.to_i}" @@ -29,7 +25,6 @@ module QA end after do - Runtime::Feature.enable('job_log_json') if @job_log_json_flag_enabled Service::DockerRun::GitlabRunner.new(@runner_name).remove! end diff --git a/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb b/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb index a96bfde49f3..54014ff7067 100644 --- a/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb +++ b/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb @@ -50,7 +50,8 @@ module QA end end - describe 'Auto DevOps support', :orchestrated, :kubernetes do + # Failure issue: https://gitlab.com/gitlab-org/gitlab/issues/118481 + describe 'Auto DevOps support', :orchestrated, :kubernetes, :quarantine do context 'when rbac is enabled' do before(:all) do @cluster = Service::KubernetesCluster.new.create! diff --git a/qa/qa/specs/features/browser_ui/7_configure/kubernetes/kubernetes_integration_spec.rb b/qa/qa/specs/features/browser_ui/7_configure/kubernetes/kubernetes_integration_spec.rb new file mode 100644 index 00000000000..73b5a579e08 --- /dev/null +++ b/qa/qa/specs/features/browser_ui/7_configure/kubernetes/kubernetes_integration_spec.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +module QA + context 'Configure' do + # This test requires GITLAB_QA_ADMIN_ACCESS_TOKEN to be specified + describe 'Kubernetes Cluster Integration', :orchestrated, :kubernetes, :requires_admin, :skip do + context 'Project Clusters' do + let(:cluster) { Service::KubernetesCluster.new(provider_class: Service::ClusterProvider::K3d).create! } + let(:project) do + Resource::Project.fabricate_via_api! do |project| + project.name = 'project-with-k8s' + project.description = 'Project with Kubernetes cluster integration' + end + end + + before do + Flow::Login.sign_in + end + + after do + cluster.remove! + end + + it 'can create and associate a project cluster', :smoke do + Resource::KubernetesCluster.fabricate_via_browser_ui! do |k8s_cluster| + k8s_cluster.project = project + k8s_cluster.cluster = cluster + end + + project.visit! + + Page::Project::Menu.perform(&:go_to_operations_kubernetes) + + Page::Project::Operations::Kubernetes::Index.perform do |index| + expect(index).to have_cluster(cluster) + end + end + + it 'installs helm and tiller on a gitlab managed app' do + Resource::KubernetesCluster.fabricate_via_browser_ui! do |k8s_cluster| + k8s_cluster.project = project + k8s_cluster.cluster = cluster + k8s_cluster.install_helm_tiller = true + end + + Page::Project::Operations::Kubernetes::Show.perform do |show| + expect(show).to have_application_installed(:helm) + end + end + end + end + end +end diff --git a/qa/qa/specs/features/browser_ui/non_devops/performance_bar_spec.rb b/qa/qa/specs/features/browser_ui/non_devops/performance_bar_spec.rb index b067a44e325..4a5bb077e69 100644 --- a/qa/qa/specs/features/browser_ui/non_devops/performance_bar_spec.rb +++ b/qa/qa/specs/features/browser_ui/non_devops/performance_bar_spec.rb @@ -1,32 +1,36 @@ # frozen_string_literal: true module QA - # https://gitlab.com/gitlab-org/gitlab/issues/38315 - context 'Performance bar', :quarantine do - context 'when logged in as an admin user', :requires_admin do - before do - Flow::Login.sign_in_as_admin - Page::Main::Menu.perform(&:go_to_admin_area) - Page::Admin::Menu.perform(&:go_to_metrics_and_profiling_settings) + context 'Non-devops' do + describe 'Performance bar display', :requires_admin do + context 'when logged in as an admin user' do + # 4 metrics: pg, gitaly, redis, total + let(:metrics_count) { 4 } - Page::Admin::Settings::MetricsAndProfiling.perform do |setting| - setting.expand_performance_bar do |page| - page.enable_performance_bar - page.save_settings + before do + Flow::Login.sign_in_as_admin + Page::Main::Menu.perform(&:go_to_admin_area) + Page::Admin::Menu.perform(&:go_to_metrics_and_profiling_settings) + + Page::Admin::Settings::MetricsAndProfiling.perform do |setting| + setting.expand_performance_bar do |page| + page.enable_performance_bar + page.save_settings + end end end - end - it 'shows results for the original request and AJAX requests' do - # Issue pages always make AJAX requests - Resource::Issue.fabricate_via_browser_ui! do |issue| - issue.title = 'Performance bar test' - end + it 'shows results for the original request and AJAX requests' do + # Issue pages always make AJAX requests + Resource::Issue.fabricate_via_browser_ui! do |issue| + issue.title = 'Performance bar test' + end - Page::Layout::PerformanceBar.perform do |bar_component| - expect(bar_component).to have_performance_bar - expect(bar_component).to have_detailed_metrics - expect(bar_component).to have_request_for('realtime_changes') # Always requested on issue pages + Page::Layout::PerformanceBar.perform do |bar_component| + expect(bar_component).to have_performance_bar + expect(bar_component).to have_detailed_metrics(metrics_count) + expect(bar_component).to have_request_for('realtime_changes') # Always requested on issue pages + end end end end diff --git a/qa/qa/specs/helpers/quarantine.rb b/qa/qa/specs/helpers/quarantine.rb index ca0ce32e74f..8b14184f3b7 100644 --- a/qa/qa/specs/helpers/quarantine.rb +++ b/qa/qa/specs/helpers/quarantine.rb @@ -43,7 +43,23 @@ module QA::Specs::Helpers # the quarantined tests when they're not run so that we're aware of them skip("Only running tests tagged with :quarantine and any of #{included_filters.keys}") if should_skip_when_focused?(example.metadata, included_filters) else - skip('In quarantine') if example.metadata.key?(:quarantine) + if example.metadata.key?(:quarantine) + quarantine_message = %w(In quarantine) + quarantine_tag = example.metadata[:quarantine] + + if !!quarantine_tag + quarantine_message << case quarantine_tag + when String + ": #{quarantine_tag}" + when Hash + ": #{quarantine_tag[:issue]}" + else + '' + end + end + + skip(quarantine_message.join(' ').strip) + end end end diff --git a/qa/qa/support/page/logging.rb b/qa/qa/support/page/logging.rb index 5d73d9635f8..f59795e17c3 100644 --- a/qa/qa/support/page/logging.rb +++ b/qa/qa/support/page/logging.rb @@ -16,7 +16,7 @@ module QA super end - def wait(max: 60, interval: 0.1, reload: true) + def wait_until(max_duration: 60, sleep_interval: 0.1, reload: true, raise_on_failure: false) log("next wait uses reload: #{reload}") # Logging of wait start/end/duration is handled by QA::Support::Waiter @@ -119,10 +119,10 @@ module QA found end - def has_no_text?(text) + def has_no_text?(text, **kwargs) found = super - log(%Q{has_no_text?('#{text}') returned #{found}}) + log(%Q{has_no_text?('#{text}', wait: #{kwargs[:wait] || Capybara.default_max_wait_time}) returned #{found}}) found end @@ -173,6 +173,7 @@ module QA def log_has_element_or_not(method, name, found, **kwargs) msg = ["#{method} :#{name}"] msg << %Q(with text "#{kwargs[:text]}") if kwargs[:text] + msg << "class: #{kwargs[:class]}" if kwargs[:class] msg << "(wait: #{kwargs[:wait] || Capybara.default_max_wait_time})" msg << "returned: #{found}" 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/wait_for_requests.rb b/qa/qa/support/wait_for_requests.rb new file mode 100644 index 00000000000..5d5ba70a0c2 --- /dev/null +++ b/qa/qa/support/wait_for_requests.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module QA + module Support + module WaitForRequests + module_function + + def wait_for_requests + Waiter.wait_until do + finished_all_ajax_requests? && finished_all_axios_requests? + end + end + + def finished_all_axios_requests? + Capybara.page.evaluate_script('window.pendingRequests || 0').zero? + end + + def finished_all_ajax_requests? + return true if Capybara.page.evaluate_script('typeof jQuery === "undefined"') + + Capybara.page.evaluate_script('jQuery.active').zero? + end + end + end +end diff --git a/qa/qa/support/waiter.rb b/qa/qa/support/waiter.rb index fdcf2d7e157..fe63c930c7c 100644 --- a/qa/qa/support/waiter.rb +++ b/qa/qa/support/waiter.rb @@ -3,30 +3,33 @@ 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_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 + ) - while Time.now - start < max + 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 - if result - log_end(Time.now - start) - return result - end - - sleep(interval) 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..4675e33b514 100644 --- a/qa/qa/vendor/github/page/login.rb +++ b/qa/qa/vendor/github/page/login.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require 'capybara/dsl' +require 'benchmark' module QA module Vendor @@ -12,10 +13,16 @@ 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 - otp = OnePassword::CLI.new.otp + Support::Retrier.retry_until(raise_on_failure: true, sleep_interval: 35) do + fresh_otp = nil - fill_in 'otp', with: otp + time = Benchmark.realtime do + fresh_otp = OnePassword::CLI.instance.fresh_otp + end + + QA::Runtime::Logger.info("Returned fresh_otp: #{fresh_otp} in #{time} seconds") + + fill_in 'otp', with: fresh_otp click_on 'Verify' 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/qa/vendor/jenkins/page/new_credentials.rb b/qa/qa/vendor/jenkins/page/new_credentials.rb index bdef1a13fd4..b0d13973090 100644 --- a/qa/qa/vendor/jenkins/page/new_credentials.rb +++ b/qa/qa/vendor/jenkins/page/new_credentials.rb @@ -39,7 +39,7 @@ module QA end def wait_for_page_to_load - QA::Support::Waiter.wait(interval: 1.0) do + QA::Support::Waiter.wait_until(sleep_interval: 1.0) do page.has_css?('.setting-name', text: "Description") end end diff --git a/qa/qa/vendor/one_password/cli.rb b/qa/qa/vendor/one_password/cli.rb index 3cb69391783..cf8b7f8a4f9 100644 --- a/qa/qa/vendor/one_password/cli.rb +++ b/qa/qa/vendor/one_password/cli.rb @@ -1,9 +1,13 @@ # frozen_string_literal: true +require 'benchmark' + module QA module Vendor module OnePassword class CLI + include Singleton + def initialize @email = QA::Runtime::Env.gitlab_qa_1p_email @password = QA::Runtime::Env.gitlab_qa_1p_password @@ -11,14 +15,39 @@ module QA @github_uuid = QA::Runtime::Env.gitlab_qa_1p_github_uuid end - def otp - `#{op_path} get totp #{@github_uuid} --session=#{session_token}`.to_i + def fresh_otp + otps = [] + + # Fetches a fresh OTP and returns it only after op provides the same OTP twice + # An OTP is valid for 30 seconds so 70 attempts with 0.5 interval would ensure we complete 1 cycle + Support::Retrier.retry_until(max_attempts: 70, sleep_interval: 0.5) do + otps << fetch_otp + otps.size >= 3 && otps[-1] == otps[-2] && otps[-1] != otps[-3] + end + + otps.last end private + def fetch_otp + result = nil + + time = Benchmark.realtime do + result = `#{op_path} get totp #{@github_uuid} --session=#{session_token}`.to_i + end + + QA::Runtime::Logger.info("Fetched OTP: #{result} in: #{time} seconds") + + result + end + + # OP session tokens are valid for 30 minutes. We are caching the session token here and this is fine currently + # as we just have one test that is not expected to go over 30 minutes. + # But note that if we add more tests that use this class, we might need to add a mechanism to invalidate + # the cache after 30 minutes or if the session_token is rejected by op CLI. def session_token - `echo '#{@password}' | #{op_path} signin gitlab.1password.com #{@email} #{@secret} --output=raw --shorthand=gitlab_qa` + @session_token ||= `echo '#{@password}' | #{op_path} signin gitlab.1password.com #{@email} #{@secret} --output=raw --shorthand=gitlab_qa` end def op_path diff --git a/qa/qa/vendor/one_password/darwin/op b/qa/qa/vendor/one_password/darwin/op Binary files differindex 0f646522834..be7a3721b14 100755 --- a/qa/qa/vendor/one_password/darwin/op +++ b/qa/qa/vendor/one_password/darwin/op diff --git a/qa/qa/vendor/one_password/linux/op b/qa/qa/vendor/one_password/linux/op Binary files differindex 47ce87731be..47e79d7c599 100755 --- a/qa/qa/vendor/one_password/linux/op +++ b/qa/qa/vendor/one_password/linux/op diff --git a/qa/spec/page/base_spec.rb b/qa/spec/page/base_spec.rb index 9e3f143ea5b..2d13889d26d 100644 --- a/qa/spec/page/base_spec.rb +++ b/qa/spec/page/base_spec.rb @@ -3,7 +3,7 @@ describe QA::Page::Base do describe 'page helpers' do it 'exposes helpful page helpers' do - expect(subject).to respond_to :refresh, :wait, :scroll_to + expect(subject).to respond_to :refresh, :wait_until, :scroll_to end end @@ -62,18 +62,18 @@ describe QA::Page::Base do end end - describe '#wait' do + describe '#wait_until' do subject { Class.new(described_class).new } context 'when the condition is true' do it 'does not refresh' do expect(subject).not_to receive(:refresh) - subject.wait(max: 0.01) { true } + subject.wait_until(max_duration: 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_until(max_duration: 0.1, raise_on_failure: false) { true }).to be_truthy end end @@ -81,13 +81,29 @@ describe QA::Page::Base do it 'refreshes' do expect(subject).to receive(:refresh).at_least(:once) - subject.wait(max: 0.01) { false } + subject.wait_until(max_duration: 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_until(max_duration: 0.01, raise_on_failure: false) { false }).to be_falsey + end + end + end + + describe '#all_elements' do + before do + allow(subject).to receive(:all) + end + + it 'raises an error if count or minimum are not specified' do + expect { subject.all_elements(:foo) }.to raise_error ArgumentError + end + + it 'does not raise an error if :minimum, :maximum, :count, or :between is specified' do + [:minimum, :maximum, :count, :between].each do |param| + expect { subject.all_elements(:foo, param => 1) }.not_to raise_error end end end diff --git a/qa/spec/page/logging_spec.rb b/qa/spec/page/logging_spec.rb index fb89bcd3ab4..a6b61e9b1ee 100644 --- a/qa/spec/page/logging_spec.rb +++ b/qa/spec/page/logging_spec.rb @@ -28,21 +28,21 @@ describe QA::Support::Page::Logging do end it 'logs wait' do - expect { subject.wait(max: 0) {} } + expect { subject.wait_until(max_duration: 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 - expect { subject.wait(max: 0) {} } - .to output(/ended wait after .* seconds$/).to_stdout_from_any_process + expect { subject.wait_until(max_duration: 0) {} } + .to output(/with wait_until/).to_stdout_from_any_process + expect { subject.wait_until(max_duration: 0) {} } + .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) {} } + expect { subject.wait_until(max_duration: 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 - expect { subject.wait(max: 0, reload: false) {} } - .to output(/ended wait after .* seconds$/).to_stdout_from_any_process + expect { subject.wait_until(max_duration: 0, reload: false) {} } + .to output(/with wait_until/).to_stdout_from_any_process + expect { subject.wait_until(max_duration: 0, reload: false) {} } + .to output(/ended wait_until$/).to_stdout_from_any_process end it 'logs scroll_to' do @@ -121,10 +121,10 @@ describe QA::Support::Page::Logging do end it 'logs has_no_text?' do - allow(page).to receive(:has_no_text?).with('foo').and_return(true) + allow(page).to receive(:has_no_text?).with('foo', any_args).and_return(true) expect { subject.has_no_text? 'foo' } - .to output(/has_no_text\?\('foo'\) returned true/).to_stdout_from_any_process + .to output(/has_no_text\?\('foo', wait: #{QA::Runtime::Browser::CAPYBARA_MAX_WAIT_TIME}\) returned true/).to_stdout_from_any_process end it 'logs finished_loading?' do @@ -145,18 +145,18 @@ describe QA::Support::Page::Logging do it 'logs the number of elements found' do allow(page).to receive(:all).and_return([1, 2]) - expect { subject.all_elements(:element) } + expect { subject.all_elements(:element, count: 2) } .to output(/finding all :element/).to_stdout_from_any_process - expect { subject.all_elements(:element) } + expect { subject.all_elements(:element, count: 2) } .to output(/found 2 :element/).to_stdout_from_any_process end it 'logs 0 if no elements are found' do allow(page).to receive(:all).and_return([]) - expect { subject.all_elements(:element) } + expect { subject.all_elements(:element, count: 1) } .to output(/finding all :element/).to_stdout_from_any_process - expect { subject.all_elements(:element) } + expect { subject.all_elements(:element, count: 1) } .not_to output(/found 0 :elements/).to_stdout_from_any_process end end 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/runtime/application_settings_spec.rb b/qa/spec/runtime/application_settings_spec.rb new file mode 100644 index 00000000000..fce0361aee0 --- /dev/null +++ b/qa/spec/runtime/application_settings_spec.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +describe QA::Runtime::ApplicationSettings do + let(:api_client) { double('QA::Runtime::API::Client') } + let(:request) { Struct.new(:url).new('http://api') } + let(:get_response) { Struct.new(:body).new("{}") } + + before do + allow(described_class).to receive(:api_client).and_return(api_client) + end + + describe '.set_application_settings' do + it 'sets application settings' do + expect(QA::Runtime::API::Request) + .to receive(:new) + .with(api_client, '/application/settings') + .and_return(request) + + expect(described_class) + .to receive(:put) + .with(request.url, { allow_local_requests_from_web_hooks_and_services: true }) + .and_return(Struct.new(:code).new(200)) + + subject.set_application_settings(allow_local_requests_from_web_hooks_and_services: true) + end + end + + describe '.get_application_settings' do + it 'gets application settings' do + expect(QA::Runtime::API::Request) + .to receive(:new) + .with(api_client, '/application/settings') + .and_return(request) + + expect(described_class) + .to receive(:get) + .with(request.url) + .and_return(get_response) + + subject.get_application_settings + end + end +end diff --git a/qa/spec/runtime/env_spec.rb b/qa/spec/runtime/env_spec.rb index 340831aa06d..0a0bf33a726 100644 --- a/qa/spec/runtime/env_spec.rb +++ b/qa/spec/runtime/env_spec.rb @@ -230,6 +230,20 @@ describe QA::Runtime::Env do end end + describe '.require_admin_access_token!' do + it 'raises ArgumentError if GITLAB_QA_ADMIN_ACCESS_TOKEN is not specified' do + stub_env('GITLAB_QA_ADMIN_ACCESS_TOKEN', nil) + + expect { described_class.require_admin_access_token! }.to raise_error(ArgumentError) + end + + it 'does not raise exception if GITLAB_QA_ADMIN_ACCESS_TOKEN is specified' do + stub_env('GITLAB_QA_ADMIN_ACCESS_TOKEN', 'foobar123') + + expect { described_class.require_admin_access_token! }.not_to raise_error + end + end + describe '.log_destination' do it 'returns $stdout if QA_LOG_PATH is not defined' do stub_env('QA_LOG_PATH', nil) diff --git a/qa/spec/spec_helper.rb b/qa/spec/spec_helper.rb index 3a26ed89e9c..1336bea16bc 100644 --- a/qa/spec/spec_helper.rb +++ b/qa/spec/spec_helper.rb @@ -12,9 +12,9 @@ QA::Runtime::Browser.configure! QA::Runtime::Scenario.from_env(QA::Runtime::Env.runtime_scenario_attributes) if QA::Runtime::Env.runtime_scenario_attributes -%w[helpers shared_examples].each do |d| - Dir[::File.join(__dir__, d, '**', '*.rb')].each { |f| require f } -end +Dir[::File.join(__dir__, "support/helpers/*.rb")].each { |f| require f } +Dir[::File.join(__dir__, "support/shared_contexts/*.rb")].each { |f| require f } +Dir[::File.join(__dir__, "support/shared_examples/*.rb")].each { |f| require f } RSpec.configure do |config| QA::Specs::Helpers::Quarantine.configure_rspec diff --git a/qa/spec/specs/helpers/quarantine_spec.rb b/qa/spec/specs/helpers/quarantine_spec.rb index 2538632c032..d5c6820f0a9 100644 --- a/qa/spec/specs/helpers/quarantine_spec.rb +++ b/qa/spec/specs/helpers/quarantine_spec.rb @@ -155,6 +155,26 @@ describe QA::Specs::Helpers::Quarantine do expect(group.examples.first.execution_result.status).to eq(:passed) end + + context 'quarantine message' do + shared_examples 'test with quarantine message' do |quarantine_tag| + it 'outputs the quarantine message' do + group = describe_successfully do + it('is quarantined', quarantine: quarantine_tag) {} + end + + expect(group.examples.first.execution_result.pending_message) + .to eq('In quarantine : for a reason') + end + end + + it_behaves_like 'test with quarantine message', 'for a reason' + + it_behaves_like 'test with quarantine message', { + issue: 'for a reason', + environment: [:nightly, :staging] + } + end end context 'with :quarantine focused' do diff --git a/qa/spec/helpers/stub_env.rb b/qa/spec/support/helpers/stub_env.rb index 8ad864dbec8..8ad864dbec8 100644 --- a/qa/spec/helpers/stub_env.rb +++ b/qa/spec/support/helpers/stub_env.rb 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/shared_examples/scenario_shared_examples.rb b/qa/spec/support/shared_examples/scenario_shared_examples.rb index 697e6cb39c8..17469ea470c 100644 --- a/qa/spec/shared_examples/scenario_shared_examples.rb +++ b/qa/spec/support/shared_examples/scenario_shared_examples.rb @@ -31,12 +31,6 @@ shared_examples 'a QA scenario class' do expect(attributes).to have_received(:define).with(:gitlab_address, 'http://gitlab_address').at_least(:once) end - it 'performs before hooks' do - subject.perform(args) - - expect(release).to have_received(:perform_before_hooks) - end - it 'sets tags on runner' do subject.perform(args) 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 |