Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support compound stream matchers #1460

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
36 changes: 32 additions & 4 deletions lib/rspec/matchers/built_in/output.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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

Expand All @@ -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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there not a way to assert against one of our internal classes here? checking to see if we're already in a splitter etc

Copy link
Contributor Author

@nevinera nevinera Aug 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only in some of the cases. We're interacting with whatever's in $stderr and $stdout, so I think we're reliably getting an internal class for the former but not the latter. Wrapping with an internal class for the latter as well did occur to me as a potential solution, but it was large and involved enough that I wasn't willing to attempt it for such a minor issue :-\ (also I'm scared)


# 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
Expand Down
6 changes: 6 additions & 0 deletions spec/rspec/matchers/built_in/output_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down