From b07ee624e4806af7372423d47ea0318ff0ece2cb Mon Sep 17 00:00:00 2001 From: Eric Mueller Date: Sun, 19 May 2024 00:44:30 -0400 Subject: [PATCH] Nearly support compound matchers on CaptureStreamToTempfile Sadly, it doesn't quite work on StdErrSplitter, because of the existing (but possibly non-impactful?) bug around restoring the original stream during the ensure block. --- lib/rspec/matchers/built_in/output.rb | 39 ++++++++++++++++++++++----- 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/lib/rspec/matchers/built_in/output.rb b/lib/rspec/matchers/built_in/output.rb index ae588a6d3..d173b1005 100644 --- a/lib/rspec/matchers/built_in/output.rb +++ b/lib/rspec/matchers/built_in/output.rb @@ -152,7 +152,7 @@ def self.capture(block) captured_stream.string ensure $stdout = original_stream - $stdout.write(captured_stream.string) unless $stdout == STDOUT + $stdout.write(captured_stream.string) unless $stdout == STDOUT # rubocop:disable Style/GlobalStdStream end end @@ -173,7 +173,7 @@ def self.capture(block) captured_stream.string ensure $stderr = original_stream - $stderr.write(captured_stream.string) unless $stderr == STDERR + $stderr.write(captured_stream.string) unless $stderr == STDERR # rubocop:disable Style/GlobalStdStream end end @@ -188,21 +188,48 @@ def capture(block) # thread, fileutils, etc), so it's worth delaying it until this point. require 'tempfile' + # This is.. awkward-looking. But it's written this way because of how + # compound matchers work - we essentially need to be able to tell if + # we're in an _inner_ matcher, so we can pass the stream-output along + # to the outer matcher for further evaluation in that case. Added to + # that, it's fairly difficult to _tell_, because the only actual state + # we have access to is the stream itself, and in the case of stderr, + # that stream is really a RSpec::Support::StdErrSplitter (which is why + # we're testing `is_a?(File)` in such an obnoxious way). + inner_matcher = stream.to_io.is_a?(File) + + # FIXME: stream.clone isn't sufficient right now - the StdErrSplitter + # clones with the same stream, and reopening the stream on either of + # them effectively updates both. Which means that the ensure-reopen + # further down doesn't really do anything currently. 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 + read_contents(captured_stream) ensure + captured_content = inner_matcher ? read_contents(captured_stream) : nil stream.reopen(original_stream) - captured_stream.close - captured_stream.unlink + stream.write(captured_content) if captured_content + clean_up_tempfile(captured_stream) end end + + private + + def read_contents(strm) + strm.rewind + strm.read + end + + def clean_up_tempfile(tempfile) + tempfile.close + tempfile.unlink + end end end end