Skip to content

Commit

Permalink
Add to_std(out|err)_from_any_process matchers
Browse files Browse the repository at this point in the history
These matchers will capture all output from spawned subprocesses in
addition to the main Ruby process.
  • Loading branch information
alexgenco committed Dec 23, 2014
1 parent 0778ffa commit 1a5f76c
Show file tree
Hide file tree
Showing 2 changed files with 88 additions and 30 deletions.
41 changes: 41 additions & 0 deletions lib/rspec/matchers/built_in/output.rb
@@ -1,4 +1,5 @@
require 'stringio'
require 'tempfile'

module RSpec
module Matchers
Expand Down Expand Up @@ -27,18 +28,38 @@ 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
end

# @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
Expand Down Expand Up @@ -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
77 changes: 47 additions & 30 deletions 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)
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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

Expand Down

0 comments on commit 1a5f76c

Please sign in to comment.