Skip to content

Commit

Permalink
! Added assert_pattern & refute_pattern for pattern matching. (flavor…
Browse files Browse the repository at this point in the history
…jones)

! Added matching must_pattern_match & wont_pattern_match to minitest/spec.

[git-p4: depot-paths = "//src/minitest/dev/": change = 13686]
  • Loading branch information
zenspider committed Feb 8, 2023
1 parent 899b420 commit 0c44f4e
Show file tree
Hide file tree
Showing 4 changed files with 219 additions and 0 deletions.
50 changes: 50 additions & 0 deletions lib/minitest/assertions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,32 @@ def assert_path_exists path, msg = nil
assert File.exist?(path), msg
end

##
# For testing with pattern matching (only supported with Ruby 3.0 and later)
#
# # pass
# assert_pattern { [1,2,3] => [Integer, Integer, Integer] }
#
# # fail "length mismatch (given 3, expected 1)"
# assert_pattern { [1,2,3] => [Integer] }
#
# The bare <tt>=></tt> pattern will raise a NoMatchingPatternError on failure, which would
# normally be counted as a test error. This assertion rescues NoMatchingPatternError and
# generates a test failure. Any other exception will be raised as normal and generate a test
# error.

def assert_pattern
raise NotImplementedError, "only available in Ruby 3.0+" unless RUBY_VERSION >= "3.0"
flunk "assert_pattern requires a block to capture errors." unless block_given?

begin # TODO: remove after ruby 2.6 dropped
yield
pass
rescue NoMatchingPatternError => e
flunk e.message
end
end

##
# For testing with predicates. Eg:
#
Expand Down Expand Up @@ -721,6 +747,30 @@ def refute_nil obj, msg = nil
refute obj.nil?, msg
end

##
# For testing with pattern matching (only supported with Ruby 3.0 and later)
#
# # pass
# refute_pattern { [1,2,3] => [String] }
#
# # fail "NoMatchingPatternError expected, but nothing was raised."
# refute_pattern { [1,2,3] => [Integer, Integer, Integer] }
#
# This assertion expects a NoMatchingPatternError exception, and will fail if none is raised. Any
# other exceptions will be raised as normal and generate a test error.

def refute_pattern
raise NotImplementedError, "only available in Ruby 3.0+" unless RUBY_VERSION >= "3.0"
flunk "refute_pattern requires a block to capture errors." unless block_given?

begin
yield
flunk("NoMatchingPatternError expected, but nothing was raised.")
rescue NoMatchingPatternError
pass
end
end

##
# Fails if +o1+ is not +op+ +o2+. Eg:
#
Expand Down
18 changes: 18 additions & 0 deletions lib/minitest/expectations.rb
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,15 @@ module Minitest::Expectations

infect_an_assertion :assert_output, :must_output, :block

##
# See Minitest::Assertions#assert_pattern_match
#
# _ { ... }.must_pattern_match [...]
#
# :method: must_pattern_match

infect_an_assertion :assert_pattern, :must_pattern_match, :block

##
# See Minitest::Assertions#assert_raises
#
Expand Down Expand Up @@ -283,6 +292,15 @@ module Minitest::Expectations

infect_an_assertion :refute_operator, :wont_be, :reverse

##
# See Minitest::Assertions#refute_pattern_match
#
# _ { ... }.wont_pattern_match [...]
#
# :method: wont_pattern_match

infect_an_assertion :refute_pattern, :wont_pattern_match, :block

##
# See Minitest::Assertions#refute_respond_to
#
Expand Down
110 changes: 110 additions & 0 deletions test/minitest/test_minitest_assertions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1062,6 +1062,66 @@ def test_assert_path_exists_triggered
end
end

def test_assert_pattern
if RUBY_VERSION > "3" then
@tc.assert_pattern do
exp = if RUBY_VERSION.start_with? "3.0"
"(eval):1: warning: One-line pattern matching is experimental, and the behavior may change in future versions of Ruby!\n"
else
""
end
assert_output nil, exp do
eval "[1,2,3] => [Integer, Integer, Integer]" # eval to escape parser for ruby<3
end
end
else
@assertion_count = 0

assert_raises NotImplementedError do
@tc.assert_pattern do
# do nothing
end
end
end
end

def test_assert_pattern_traps_nomatchingpatternerror
skip unless RUBY_VERSION > "3"
exp = if RUBY_VERSION.start_with? "3.0" then
"[1, 2, 3]" # terrible error message!
else
/length mismatch/
end

assert_triggered exp do
@tc.assert_pattern do
capture_io do # 3.0 is noisy
eval "[1,2,3] => [Integer, Integer]" # eval to escape parser for ruby<3
end
end
end
end

def test_assert_pattern_raises_other_exceptions
skip unless RUBY_VERSION >= "3.0"

@assertion_count = 0

assert_raises RuntimeError do
@tc.assert_pattern do
raise "boom"
end
end
end

def test_assert_pattern_with_no_block
skip unless RUBY_VERSION >= "3.0"

assert_triggered "assert_pattern requires a block to capture errors." do
@tc.assert_pattern
end
end

def test_capture_io
@assertion_count = 0

Expand Down Expand Up @@ -1314,6 +1374,56 @@ def test_refute_operator_triggered
end
end

def test_refute_pattern
if RUBY_VERSION >= "3.0"
@tc.refute_pattern do
capture_io do # 3.0 is noisy
eval "[1,2,3] => [Integer, Integer, String]"
end
end
else
@assertion_count = 0

assert_raises NotImplementedError do
@tc.refute_pattern do
eval "[1,2,3] => [Integer, Integer, String]"
end
end
end
end

def test_refute_pattern_expects_nomatchingpatternerror
skip unless RUBY_VERSION > "3"

assert_triggered(/NoMatchingPatternError expected, but nothing was raised./) do
@tc.refute_pattern do
capture_io do # 3.0 is noisy
eval "[1,2,3] => [Integer, Integer, Integer]"
end
end
end
end

def test_refute_pattern_raises_other_exceptions
skip unless RUBY_VERSION >= "3.0"

@assertion_count = 0

assert_raises RuntimeError do
@tc.refute_pattern do
raise "boom"
end
end
end

def test_refute_pattern_with_no_block
skip unless RUBY_VERSION >= "3.0"

assert_triggered "refute_pattern requires a block to capture errors." do
@tc.refute_pattern
end
end

def test_refute_predicate
@tc.refute_predicate "42", :empty?
end
Expand Down
41 changes: 41 additions & 0 deletions test/minitest/test_minitest_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,46 @@ def assert_success spec
end
end

def good_pattern
capture_io do # 3.0 is noisy
eval "[1,2,3] => [Integer, Integer, Integer]" # eval to escape parser for ruby<3
end
end

def bad_pattern
capture_io do # 3.0 is noisy
eval "[1,2,3] => [Integer, Integer]" # eval to escape parser for ruby<3
end
end

it "needs to pattern match" do
@assertion_count = 1

if RUBY_VERSION > "3" then
expect { good_pattern }.must_pattern_match
else
assert_raises NotImplementedError do
expect {}.must_pattern_match
end
end
end

it "needs to error on bad pattern match" do
skip unless RUBY_VERSION > "3"

@assertion_count = 1

exp = if RUBY_VERSION.start_with? "3.0"
"[1, 2, 3]" # terrible error message!
else
/length mismatch/
end

assert_triggered exp do
expect { bad_pattern }.must_pattern_match
end
end

it "needs to ensure silence" do
@assertion_count -= 1 # no msg
@assertion_count += 2 # assert_output is 2 assertions
Expand Down Expand Up @@ -172,6 +212,7 @@ def assert_success spec
must_include
must_match
must_output
must_pattern_match
must_raise
must_respond_to
must_throw
Expand Down

0 comments on commit 0c44f4e

Please sign in to comment.