require 'stringio' module RSpec module Matchers module BuiltIn # @api private # Provides the implementation for `output`. # Not intended to be instantiated directly. class Output < BaseMatcher def initialize(expected) @expected = expected @actual = "" @block = nil @stream_capturer = NullCapture end def matches?(block) @block = block return false unless Proc === block @actual = @stream_capturer.capture(block) @expected ? values_match?(@expected, @actual) : captured? end def does_not_match?(block) !matches?(block) && Proc === block end # @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 "expected block to #{description}, but #{positive_failure_reason}" end # @api private # @return [String] def failure_message_when_negated "expected block to not #{description}, but #{negative_failure_reason}" end # @api private # @return [String] def description if @expected "output #{description_of @expected} to #{@stream_capturer.name}" else "output to #{@stream_capturer.name}" end end # @api private # @return [Boolean] def diffable? true end # @api private # Indicates this matcher matches against a block. # @return [True] def supports_block_expectations? true end # @api private # Indicates this matcher matches against a block only. # @return [False] def supports_value_expectations? false end private def captured? @actual.length > 0 end def positive_failure_reason return "was not a block" unless Proc === @block return "output #{actual_output_description}" if @expected "did not" end def negative_failure_reason return "was not a block" unless Proc === @block "output #{actual_output_description}" end def actual_output_description return "nothing" unless captured? actual_formatted end end # @private module NullCapture def self.name "some stream" end def self.capture(_block) raise "You must chain `to_stdout` or `to_stderr` off of the `output(...)` matcher." end end # @private module CaptureStdout def self.name 'stdout' end def self.capture(block) captured_stream = StringIO.new original_stream = $stdout $stdout = captured_stream block.call captured_stream.string ensure $stdout = original_stream end end # @private module CaptureStderr def self.name 'stderr' end def self.capture(block) captured_stream = StringIO.new original_stream = $stderr $stderr = captured_stream block.call captured_stream.string ensure $stderr = original_stream end end # @private class CaptureStreamToTempfile < Struct.new(:name, :stream) def capture(block) # We delay loading tempfile until it is actually needed because # we want to minimize stdlibs loaded so that users who use a # portion of the stdlib can't have passing specs while forgetting # to load it themselves. `CaptureStreamToTempfile` is rarely used # and `tempfile` pulls in a bunch of things (delegate, tmpdir, # thread, fileutils, etc), so it's worth delaying it until this point. require 'tempfile' 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