diff --git a/Changelog.md b/Changelog.md index 6196f978c..9258b0d9c 100644 --- a/Changelog.md +++ b/Changelog.md @@ -12,6 +12,7 @@ Enhancements: * Improve the IO emulation in the output capture matchers (`output(...).to_stdout` et al) by adding `as_tty` and `as_not_tty` to change the `tty?` flags. (Sergio Gil PĂ©rez de la Manga, #1459) +* Add support for compound output matchers. (Eric Mueller, #1460) ### 3.13.1 / 2024-06-13 [Full Changelog](http://github.com/rspec/rspec-expectations/compare/v3.13.0...v3.13.1) diff --git a/lib/rspec/matchers/built_in/output.rb b/lib/rspec/matchers/built_in/output.rb index 888ccaee2..503ebaa1f 100644 --- a/lib/rspec/matchers/built_in/output.rb +++ b/lib/rspec/matchers/built_in/output.rb @@ -186,6 +186,7 @@ def capture(block) captured_stream.string ensure $stdout = original_stream + $stdout.write(captured_stream.string) unless $stdout == STDOUT # rubocop:disable Style/GlobalStdStream end end @@ -209,6 +210,7 @@ def capture(block) captured_stream.string ensure $stderr = original_stream + $stderr.write(captured_stream.string) unless $stderr == STDERR # rubocop:disable Style/GlobalStdStream end end @@ -223,21 +225,47 @@ 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) + + # Careful here - the StdErrSplitter is what is being cloned; we're + # relying on the implemented clone method of that class (in + # rspec-support) to actually clone the File for ensure-reopen. 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(captured_stream) + captured_stream.rewind + captured_stream.read + end + + def clean_up_tempfile(tempfile) + tempfile.close + tempfile.unlink + 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 3873013d1..68a1b1672 100644 --- a/spec/rspec/matchers/built_in/output_spec.rb +++ b/spec/rspec/matchers/built_in/output_spec.rb @@ -139,6 +139,12 @@ def invalid_block }.to fail_including("expected block to not output a string starting with \"f\" to #{stream_name}, but output \"foo\"\nDiff") end end + + context "expect { ... }.to output(matcher1).#{matcher_method}.and output(matcher2).#{matcher_method}" do + it "passes if the block outputs lines to #{stream_name} matching both matchers", :pending => RSpec::Support::Ruby.jruby? && matcher_method =~ /any_process/ do + expect { print_to_stream "foo_bar" }.to matcher(/foo/).and matcher(/bar/) + end + end end module RSpec