diff options
author | Lin Jen-Shin <godfat@godfat.org> | 2017-12-15 17:14:26 +0800 |
---|---|---|
committer | Lin Jen-Shin <godfat@godfat.org> | 2017-12-15 17:14:26 +0800 |
commit | 59ac184fcf64f1812fbfd88a00ea029ca3c1f4e7 (patch) | |
tree | 5db0594a6f568f02b4f54c6bf4eabe01229a9f95 /qa | |
parent | 85be6d83be4632c76760e373da131a90afb093b9 (diff) | |
parent | 1baea77438779e74657b49ca26810d6c8f041b41 (diff) | |
download | gitlab-ce-59ac184fcf64f1812fbfd88a00ea029ca3c1f4e7.tar.gz |
Merge remote-tracking branch 'upstream/master' into no-ivar-in-modules
* upstream/master: (671 commits)
Make rubocop happy
Use guard clause
Improve language
Prettify
Use temp branch
Pass info about who started the job and which job triggered it
Docs: add indexes for monitoring and performance monitoring
clearer-documentation-on-inline-diffs
Add docs for commit diff discussion in merge requests
sorting for tags api
Clear BatchLoader after each spec to prevent holding onto records longer than necessary
Include project in BatchLoader key to prevent returning blobs for the wrong project
moved lfs_blob_ids method into ExtractsPath module
Converted JS modules into exported modules
spec fixes
Bump gitlab-shell version to 5.10.3
Clear caches before updating MR diffs
Use new Ruby version 2.4 in GitLab QA images
moved lfs blob fetch from extractspath file
Update GitLab QA dependencies
...
Diffstat (limited to 'qa')
-rw-r--r-- | qa/Dockerfile | 2 | ||||
-rw-r--r-- | qa/Gemfile | 12 | ||||
-rw-r--r-- | qa/Gemfile.lock | 84 | ||||
-rw-r--r-- | qa/qa.rb | 6 | ||||
-rw-r--r-- | qa/qa/page/base.rb | 16 | ||||
-rw-r--r-- | qa/qa/page/group/new.rb | 1 | ||||
-rw-r--r-- | qa/qa/page/main/entry.rb | 26 | ||||
-rw-r--r-- | qa/qa/page/main/login.rb | 8 | ||||
-rw-r--r-- | qa/qa/page/mattermost/login.rb | 8 | ||||
-rw-r--r-- | qa/qa/runtime/browser.rb | 109 | ||||
-rw-r--r-- | qa/qa/scenario/entrypoint.rb | 1 | ||||
-rw-r--r-- | qa/qa/scenario/gitlab/admin/hashed_storage.rb | 1 | ||||
-rw-r--r-- | qa/qa/scenario/gitlab/repository/push.rb | 47 | ||||
-rw-r--r-- | qa/qa/specs/config.rb | 63 | ||||
-rw-r--r-- | qa/qa/specs/features/login/standard_spec.rb | 4 | ||||
-rw-r--r-- | qa/qa/specs/features/mattermost/group_create_spec.rb | 2 | ||||
-rw-r--r-- | qa/qa/specs/features/mattermost/login_spec.rb | 25 | ||||
-rw-r--r-- | qa/qa/specs/features/project/create_spec.rb | 2 | ||||
-rw-r--r-- | qa/qa/specs/features/repository/clone_spec.rb | 2 | ||||
-rw-r--r-- | qa/qa/specs/features/repository/push_spec.rb | 21 | ||||
-rw-r--r-- | qa/qa/specs/runner.rb | 2 |
21 files changed, 256 insertions, 186 deletions
diff --git a/qa/Dockerfile b/qa/Dockerfile index 9b6ffff7c4d..ed2ee73bea0 100644 --- a/qa/Dockerfile +++ b/qa/Dockerfile @@ -1,4 +1,4 @@ -FROM ruby:2.3 +FROM ruby:2.4 LABEL maintainer "Grzegorz Bizon <grzegorz@gitlab.com>" ENV DEBIAN_FRONTEND noninteractive diff --git a/qa/Gemfile b/qa/Gemfile index ff29824529f..4c866a3f893 100644 --- a/qa/Gemfile +++ b/qa/Gemfile @@ -1,8 +1,8 @@ source 'https://rubygems.org' -gem 'pry-byebug', '~> 3.4.1', platform: :mri -gem 'capybara', '~> 2.12.1' -gem 'capybara-screenshot', '~> 1.0.14' -gem 'rake', '~> 12.0.0' -gem 'rspec', '~> 3.5' -gem 'selenium-webdriver', '~> 2.53' +gem 'pry-byebug', '~> 3.5.1', platform: :mri +gem 'capybara', '~> 2.16.1' +gem 'capybara-screenshot', '~> 1.0.18' +gem 'rake', '~> 12.3.0' +gem 'rspec', '~> 3.7' +gem 'selenium-webdriver', '~> 3.8.0' diff --git a/qa/Gemfile.lock b/qa/Gemfile.lock index 22d12b479cb..88d5fe834a0 100644 --- a/qa/Gemfile.lock +++ b/qa/Gemfile.lock @@ -1,78 +1,72 @@ GEM remote: https://rubygems.org/ specs: - addressable (2.5.0) - public_suffix (~> 2.0, >= 2.0.2) - byebug (9.0.6) - capybara (2.12.1) + addressable (2.5.2) + public_suffix (>= 2.0.2, < 4.0) + byebug (9.1.0) + capybara (2.16.1) addressable - mime-types (>= 1.16) + mini_mime (>= 0.1.3) nokogiri (>= 1.3.3) rack (>= 1.0.0) rack-test (>= 0.5.4) xpath (~> 2.0) - capybara-screenshot (1.0.14) + capybara-screenshot (1.0.18) capybara (>= 1.0, < 3) launchy - childprocess (0.7.0) + childprocess (0.8.0) ffi (~> 1.0, >= 1.0.11) - coderay (1.1.1) + coderay (1.1.2) diff-lcs (1.3) ffi (1.9.18) launchy (2.4.3) addressable (~> 2.3) - method_source (0.8.2) - mime-types (3.1) - mime-types-data (~> 3.2015) - mime-types-data (3.2016.0521) + method_source (0.9.0) + mini_mime (1.0.0) mini_portile2 (2.3.0) nokogiri (1.8.1) mini_portile2 (~> 2.3.0) - pry (0.10.4) + pry (0.11.3) coderay (~> 1.1.0) - method_source (~> 0.8.1) - slop (~> 3.4) - pry-byebug (3.4.2) - byebug (~> 9.0) + method_source (~> 0.9.0) + pry-byebug (3.5.1) + byebug (~> 9.1) pry (~> 0.10) - public_suffix (2.0.5) - rack (2.0.1) - rack-test (0.6.3) - rack (>= 1.0) - rake (12.0.0) - rspec (3.5.0) - rspec-core (~> 3.5.0) - rspec-expectations (~> 3.5.0) - rspec-mocks (~> 3.5.0) - rspec-core (3.5.4) - rspec-support (~> 3.5.0) - rspec-expectations (3.5.0) + public_suffix (3.0.1) + rack (2.0.3) + rack-test (0.8.2) + rack (>= 1.0, < 3) + rake (12.3.0) + rspec (3.7.0) + rspec-core (~> 3.7.0) + rspec-expectations (~> 3.7.0) + rspec-mocks (~> 3.7.0) + rspec-core (3.7.0) + rspec-support (~> 3.7.0) + rspec-expectations (3.7.0) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.5.0) - rspec-mocks (3.5.0) + rspec-support (~> 3.7.0) + rspec-mocks (3.7.0) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.5.0) - rspec-support (3.5.0) + rspec-support (~> 3.7.0) + rspec-support (3.7.0) rubyzip (1.2.1) - selenium-webdriver (2.53.4) + selenium-webdriver (3.8.0) childprocess (~> 0.5) rubyzip (~> 1.0) - websocket (~> 1.0) - slop (3.6.0) - websocket (1.2.4) - xpath (2.0.0) + xpath (2.1.0) nokogiri (~> 1.3) PLATFORMS ruby DEPENDENCIES - capybara (~> 2.12.1) - capybara-screenshot (~> 1.0.14) - pry-byebug (~> 3.4.1) - rake (~> 12.0.0) - rspec (~> 3.5) - selenium-webdriver (~> 2.53) + capybara (~> 2.16.1) + capybara-screenshot (~> 1.0.18) + pry-byebug (~> 3.5.1) + rake (~> 12.3.0) + rspec (~> 3.7) + selenium-webdriver (~> 3.8.0) BUNDLED WITH - 1.15.4 + 1.16.0 @@ -9,6 +9,7 @@ module QA autoload :User, 'qa/runtime/user' autoload :Namespace, 'qa/runtime/namespace' autoload :Scenario, 'qa/runtime/scenario' + autoload :Browser, 'qa/runtime/browser' end ## @@ -46,6 +47,10 @@ module QA autoload :Create, 'qa/scenario/gitlab/project/create' end + module Repository + autoload :Push, 'qa/scenario/gitlab/repository/push' + end + module Sandbox autoload :Prepare, 'qa/scenario/gitlab/sandbox/prepare' end @@ -65,7 +70,6 @@ module QA autoload :Base, 'qa/page/base' module Main - autoload :Entry, 'qa/page/main/entry' autoload :Login, 'qa/page/main/login' autoload :Menu, 'qa/page/main/menu' autoload :OAuth, 'qa/page/main/oauth' diff --git a/qa/qa/page/base.rb b/qa/qa/page/base.rb index f9a93ef051e..99eba02b6e3 100644 --- a/qa/qa/page/base.rb +++ b/qa/qa/page/base.rb @@ -10,6 +10,18 @@ module QA visit current_url end + def wait(css = '.application', time: 60) + Time.now.tap do |start| + while Time.now - start < time + break if page.has_css?(css, wait: 5) + + refresh + end + end + + yield if block_given? + end + def scroll_to(selector, text: nil) page.execute_script <<~JS var elements = Array.from(document.querySelectorAll('#{selector}')); @@ -24,6 +36,10 @@ module QA page.within(selector) { yield } if block_given? end + + def self.path + raise NotImplementedError + end end end end diff --git a/qa/qa/page/group/new.rb b/qa/qa/page/group/new.rb index cb743a7bf11..53fdaaed078 100644 --- a/qa/qa/page/group/new.rb +++ b/qa/qa/page/group/new.rb @@ -4,6 +4,7 @@ module QA class New < Page::Base def set_path(path) fill_in 'group_path', with: path + fill_in 'group_name', with: path end def set_description(description) diff --git a/qa/qa/page/main/entry.rb b/qa/qa/page/main/entry.rb deleted file mode 100644 index ae6484b4bfe..00000000000 --- a/qa/qa/page/main/entry.rb +++ /dev/null @@ -1,26 +0,0 @@ -module QA - module Page - module Main - class Entry < Page::Base - def visit_login_page - visit("#{Runtime::Scenario.gitlab_address}/users/sign_in") - wait_for_instance_to_be_ready - end - - private - - def wait_for_instance_to_be_ready - # This resolves cold boot / background tasks problems - # - start = Time.now - - while Time.now - start < 240 - break if page.has_css?('.application', wait: 10) - - refresh - end - end - end - end - end -end diff --git a/qa/qa/page/main/login.rb b/qa/qa/page/main/login.rb index 8b0111a78a2..f88325f408b 100644 --- a/qa/qa/page/main/login.rb +++ b/qa/qa/page/main/login.rb @@ -2,6 +2,10 @@ module QA module Page module Main class Login < Page::Base + def initialize + wait('.application', time: 500) + end + def sign_in_using_credentials if page.has_content?('Change your password') fill_in :user_password, with: Runtime::User.password @@ -13,6 +17,10 @@ module QA fill_in :user_password, with: Runtime::User.password click_button 'Sign in' end + + def self.path + '/users/sign_in' + end end end end diff --git a/qa/qa/page/mattermost/login.rb b/qa/qa/page/mattermost/login.rb index 42ab9c6f675..8ffd4fdad13 100644 --- a/qa/qa/page/mattermost/login.rb +++ b/qa/qa/page/mattermost/login.rb @@ -2,10 +2,6 @@ module QA module Page module Mattermost class Login < Page::Base - def initialize - visit(Runtime::Scenario.mattermost_address + '/login') - end - def sign_in_using_oauth click_link class: 'btn btn-custom-login gitlab' @@ -13,6 +9,10 @@ module QA click_button 'Authorize' end end + + def self.path + '/login' + end end end end diff --git a/qa/qa/runtime/browser.rb b/qa/qa/runtime/browser.rb new file mode 100644 index 00000000000..6fb37fdfc7f --- /dev/null +++ b/qa/qa/runtime/browser.rb @@ -0,0 +1,109 @@ +require 'rspec/core' +require 'capybara/rspec' +require 'capybara-screenshot/rspec' +require 'selenium-webdriver' + +module QA + module Runtime + class Browser + include QA::Scenario::Actable + + def initialize + self.class.configure! + end + + ## + # Visit a page that belongs to a GitLab instance under given address. + # + # Example: + # + # visit(:gitlab, Page::Main::Login) + # visit('http://gitlab.example/users/sign_in') + # + # In case of an address that is a symbol we will try to guess address + # based on `Runtime::Scenario#something_address`. + # + def visit(address, page, &block) + Browser::Session.new(address, page).tap do |session| + session.perform(&block) + end + end + + def self.visit(address, page, &block) + new.visit(address, page, &block) + end + + def self.configure! + return if Capybara.drivers.include?(:chrome) + + Capybara.register_driver :chrome do |app| + capabilities = Selenium::WebDriver::Remote::Capabilities.chrome( + 'chromeOptions' => { + 'args' => %w[headless no-sandbox disable-gpu window-size=1280,1680] + } + ) + + Capybara::Selenium::Driver + .new(app, browser: :chrome, desired_capabilities: capabilities) + end + + Capybara::Screenshot.register_driver(:chrome) do |driver, path| + driver.browser.save_screenshot(path) + end + + Capybara.configure do |config| + config.default_driver = :chrome + config.javascript_driver = :chrome + config.default_max_wait_time = 10 + # https://github.com/mattheworiordan/capybara-screenshot/issues/164 + config.save_path = 'tmp' + end + end + + class Session + include Capybara::DSL + + def initialize(instance, page = nil) + @instance = instance + @address = host + page&.path + end + + def host + if @instance.is_a?(Symbol) + Runtime::Scenario.send("#{@instance}_address") + else + @instance.to_s + end + end + + def perform(&block) + visit(@address) + + yield if block_given? + rescue + raise if block.nil? + + # RSpec examples will take care of screenshots on their own + # + unless block.binding.receiver.is_a?(RSpec::Core::ExampleGroup) + screenshot_and_save_page + end + + raise + ensure + clear! if block_given? + end + + ## + # Selenium allows to reset session cookies for current domain only. + # + # See gitlab-org/gitlab-qa#102 + # + def clear! + visit(@address) + reset_session! + end + end + end + end +end diff --git a/qa/qa/scenario/entrypoint.rb b/qa/qa/scenario/entrypoint.rb index b9d924651a0..ae099fd911e 100644 --- a/qa/qa/scenario/entrypoint.rb +++ b/qa/qa/scenario/entrypoint.rb @@ -8,7 +8,6 @@ module QA include Bootable def perform(address, *files) - Specs::Config.act { configure_capybara! } Runtime::Scenario.define(:gitlab_address, address) ## diff --git a/qa/qa/scenario/gitlab/admin/hashed_storage.rb b/qa/qa/scenario/gitlab/admin/hashed_storage.rb index ac2cd549829..44604c6bc66 100644 --- a/qa/qa/scenario/gitlab/admin/hashed_storage.rb +++ b/qa/qa/scenario/gitlab/admin/hashed_storage.rb @@ -6,7 +6,6 @@ module QA def perform(*traits) raise ArgumentError unless traits.include?(:enabled) - Page::Main::Entry.act { visit_login_page } Page::Main::Login.act { sign_in_using_credentials } Page::Main::Menu.act { go_to_admin_area } Page::Admin::Menu.act { go_to_settings } diff --git a/qa/qa/scenario/gitlab/repository/push.rb b/qa/qa/scenario/gitlab/repository/push.rb new file mode 100644 index 00000000000..b00ab0c313a --- /dev/null +++ b/qa/qa/scenario/gitlab/repository/push.rb @@ -0,0 +1,47 @@ +require "pry-byebug" + +module QA + module Scenario + module Gitlab + module Repository + class Push < Scenario::Template + PAGE_REGEX_CHECK = + %r{\/#{Runtime::Namespace.sandbox_name}\/qa-test[^\/]+\/{1}[^\/]+\z}.freeze + + attr_writer :file_name, + :file_content, + :commit_message, + :branch_name + + def initialize + @file_name = 'file.txt' + @file_content = '# This is test project' + @commit_message = "Add #{@file_name}" + @branch_name = 'master' + end + + def perform + Git::Repository.perform do |repository| + repository.location = Page::Project::Show.act do + unless PAGE_REGEX_CHECK.match(current_path) + raise "To perform this scenario the current page should be project show." + end + + choose_repository_clone_http + repository_location + end + + repository.use_default_credentials + repository.clone + repository.configure_identity('GitLab QA', 'root@gitlab.com') + + repository.add_file(@file_name, @file_content) + repository.commit(@commit_message) + repository.push_changes(@branch_name) + end + end + end + end + end + end +end diff --git a/qa/qa/specs/config.rb b/qa/qa/specs/config.rb deleted file mode 100644 index bce7923e52d..00000000000 --- a/qa/qa/specs/config.rb +++ /dev/null @@ -1,63 +0,0 @@ -require 'rspec/core' -require 'capybara/rspec' -require 'capybara-screenshot/rspec' -require 'selenium-webdriver' - -# rubocop:disable Metrics/MethodLength -# rubocop:disable Metrics/LineLength - -module QA - module Specs - class Config < Scenario::Template - include Scenario::Actable - - def perform - configure_rspec! - configure_capybara! - end - - def configure_rspec! - RSpec.configure do |config| - config.expect_with :rspec do |expectations| - expectations.include_chain_clauses_in_custom_matcher_descriptions = true - end - - config.mock_with :rspec do |mocks| - mocks.verify_partial_doubles = true - end - - config.order = :random - Kernel.srand config.seed - config.formatter = :documentation - config.color = true - end - end - - def configure_capybara! - Capybara.register_driver :chrome do |app| - capabilities = Selenium::WebDriver::Remote::Capabilities.chrome( - 'chromeOptions' => { - 'args' => %w[headless no-sandbox disable-gpu window-size=1280,1680] - } - ) - - Capybara::Selenium::Driver - .new(app, browser: :chrome, desired_capabilities: capabilities) - end - - Capybara::Screenshot.register_driver(:chrome) do |driver, path| - driver.browser.save_screenshot(path) - end - - Capybara.configure do |config| - config.default_driver = :chrome - config.javascript_driver = :chrome - config.default_max_wait_time = 10 - - # https://github.com/mattheworiordan/capybara-screenshot/issues/164 - config.save_path = 'tmp' - end - end - end - end -end diff --git a/qa/qa/specs/features/login/standard_spec.rb b/qa/qa/specs/features/login/standard_spec.rb index b155708c387..9eaa2b772e6 100644 --- a/qa/qa/specs/features/login/standard_spec.rb +++ b/qa/qa/specs/features/login/standard_spec.rb @@ -1,7 +1,7 @@ module QA - feature 'standard root login', :core do + feature 'standard user login', :core do scenario 'user logs in using credentials' do - Page::Main::Entry.act { visit_login_page } + Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } # TODO, since `Signed in successfully` message was removed diff --git a/qa/qa/specs/features/mattermost/group_create_spec.rb b/qa/qa/specs/features/mattermost/group_create_spec.rb index 853a9a6a4f4..b3dbe44bf6e 100644 --- a/qa/qa/specs/features/mattermost/group_create_spec.rb +++ b/qa/qa/specs/features/mattermost/group_create_spec.rb @@ -1,7 +1,7 @@ module QA feature 'create a new group', :mattermost do scenario 'creating a group with a mattermost team' do - Page::Main::Entry.act { visit_login_page } + Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } Page::Main::Menu.act { go_to_groups } diff --git a/qa/qa/specs/features/mattermost/login_spec.rb b/qa/qa/specs/features/mattermost/login_spec.rb index 1fde3f89a99..637bbdd643a 100644 --- a/qa/qa/specs/features/mattermost/login_spec.rb +++ b/qa/qa/specs/features/mattermost/login_spec.rb @@ -1,24 +1,17 @@ module QA feature 'logging in to Mattermost', :mattermost do scenario 'can use gitlab oauth' do - Page::Main::Entry.act { visit_login_page } - Page::Main::Login.act { sign_in_using_credentials } - Page::Mattermost::Login.act { sign_in_using_oauth } + Runtime::Browser.visit(:gitlab, Page::Main::Login) do + Page::Main::Login.act { sign_in_using_credentials } - Page::Mattermost::Main.perform do |page| - expect(page).to have_content(/(Welcome to: Mattermost|Logout GitLab Mattermost)/) - end - end + Runtime::Browser.visit(:mattermost, Page::Mattermost::Login) do + Page::Mattermost::Login.act { sign_in_using_oauth } - ## - # TODO, temporary workaround for gitlab-org/gitlab-qa#102. - # - after do - visit Runtime::Scenario.mattermost_address - reset_session! - - visit Runtime::Scenario.gitlab_address - reset_session! + Page::Mattermost::Main.perform do |page| + expect(page).to have_content(/(Welcome to: Mattermost|Logout GitLab Mattermost)/) + end + end + end end end end diff --git a/qa/qa/specs/features/project/create_spec.rb b/qa/qa/specs/features/project/create_spec.rb index aba0c2b4c14..0b3accb848d 100644 --- a/qa/qa/specs/features/project/create_spec.rb +++ b/qa/qa/specs/features/project/create_spec.rb @@ -1,7 +1,7 @@ module QA feature 'create a new project', :core do scenario 'user creates a new project' do - Page::Main::Entry.act { visit_login_page } + Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } Scenario::Gitlab::Project::Create.perform do |project| diff --git a/qa/qa/specs/features/repository/clone_spec.rb b/qa/qa/specs/features/repository/clone_spec.rb index 5cc3b3b9c1b..c5c24622657 100644 --- a/qa/qa/specs/features/repository/clone_spec.rb +++ b/qa/qa/specs/features/repository/clone_spec.rb @@ -9,7 +9,7 @@ module QA end before do - Page::Main::Entry.act { visit_login_page } + Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } Scenario::Gitlab::Project::Create.perform do |scenario| diff --git a/qa/qa/specs/features/repository/push_spec.rb b/qa/qa/specs/features/repository/push_spec.rb index 30935dc1e13..ef29dfa2d2f 100644 --- a/qa/qa/specs/features/repository/push_spec.rb +++ b/qa/qa/specs/features/repository/push_spec.rb @@ -2,7 +2,7 @@ module QA feature 'push code to repository', :core do context 'with regular account over http' do scenario 'user pushes code to the repository' do - Page::Main::Entry.act { visit_login_page } + Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } Scenario::Gitlab::Project::Create.perform do |scenario| @@ -10,21 +10,10 @@ module QA scenario.description = 'project with repository' end - Git::Repository.perform do |repository| - repository.location = Page::Project::Show.act do - choose_repository_clone_http - repository_location - end - - repository.use_default_credentials - - repository.act do - clone - configure_identity('GitLab QA', 'root@gitlab.com') - add_file('README.md', '# This is test project') - commit('Add README.md') - push_changes - end + Scenario::Gitlab::Repository::Push.perform do |scenario| + scenario.file_name = 'README.md' + scenario.file_content = '# This is test project' + scenario.commit_message = 'Add README.md' end Page::Project::Show.act do diff --git a/qa/qa/specs/runner.rb b/qa/qa/specs/runner.rb index f98b8f88e9a..3f7b75df986 100644 --- a/qa/qa/specs/runner.rb +++ b/qa/qa/specs/runner.rb @@ -17,7 +17,7 @@ module QA tags.to_a.each { |tag| args.push(['-t', tag.to_s]) } args.push(files) - Specs::Config.perform + Runtime::Browser.configure! RSpec::Core::Runner.run(args.flatten, $stderr, $stdout).tap do |status| abort if status.nonzero? |