diff options
| author | David RodrÃguez <deivid.rodriguez@riseup.net> | 2024-05-23 21:55:04 +0200 |
|---|---|---|
| committer | git <svn-admin@ruby-lang.org> | 2024-05-31 16:49:44 +0000 |
| commit | 082472451dcea0b28fd7a6bdeef227fda0425751 (patch) | |
| tree | 1154bddf0515fa77ec12f165253320f497b03cbd /spec | |
| parent | cc8b9855e1849dadf0590e646872ff1bed5101f9 (diff) | |
[rubygems/rubygems] Raise a friendly error whenever commands run in subshell take more than a minute
I expect to make occasional CI hangs easier to investigate.
Implementation was adapted from tty-command.
https://github.com/rubygems/rubygems/commit/39c92955bf
Diffstat (limited to 'spec')
| -rw-r--r-- | spec/bundler/commands/update_spec.rb | 3 | ||||
| -rw-r--r-- | spec/bundler/support/command_execution.rb | 44 | ||||
| -rw-r--r-- | spec/bundler/support/helpers.rb | 55 |
3 files changed, 85 insertions, 17 deletions
diff --git a/spec/bundler/commands/update_spec.rb b/spec/bundler/commands/update_spec.rb index 8565e27ebf..d5d38b81f1 100644 --- a/spec/bundler/commands/update_spec.rb +++ b/spec/bundler/commands/update_spec.rb @@ -849,8 +849,7 @@ RSpec.describe "bundle update" do end bundle "update", all: true - out.sub!("Removing foo (1.0)\n", "") - expect(out).to match(/Resolving dependencies\.\.\.\.*\nFetching foo 2\.0 \(was 1\.0\)\nInstalling foo 2\.0 \(was 1\.0\)\nBundle updated/) + expect(out.sub("Removing foo (1.0)\n", "")).to match(/Resolving dependencies\.\.\.\.*\nFetching foo 2\.0 \(was 1\.0\)\nInstalling foo 2\.0 \(was 1\.0\)\nBundle updated/) end it "shows error message when Gemfile.lock is not preset and gem is specified" do diff --git a/spec/bundler/support/command_execution.rb b/spec/bundler/support/command_execution.rb index 23a8e06041..02726744d3 100644 --- a/spec/bundler/support/command_execution.rb +++ b/spec/bundler/support/command_execution.rb @@ -1,7 +1,37 @@ # frozen_string_literal: true module Spec - CommandExecution = Struct.new(:command, :working_directory, :exitstatus, :original_stdout, :original_stderr) do + class CommandExecution + def initialize(command, working_directory:, timeout:) + @command = command + @working_directory = working_directory + @timeout = timeout + @original_stdout = String.new + @original_stderr = String.new + end + + attr_accessor :exitstatus, :command, :original_stdout, :original_stderr + attr_reader :timeout + attr_writer :failure_reason + + def raise_error! + return unless failure? + + error_header = if failure_reason == :timeout + "Invoking `#{command}` was aborted after #{timeout} seconds with output:" + else + "Invoking `#{command}` failed with output:" + end + + raise <<~ERROR + #{error_header} + + ---------------------------------------------------------------------- + #{stdboth} + ---------------------------------------------------------------------- + ERROR + end + def to_s "$ #{command}" end @@ -12,11 +42,11 @@ module Spec end def stdout - original_stdout + normalize(original_stdout) end def stderr - original_stderr + normalize(original_stderr) end def to_s_verbose @@ -37,5 +67,13 @@ module Spec return true unless exitstatus exitstatus > 0 end + + private + + attr_reader :failure_reason + + def normalize(string) + string.force_encoding(Encoding::UTF_8).strip.gsub("\r\n", "\n") + end end end diff --git a/spec/bundler/support/helpers.rb b/spec/bundler/support/helpers.rb index dbb8b8152b..049148fab0 100644 --- a/spec/bundler/support/helpers.rb +++ b/spec/bundler/support/helpers.rb @@ -8,6 +8,8 @@ module Spec module Helpers include Spec::Path + class TimeoutExceeded < StandardError; end + def reset! Dir.glob("#{tmp}/{gems/*,*}", File::FNM_DOTMATCH).each do |dir| next if %w[base base_system remote1 rubocop standard gems rubygems . ..].include?(File.basename(dir)) @@ -183,7 +185,7 @@ module Spec env = options[:env] || {} env["RUBYOPT"] = opt_add(opt_add("-r#{spec_dir}/support/switch_rubygems.rb", env["RUBYOPT"]), ENV["RUBYOPT"]) dir = options[:dir] || bundled_app - command_execution = CommandExecution.new(cmd.to_s, dir) + command_execution = CommandExecution.new(cmd.to_s, working_directory: dir, timeout: 60) require "open3" require "shellwords" @@ -191,10 +193,14 @@ module Spec yield stdin, stdout, wait_thr if block_given? stdin.close - stdout_read_thread = Thread.new { stdout.read } - stderr_read_thread = Thread.new { stderr.read } - command_execution.original_stdout = stdout_read_thread.value.strip - command_execution.original_stderr = stderr_read_thread.value.strip + stdout_handler = ->(data) { command_execution.original_stdout << data } + stderr_handler = ->(data) { command_execution.original_stderr << data } + + stdout_thread = read_stream(stdout, stdout_handler, timeout: command_execution.timeout) + stderr_thread = read_stream(stderr, stderr_handler, timeout: command_execution.timeout) + + stdout_thread.join + stderr_thread.join status = wait_thr.value command_execution.exitstatus = if status.exited? @@ -202,16 +208,13 @@ module Spec elsif status.signaled? exit_status_for_signal(status.termsig) end + rescue TimeoutExceeded + command_execution.failure_reason = :timeout + command_execution.exitstatus = exit_status_for_signal(Signal.list["INT"]) end unless options[:raise_on_error] == false || command_execution.success? - raise <<~ERROR - - Invoking `#{cmd}` failed with output: - ---------------------------------------------------------------------- - #{command_execution.stdboth} - ---------------------------------------------------------------------- - ERROR + command_execution.raise_error! end command_executions << command_execution @@ -219,6 +222,34 @@ module Spec command_execution.stdout end + # Mostly copied from https://github.com/piotrmurach/tty-command/blob/49c37a895ccea107e8b78d20e4cb29de6a1a53c8/lib/tty/command/process_runner.rb#L165-L193 + def read_stream(stream, handler, timeout:) + Thread.new do + Thread.current.report_on_exception = false + cmd_start = Time.now + readers = [stream] + + while readers.any? + ready = IO.select(readers, nil, readers, timeout) + raise TimeoutExceeded if ready.nil? + + ready[0].each do |reader| + chunk = reader.readpartial(16 * 1024) + handler.call(chunk) + + # control total time spent reading + runtime = Time.now - cmd_start + time_left = timeout - runtime + raise TimeoutExceeded if time_left < 0.0 + rescue Errno::EAGAIN, Errno::EINTR + rescue EOFError, Errno::EPIPE, Errno::EIO + readers.delete(reader) + reader.close + end + end + end + end + def all_commands_output return "" if command_executions.empty? |
