-
-
Notifications
You must be signed in to change notification settings - Fork 395
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
New matcher idea: match_pattern
for Ruby's pattern-matching
#1436
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
Feature: `match_pattern` matcher | ||
|
||
Use the `match_pattern` matcher to specify that a value matches with | ||
expected patterns by Ruby's pattern-matching. | ||
|
||
```ruby | ||
expect([1, 2, 3]).to match_pattern([Integer, Integer, Integer]) | ||
expect([1, 2, 3]).to match_pattern([Integer, Integer, String]) | ||
``` | ||
|
||
Scenario: Pattern usage | ||
Given a file named "example_spec.rb" with: | ||
"""ruby | ||
RSpec.describe [1, 2, 3] do | ||
it { is_expected.to match_pattern([Integer, Integer, Integer]) } | ||
it { is_expected.not_to match_pattern([Integer, Integer, String]) } | ||
|
||
# deliberate failures | ||
it { is_expected.to match_pattern([Integer, Integer, String]) } | ||
it { is_expected.not_to match_pattern([Integer, Integer, Integer]) } | ||
end | ||
""" | ||
When I run `rspec example_spec.rb` | ||
Then the output should contain all of these: | ||
| 4 examples, 2 failures | | ||
| expected [1, 2, 3] to match pattern [Integer, Integer, String] | | ||
| expected [1, 2, 3] not to match pattern [Integer, Integer, Integer] | |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
module RSpec | ||
module Matchers | ||
module BuiltIn | ||
# @api private | ||
# Provides the implementation for `match_pattern`. | ||
# Not intended to be instantiated directly. | ||
class MatchPattern < BaseMatcher | ||
def match(expected, actual) # rubocop:disable Lint/UnusedMethodArgument | ||
raise_not_supported_ruby_version_error unless supported_ruby_version? | ||
|
||
begin | ||
instance_eval(<<~RUBY, __FILE__, __LINE__ + 1) | ||
actual in #{expected.inspect} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If theres a way to avoid using eval that would be ideal... |
||
RUBY | ||
rescue SyntaxError | ||
raise_invalid_pattern_error | ||
end | ||
end | ||
|
||
def failure_message | ||
"expected #{description_of(@actual)} to match pattern #{@expected.inspect}" | ||
end | ||
|
||
def failure_message_when_negated | ||
"expected #{description_of(@actual)} not to match pattern #{@expected.inspect}" | ||
end | ||
|
||
private | ||
|
||
def raise_invalid_pattern_error | ||
raise SyntaxError, "The #{matcher_name} matcher requires that " \ | ||
"the expected object can be used as Ruby's " \ | ||
"pattern-matching but a `SyntaxError` was raised instead." | ||
end | ||
|
||
def raise_not_supported_ruby_version_error | ||
raise NotImplementedError, "The #{matcher_name} matcher is only " \ | ||
"supported on Ruby 3 or higher." | ||
end | ||
|
||
def supported_ruby_version? | ||
RUBY_VERSION.to_f >= 3.0 | ||
end | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We would typically conditionally define the entire matcher on supported rubies, and then have a dummy implementated that raises the not implemented error. |
||
end | ||
end | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
RSpec.describe 'match_pattern matcher' do | ||
it_behaves_like 'an RSpec value matcher', :valid_value => [1, 2, 3], | ||
:invalid_value => [1, 2, '3'] do | ||
let(:matcher) { match_pattern([Integer, Integer, Integer]) } | ||
end | ||
|
||
context 'when expected pattern matches with actual' do | ||
it 'passes' do | ||
expect([1, 2, 3]).to match_pattern([Integer, Integer, Integer]) | ||
end | ||
end | ||
|
||
context 'when expected pattern does not match with actual' do | ||
it 'fails' do | ||
expect { | ||
expect([1, 2, 3]).to match_pattern([Integer, Integer, String]) | ||
}.to fail_with('expected [1, 2, 3] to match pattern [Integer, Integer, String]') | ||
end | ||
end | ||
|
||
context 'when expected pattern cannot be used as pattern-matching' do | ||
it 'raises SyntaxError' do | ||
expect { | ||
expect([1, 2, 3]).to match_pattern(Object.new) | ||
}.to raise_error(SyntaxError, "The match_pattern matcher requires that the expected object can be used as Ruby's pattern-matching but a `SyntaxError` was raised instead.") | ||
end | ||
end | ||
|
||
context 'when pattern-matching is not supported on the current Ruby version' do | ||
before do | ||
stub_const('RUBY_VERSION', '2.7.0') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Considering we run builds on multiple rubies, you don't need to do this, you can add a flag to |
||
end | ||
|
||
it 'raises NotImplementedError' do | ||
expect { | ||
expect([1, 2, 3]).to match_pattern([Integer, Integer, Integer]) | ||
}.to raise_error(NotImplementedError, 'The match_pattern matcher is only supported on Ruby 3 or higher.') | ||
end | ||
end | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This would also have to be tagged and excluded on unsupported ruby, and as it is used in our docs we would need a note about supported ruby versions in that,