diff --git a/lib/cc/engine/analyzers/command_line_runner.rb b/lib/cc/engine/analyzers/command_line_runner.rb index 66e09aae..59d0a698 100644 --- a/lib/cc/engine/analyzers/command_line_runner.rb +++ b/lib/cc/engine/analyzers/command_line_runner.rb @@ -15,6 +15,9 @@ def initialize(command, timeout = DEFAULT_TIMEOUT) def run(input) Timeout.timeout(timeout) do out, err, status = Open3.capture3(command, stdin_data: input) + + status ||= handle_open3_race_condition(out) + if status.success? yield out else @@ -26,6 +29,25 @@ def run(input) private attr_reader :command, :timeout + + # Work around a race condition in JRuby's Open3.capture3 that can lead + # to a nil status returned. We'll consider the process successful if it + # produced output that can be parsed as JSON. + # + # https://github.com/jruby/jruby/blob/master/lib/ruby/stdlib/open3.rb#L200-L201 + # + def handle_open3_race_condition(out) + JSON.parse(out) + NullStatus.new(true, 0) + rescue JSON::ParserError + NullStatus.new(false, 1) + end + + NullStatus = Struct.new(:success, :exitstatus) do + def success? + success + end + end end end end diff --git a/spec/cc/engine/analyzers/command_line_runner_spec.rb b/spec/cc/engine/analyzers/command_line_runner_spec.rb new file mode 100644 index 00000000..5de8e5b1 --- /dev/null +++ b/spec/cc/engine/analyzers/command_line_runner_spec.rb @@ -0,0 +1,52 @@ +require "spec_helper" +require "cc/engine/duplication" + +module CC::Engine::Analyzers + RSpec.describe CommandLineRunner do + describe "#run" do + it "runs the command on the input and yields the output" do + runner = CommandLineRunner.new("cat; echo hi") + + output = runner.run("oh ") { |o| o } + + expect(output).to eq "oh hi\n" + end + + + it "raises on errors" do + runner = CommandLineRunner.new("echo error output >&2; false") + + expect { runner.run("") }.to raise_error( + ParserError, /code 1:\nerror output/ + ) + end + + it "times out commands" do + runner = CommandLineRunner.new("sleep 3", 0.01) + + expect { runner.run("") }.to raise_error(Timeout::Error) + end + + context "when Open3 returns a nil status" do + it "accepts it if the output parses as JSON" do + runner = CommandLineRunner.new("") + + allow(Open3).to receive(:capture3).and_return(["{\"type\":\"issue\"}", "", nil]) + + output = runner.run("") { |o| o } + expect(output).to eq "{\"type\":\"issue\"}" + end + + it "raises if the output was not valid JSON" do + runner = CommandLineRunner.new("") + + allow(Open3).to receive(:capture3).and_return(["", "error output", nil]) + + expect { runner.run("") }.to raise_error( + ParserError, /code 1:\nerror output/ + ) + end + end + end + end +end