diff --git a/qa/qa/git/repository.rb b/qa/qa/git/repository.rb index d718208e1a0..9de7539d6bc 100644 --- a/qa/qa/git/repository.rb +++ b/qa/qa/git/repository.rb @@ -126,12 +126,12 @@ module QA run_git("git rev-parse --abbrev-ref HEAD").to_s end - def push_changes(branch = @default_branch, push_options: nil, max_attempts: 3, raise_on_failure: true) + def push_changes(branch = @default_branch, push_options: nil, max_attempts: 3, raise_on_failure: true, timeout: 60) cmd = ['git push'] cmd << push_options_hash_to_string(push_options) cmd << uri cmd << branch - run_git(cmd.compact.join(' '), raise_on_failure: raise_on_failure, max_attempts: max_attempts).to_s + run_git(cmd.compact.join(' '), raise_on_failure: raise_on_failure, max_attempts: max_attempts, timeout: timeout).to_s end def push_all_branches @@ -335,14 +335,15 @@ module QA read_netrc_content.grep(/^#{Regexp.escape(netrc_content)}$/).any? end - def run_git(command_str, raise_on_failure: true, env: env_vars, max_attempts: 1) + def run_git(command_str, raise_on_failure: true, env: env_vars, max_attempts: 1, timeout: nil) run( command_str, raise_on_failure: raise_on_failure, env: env, max_attempts: max_attempts, sleep_internal: command_retry_sleep_interval, - log_prefix: 'Git: ' + log_prefix: 'Git: ', + timeout: timeout ) end end diff --git a/qa/qa/support/run.rb b/qa/qa/support/run.rb index 11792915710..024b40c1e1f 100644 --- a/qa/qa/support/run.rb +++ b/qa/qa/support/run.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require 'open3' +require 'timeout' module QA module Support @@ -8,6 +9,7 @@ module QA include QA::Support::Repeater CommandError = Class.new(StandardError) + CommandTimeoutError = Class.new(CommandError) Result = Struct.new(:command, :exitstatus, :response) do alias_method :to_s, :response @@ -21,26 +23,44 @@ module QA end end - def run(command_str, raise_on_failure: true, env: [], max_attempts: 1, sleep_internal: 0, log_prefix: '') + def run( + command_str, + raise_on_failure: true, + env: [], + max_attempts: 1, + sleep_internal: 0, + log_prefix: '', + timeout: nil + ) command = [*env, command_str, '2>&1'].compact.join(' ') result = nil - repeat_until(max_attempts: max_attempts, sleep_interval: sleep_internal, raise_on_failure: false) do + repeat_until( + max_attempts: max_attempts, + sleep_interval: sleep_internal, + raise_on_failure: false + ) do Runtime::Logger.debug "#{log_prefix}pwd=[#{Dir.pwd}], command=[#{command}]" - output, status = Open3.capture2e(command) - output.chomp! - Runtime::Logger.debug "#{log_prefix}output=[#{output}], exitstatus=[#{status.exitstatus}]" - result = Result.new(command, status.exitstatus, output) + begin + output, status = timeout ? Timeout.timeout(timeout) { Open3.capture2e(command) } : Open3.capture2e(command) + output.chomp! + Runtime::Logger.debug "#{log_prefix}output=[#{output}], exitstatus=[#{status.exitstatus}]" - result.success? + result = Result.new(command, status.exitstatus, output) + result.success? + rescue Timeout::Error + Runtime::Logger.debug "#{log_prefix}command timed out after #{timeout} seconds" + raise CommandTimeoutError, "The command #{command} timed out after #{timeout} seconds" + end end raise_error = raise_on_failure && !result.success? if raise_error raise CommandError, - "The command #{result.command} failed (#{result.exitstatus}) with the following output:\n#{result.response}" + "The command #{result.command} failed (#{result.exitstatus}) " \ + "with the following output:\n#{result.response}" end result diff --git a/qa/spec/git/repository_spec.rb b/qa/spec/git/repository_spec.rb index eeab458fd95..4b8ccaf697b 100644 --- a/qa/spec/git/repository_spec.rb +++ b/qa/spec/git/repository_spec.rb @@ -45,7 +45,7 @@ RSpec.describe QA::Git::Repository do context 'when command is successful' do it 'returns the #run command Result output' do - expect(repository).to receive(:run).with(command, run_params.merge(max_attempts: 3)).and_return(result) + expect(repository).to receive(:run).with(command, run_params.merge(max_attempts: 3, timeout: timeout)).and_return(result) expect(call_method).to eq(command_return) end @@ -81,7 +81,7 @@ RSpec.describe QA::Git::Repository do context 'when command is successful' do it 'returns the #run command Result output' do - expect(repository).to receive(:run).with(command, run_params.merge(max_attempts: 1)).and_return(result) + expect(repository).to receive(:run).with(command, run_params.merge(max_attempts: 1, timeout: timeout)).and_return(result) expect(call_method).to eq(command_return) end @@ -110,6 +110,7 @@ RSpec.describe QA::Git::Repository do let(:opts) { '' } let(:call_method) { repository.clone } let(:command) { "git clone #{opts} #{repo_uri_with_credentials} ./" } + let(:timeout) { nil } context 'when no opts is given' do it_behaves_like 'command with retries' @@ -128,6 +129,7 @@ RSpec.describe QA::Git::Repository do it_behaves_like 'command with retries' do let(:call_method) { repository.shallow_clone } let(:command) { "git clone --depth 1 #{repo_uri_with_credentials} ./" } + let(:timeout) { nil } end end @@ -136,6 +138,7 @@ RSpec.describe QA::Git::Repository do let(:tag_name) { 'v1.0' } let(:call_method) { repository.delete_tag(tag_name) } let(:command) { "git push origin --delete #{tag_name}" } + let(:timeout) { nil } end end @@ -143,6 +146,7 @@ RSpec.describe QA::Git::Repository do let(:branch) { QA::Runtime::Env.default_branch } let(:call_method) { repository.push_changes } let(:command) { "git push #{repo_uri_with_credentials} #{branch}" } + let(:timeout) { 60 } context 'when no branch is given' do it_behaves_like 'command with retries' @@ -234,7 +238,7 @@ RSpec.describe QA::Git::Repository do [0, 1, 2].each do |version| it "configures git to use protocol version #{version}" do expect(repository).to receive(:run).with("git config protocol.version #{version}", - run_params.merge(max_attempts: 1)) + run_params.merge(max_attempts: 1, timeout: nil)) repository.git_protocol = version end @@ -256,6 +260,7 @@ RSpec.describe QA::Git::Repository do let(:result_output) { +'packet: ls-remote< version 2' } let(:command_return) { '2' } let(:extra_env_vars) { ["GIT_TRACE_PACKET=1"] } + let(:timeout) { nil } end it "reports the detected version" do