From 1a5f76c73aae843b988f65bdfee7339f2f9873da Mon Sep 17 00:00:00 2001 From: Alex Genco Date: Tue, 23 Dec 2014 15:09:11 -0800 Subject: [PATCH] Add `to_std(out|err)_from_any_process` matchers These matchers will capture all output from spawned subprocesses in addition to the main Ruby process. --- lib/rspec/matchers/built_in/output.rb | 41 +++++++++++ spec/rspec/matchers/built_in/output_spec.rb | 77 +++++++++++++-------- 2 files changed, 88 insertions(+), 30 deletions(-) diff --git a/lib/rspec/matchers/built_in/output.rb b/lib/rspec/matchers/built_in/output.rb index 8bc78813f..b4b168d7c 100644 --- a/lib/rspec/matchers/built_in/output.rb +++ b/lib/rspec/matchers/built_in/output.rb @@ -1,4 +1,5 @@ require 'stringio' +require 'tempfile' module RSpec module Matchers @@ -27,6 +28,7 @@ def does_not_match?(block) # @api public # Tells the matcher to match against stdout. + # Works only when the main Ruby process prints to stdout def to_stdout @stream_capturer = CaptureStdout self @@ -34,11 +36,30 @@ def to_stdout # @api public # Tells the matcher to match against stderr. + # Works only when the main Ruby process prints to stderr def to_stderr @stream_capturer = CaptureStderr self end + # @api public + # Tells the matcher to match against stdout. + # Works when subprocesses print to stdout as well. + # This is significantly (~30x) slower than `to_stdout` + def to_stdout_from_any_process + @stream_capturer = CaptureStreamToTempfile.new("stdout", $stdout) + self + end + + # @api public + # Tells the matcher to match against stderr. + # Works when subprocesses print to stderr as well. + # This is significantly (~30x) slower than `to_stderr` + def to_stderr_from_any_process + @stream_capturer = CaptureStreamToTempfile.new("stderr", $stderr) + self + end + # @api private # @return [String] def failure_message @@ -147,6 +168,26 @@ def self.capture(block) $stderr = original_stream end end + + # @private + class CaptureStreamToTempfile < Struct.new(:name, :stream) + def capture(block) + original_stream = stream.clone + captured_stream = Tempfile.new(name) + + begin + captured_stream.sync = true + stream.reopen(captured_stream) + block.call + captured_stream.rewind + captured_stream.read + ensure + stream.reopen(original_stream) + captured_stream.close + captured_stream.unlink + end + end + end end end end diff --git a/spec/rspec/matchers/built_in/output_spec.rb b/spec/rspec/matchers/built_in/output_spec.rb index 2b827ada4..48814ac79 100644 --- a/spec/rspec/matchers/built_in/output_spec.rb +++ b/spec/rspec/matchers/built_in/output_spec.rb @@ -1,5 +1,10 @@ -RSpec.shared_examples "output_to_stream" do |stream_name| - matcher_method = :"to_#{stream_name}" +RSpec.shared_examples "output_to_stream" do |stream_name, matcher_method, helper_module| + include helper_module + extend helper_module + + it_behaves_like("an RSpec matcher", :valid_value => lambda { print_to_stream('foo') }, :invalid_value => lambda {}) do + let(:matcher) { output.send(matcher_method) } + end define_method :matcher do |*args| output(args.first).send(matcher_method) @@ -16,7 +21,7 @@ context "expect { ... }.to output.#{matcher_method}" do it "passes if the block outputs to #{stream_name}" do - expect { stream.print 'foo' }.to matcher + expect { print_to_stream 'foo' }.to matcher end it "fails if the block does not output to #{stream_name}" do @@ -33,14 +38,14 @@ it "fails if the block outputs to #{stream_name}" do expect { - expect { stream.print 'foo' }.not_to matcher + expect { print_to_stream 'foo' }.not_to matcher }.to fail_with("expected block to not output to #{stream_name}, but output \"foo\"") end end context "expect { ... }.to output('string').#{matcher_method}" do it "passes if the block outputs that string to #{stream_name}" do - expect { stream.print 'foo' }.to matcher("foo") + expect { print_to_stream 'foo' }.to matcher("foo") end it "fails if the block does not output to #{stream_name}" do @@ -51,14 +56,14 @@ it "fails if the block outputs a different string to #{stream_name}" do expect { - expect { stream.print 'food' }.to matcher('foo') + expect { print_to_stream 'food' }.to matcher('foo') }.to fail_with("expected block to output \"foo\" to #{stream_name}, but output \"food\"") end end context "expect { ... }.to_not output('string').#{matcher_method}" do it "passes if the block outputs a different string to #{stream_name}" do - expect { stream.print 'food' }.to_not matcher('foo') + expect { print_to_stream 'food' }.to_not matcher('foo') end it "passes if the block does not output to #{stream_name}" do @@ -67,14 +72,14 @@ it "fails if the block outputs the same string to #{stream_name}" do expect { - expect { stream.print 'foo' }.to_not matcher('foo') + expect { print_to_stream 'foo' }.to_not matcher('foo') }.to fail_with("expected block to not output \"foo\" to #{stream_name}, but output \"foo\"") end end context "expect { ... }.to output(/regex/).#{matcher_method}" do it "passes if the block outputs a string to #{stream_name} that matches the regex" do - expect { stream.print 'foo' }.to matcher(/foo/) + expect { print_to_stream 'foo' }.to matcher(/foo/) end it "fails if the block does not output to #{stream_name}" do @@ -85,14 +90,14 @@ it "fails if the block outputs a string to #{stream_name} that does not match" do expect { - expect { stream.print 'foo' }.to matcher(/food/) + expect { print_to_stream 'foo' }.to matcher(/food/) }.to fail_matching("expected block to output /food/ to #{stream_name}, but output \"foo\"\nDiff") end end context "expect { ... }.to_not output(/regex/).#{matcher_method}" do it "passes if the block outputs a string to #{stream_name} that does not match the regex" do - expect { stream.print 'food' }.to_not matcher(/bar/) + expect { print_to_stream 'food' }.to_not matcher(/bar/) end it "passes if the block does not output to #{stream_name}" do @@ -101,31 +106,31 @@ it "fails if the block outputs a string to #{stream_name} that matches the regex" do expect { - expect { stream.print 'foo' }.to_not matcher(/foo/) + expect { print_to_stream 'foo' }.to_not matcher(/foo/) }.to fail_matching("expected block to not output /foo/ to #{stream_name}, but output \"foo\"\nDiff") end end context "expect { ... }.to output(matcher).#{matcher_method}" do it "passes if the block outputs a string to #{stream_name} that passes the given matcher" do - expect { stream.print 'foo' }.to matcher(a_string_starting_with("f")) + expect { print_to_stream 'foo' }.to matcher(a_string_starting_with("f")) end it "fails if the block outputs a string to #{stream_name} that does not pass the given matcher" do expect { - expect { stream.print 'foo' }.to matcher(a_string_starting_with("b")) + expect { print_to_stream 'foo' }.to matcher(a_string_starting_with("b")) }.to fail_matching("expected block to output a string starting with \"b\" to #{stream_name}, but output \"foo\"\nDiff") end end context "expect { ... }.to_not output(matcher).#{matcher_method}" do it "passes if the block does not output a string to #{stream_name} that passes the given matcher" do - expect { stream.print 'foo' }.to_not matcher(a_string_starting_with("b")) + expect { print_to_stream 'foo' }.to_not matcher(a_string_starting_with("b")) end it "fails if the block outputs a string to #{stream_name} that passes the given matcher" do expect { - expect { stream.print 'foo' }.to_not matcher(a_string_starting_with("f")) + expect { print_to_stream 'foo' }.to_not matcher(a_string_starting_with("f")) }.to fail_matching("expected block to not output a string starting with \"f\" to #{stream_name}, but output \"foo\"\nDiff") end end @@ -134,29 +139,41 @@ module RSpec module Matchers RSpec.describe "output.to_stderr matcher" do - it_behaves_like("an RSpec matcher", :valid_value => lambda { warn('foo') }, :invalid_value => lambda {}) do - let(:matcher) { output.to_stderr } - end - - include_examples "output_to_stream", :stderr do - let(:stream) { $stderr } - end + include_examples "output_to_stream", :stderr, :to_stderr, Module.new { + def print_to_stream(msg) + $stderr.print(msg) + end + } end RSpec.describe "output.to_stdout matcher" do - it_behaves_like("an RSpec matcher", :valid_value => lambda { print 'foo' }, :invalid_value => lambda {}) do - let(:matcher) { output.to_stdout } - end + include_examples "output_to_stream", :stdout, :to_stdout, Module.new { + def print_to_stream(msg) + print(msg) + end + } + end - include_examples "output_to_stream", :stdout do - let(:stream) { $stdout } - end + RSpec.describe "output.to_stderr_from_any_process matcher" do + include_examples "output_to_stream", :stderr, :to_stderr_from_any_process, Module.new { + def print_to_stream(msg) + system("printf #{msg} 1>&2") + end + } + end + + RSpec.describe "output.to_stdout_from_any_process matcher" do + include_examples "output_to_stream", :stdout, :to_stdout_from_any_process, Module.new { + def print_to_stream(msg) + system("printf #{msg}") + end + } end RSpec.describe "output (without `to_stdout` or `to_stderr`)" do it 'raises an error explaining the use is invalid' do expect { - expect { stream.print 'foo' }.to output + expect { print 'foo' }.to output }.to raise_error(/must chain.*to_stdout.*to_stderr/) end