summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
authorDavid Rodríguez <deivid.rodriguez@riseup.net>2024-05-23 21:55:04 +0200
committergit <svn-admin@ruby-lang.org>2024-05-31 16:49:44 +0000
commit082472451dcea0b28fd7a6bdeef227fda0425751 (patch)
tree1154bddf0515fa77ec12f165253320f497b03cbd /spec
parentcc8b9855e1849dadf0590e646872ff1bed5101f9 (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.rb3
-rw-r--r--spec/bundler/support/command_execution.rb44
-rw-r--r--spec/bundler/support/helpers.rb55
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?