From 28a93885aebc8042002ddd8f3ad7178199949ef1 Mon Sep 17 00:00:00 2001 From: Phil Pirozhkov Date: Fri, 26 Jul 2019 19:27:41 +0300 Subject: [PATCH] Restrict block matchers use with value expectations https://blog.rubystyle.guide/rspec/2019/07/17/rspec-implicit-block-syntax.html https://rspec.rubystyle.guide/#implicit-block-expectations Spec changes are due to: matcher.matches?(invalid_value) doesn't work with block-only matchers, as no block is passed in, and it fails with a message: 1) RSpec::Matchers::BuiltIn::Change behaves like an RSpec block-only matcher uses the `ObjectFormatter` for `failure_message` Failure/Error: expect(message).to include("detailed inspect") expected "expected `@k` to have changed, but was not given a block" to include "detailed inspect" The redundant (due to existing check in ExpectationTarget) `Proc === @event_proc` checks could not be removed safely as well, since @actual_after is not initialized yet when we haven't executed the block: RuntimeError: Warnings were generated: rspec-dev/repos/rspec-expectations/lib/rspec/matchers/built_in/change.rb:407: warning: instance variable @actual_after not initialized Also see: https://github.com/rspec/rspec-expectations/pull/1139 https://github.com/rspec/rspec-expectations/pull/1125 --- Changelog.md | 12 +- lib/rspec/expectations/expectation_target.rb | 40 ++++++- lib/rspec/matchers/built_in/base_matcher.rb | 5 + lib/rspec/matchers/built_in/change.rb | 22 ++++ lib/rspec/matchers/built_in/compound.rb | 14 +++ lib/rspec/matchers/built_in/output.rb | 7 ++ lib/rspec/matchers/built_in/raise_error.rb | 6 + lib/rspec/matchers/built_in/throw_symbol.rb | 8 +- lib/rspec/matchers/built_in/yield.rb | 20 ++++ lib/rspec/matchers/dsl.rb | 4 + lib/rspec/matchers/matcher_protocol.rb | 6 + spec/rspec/matchers/aliased_matcher_spec.rb | 2 +- spec/rspec/matchers/built_in/all_spec.rb | 2 +- .../matchers/built_in/be_between_spec.rb | 2 +- .../matchers/built_in/be_instance_of_spec.rb | 2 +- .../matchers/built_in/be_kind_of_spec.rb | 2 +- .../rspec/matchers/built_in/be_within_spec.rb | 2 +- spec/rspec/matchers/built_in/change_spec.rb | 56 +++++---- spec/rspec/matchers/built_in/compound_spec.rb | 8 +- .../matchers/built_in/contain_exactly_spec.rb | 2 +- spec/rspec/matchers/built_in/cover_spec.rb | 2 +- spec/rspec/matchers/built_in/eq_spec.rb | 2 +- spec/rspec/matchers/built_in/eql_spec.rb | 2 +- spec/rspec/matchers/built_in/equal_spec.rb | 2 +- spec/rspec/matchers/built_in/exist_spec.rb | 4 +- spec/rspec/matchers/built_in/has_spec.rb | 4 +- .../matchers/built_in/have_attributes_spec.rb | 4 +- spec/rspec/matchers/built_in/include_spec.rb | 2 +- spec/rspec/matchers/built_in/match_spec.rb | 2 +- spec/rspec/matchers/built_in/output_spec.rb | 7 +- .../matchers/built_in/raise_error_spec.rb | 8 +- .../matchers/built_in/respond_to_spec.rb | 2 +- spec/rspec/matchers/built_in/satisfy_spec.rb | 2 +- .../built_in/start_and_end_with_spec.rb | 4 +- .../matchers/built_in/throw_symbol_spec.rb | 8 +- spec/rspec/matchers/built_in/yield_spec.rb | 57 +++++++-- .../matchers/define_negated_matcher_spec.rb | 2 +- spec/rspec/matchers/dsl_spec.rb | 2 +- spec/rspec/matchers_spec.rb | 5 +- spec/spec_helper.rb | 2 +- spec/support/shared_examples.rb | 111 ------------------ spec/support/shared_examples/block_matcher.rb | 95 +++++++++++++++ spec/support/shared_examples/matcher.rb | 44 +++++++ spec/support/shared_examples/value_matcher.rb | 66 +++++++++++ 44 files changed, 475 insertions(+), 186 deletions(-) delete mode 100644 spec/support/shared_examples.rb create mode 100644 spec/support/shared_examples/block_matcher.rb create mode 100644 spec/support/shared_examples/matcher.rb create mode 100644 spec/support/shared_examples/value_matcher.rb diff --git a/Changelog.md b/Changelog.md index a2ee4608b..3619c00a7 100644 --- a/Changelog.md +++ b/Changelog.md @@ -4,6 +4,16 @@ Breaking Changes: * Ruby < 2.3 is no longer supported. (Phil Pirozhkov, #1231) +* Remove `should` and `should_not` syntax (including one-liners). (Phil Pirozhkov, #1245) +* Turn `strict_predicate_matchers` on by default. (Phil Pirozhkov, #1277) +* Remove deprecated `LegacyMacherAdapter`. (Phil Pirozhkov, #1253) +* Remove support for legacy RSpec matchers (pre 3). (Phil Pirozhkov, #1253) +* Remove `include_chain_clauses_in_custom_matcher_descriptions` option + and make it the default. (Phil Pirozhkov, #1279) +* Restrict the implicit block expectation syntax. (Phil Pirozhkov, #????) + +### 3.10.1 / 2020-12-27 +[Full Changelog](http://github.com/rspec/rspec-expectations/compare/v3.10.0...v3.10.1) Enhancements: @@ -116,7 +126,7 @@ Bug Fixes: * Prevent composed `all` matchers from leaking into their siblings leading to duplicate failures. (Jamie English, #1086) * Prevent objects which change their hash on comparison from failing change checks. - (Phil Pirozhkov, #1110) + (Phil Pirozhkov, #1100) * Issue an `ArgumentError` rather than a `NoMethodError` when `be_an_instance_of` and `be_kind_of` matchers encounter objects not supporting those methods. (Taichi Ishitani, #1107) diff --git a/lib/rspec/expectations/expectation_target.rb b/lib/rspec/expectations/expectation_target.rb index 176b31224..b3cdbfbdb 100644 --- a/lib/rspec/expectations/expectation_target.rb +++ b/lib/rspec/expectations/expectation_target.rb @@ -42,7 +42,7 @@ def self.for(value, block) elsif block raise ArgumentError, "You cannot pass both an argument and a block to `expect`." else - new(value) + ValueExpectationTarget.new(value) end end @@ -90,6 +90,44 @@ def prevent_operator_matchers(verb) include InstanceMethods end + # @private + # Validates the provided matcher to ensure it supports block + # expectations, in order to avoid user confusion when they + # use a block thinking the expectation will be on the return + # value of the block rather than the block itself. + class ValueExpectationTarget < ExpectationTarget + def to(matcher=nil, message=nil, &block) + enforce_value_expectation(matcher) + super + end + + def not_to(matcher=nil, message=nil, &block) + enforce_value_expectation(matcher) + super + end + + private + + def enforce_value_expectation(matcher) + return if supports_value_expectations?(matcher) + + RSpec.deprecate( + "expect(value).to #{RSpec::Support::ObjectFormatter.format(matcher)}", + :message => + "The implicit block expectation syntax is deprecated, you should pass " \ + "a block rather than an argument to `expect` to use the provided " \ + "block expectation matcher (#{RSpec::Support::ObjectFormatter.format(matcher)}), " \ + "or the matcher must implement `supports_value_expectations?`." + ) + end + + def supports_value_expectations?(matcher) + matcher.supports_value_expectations? + rescue NoMethodError + true + end + end + # @private # Validates the provided matcher to ensure it supports block # expectations, in order to avoid user confusion when they diff --git a/lib/rspec/matchers/built_in/base_matcher.rb b/lib/rspec/matchers/built_in/base_matcher.rb index 5c39b80fc..0bf00df27 100644 --- a/lib/rspec/matchers/built_in/base_matcher.rb +++ b/lib/rspec/matchers/built_in/base_matcher.rb @@ -78,6 +78,11 @@ def supports_block_expectations? false end + # @private + def supports_value_expectations? + true + end + # @api private def expects_call_stack_jump? false diff --git a/lib/rspec/matchers/built_in/change.rb b/lib/rspec/matchers/built_in/change.rb index 2641c19a4..c43666a48 100644 --- a/lib/rspec/matchers/built_in/change.rb +++ b/lib/rspec/matchers/built_in/change.rb @@ -77,6 +77,11 @@ def supports_block_expectations? true end + # @private + def supports_value_expectations? + false + end + private def initialize(receiver=nil, message=nil, &block) @@ -158,6 +163,11 @@ def supports_block_expectations? true end + # @private + def supports_value_expectations? + false + end + private def failure_reason @@ -201,6 +211,11 @@ def supports_block_expectations? true end + # @private + def supports_value_expectations? + false + end + private def perform_change(event_proc) @@ -337,6 +352,8 @@ def change_description class ChangeDetails attr_reader :actual_after + UNDEFINED = Module.new.freeze + def initialize(matcher_name, receiver=nil, message=nil, &block) if receiver && !message raise( @@ -351,6 +368,11 @@ def initialize(matcher_name, receiver=nil, message=nil, &block) @receiver = receiver @message = message @value_proc = block + # TODO: temporary measure to mute warning of access to an initialized + # instance variable when a deprecated implicit block expectation + # syntax is used. This may be removed once `fail` is used, and the + # matcher never issues this warning. + @actual_after = UNDEFINED end def value_representation diff --git a/lib/rspec/matchers/built_in/compound.rb b/lib/rspec/matchers/built_in/compound.rb index 97f05dd61..56f27b1a6 100644 --- a/lib/rspec/matchers/built_in/compound.rb +++ b/lib/rspec/matchers/built_in/compound.rb @@ -26,11 +26,19 @@ def description "#{matcher_1.description} #{conjunction} #{matcher_2.description}" end + # @api private def supports_block_expectations? matcher_supports_block_expectations?(matcher_1) && matcher_supports_block_expectations?(matcher_2) end + # @api private + def supports_value_expectations? + matcher_supports_value_expectations?(matcher_1) && + matcher_supports_value_expectations?(matcher_2) + end + + # @api private def expects_call_stack_jump? NestedEvaluator.matcher_expects_call_stack_jump?(matcher_1) || NestedEvaluator.matcher_expects_call_stack_jump?(matcher_2) @@ -102,6 +110,12 @@ def matcher_supports_block_expectations?(matcher) false end + def matcher_supports_value_expectations?(matcher) + matcher.supports_value_expectations? + rescue NoMethodError + true + end + def matcher_is_diffable?(matcher) matcher.diffable? rescue NoMethodError diff --git a/lib/rspec/matchers/built_in/output.rb b/lib/rspec/matchers/built_in/output.rb index be100a26e..8c3cceda1 100644 --- a/lib/rspec/matchers/built_in/output.rb +++ b/lib/rspec/matchers/built_in/output.rb @@ -94,6 +94,13 @@ def supports_block_expectations? true end + # @api private + # Indicates this matcher matches against a block only. + # @return [False] + def supports_value_expectations? + false + end + private def captured? diff --git a/lib/rspec/matchers/built_in/raise_error.rb b/lib/rspec/matchers/built_in/raise_error.rb index bb9ffb927..a347ed3a9 100644 --- a/lib/rspec/matchers/built_in/raise_error.rb +++ b/lib/rspec/matchers/built_in/raise_error.rb @@ -86,6 +86,12 @@ def supports_block_expectations? true end + # @private + def supports_value_expectations? + false + end + + # @private def expects_call_stack_jump? true end diff --git a/lib/rspec/matchers/built_in/throw_symbol.rb b/lib/rspec/matchers/built_in/throw_symbol.rb index 97b7be4ff..ca002c867 100644 --- a/lib/rspec/matchers/built_in/throw_symbol.rb +++ b/lib/rspec/matchers/built_in/throw_symbol.rb @@ -85,12 +85,16 @@ def description end # @api private - # Indicates this matcher matches against a block. - # @return [True] def supports_block_expectations? true end + # @api private + def supports_value_expectations? + false + end + + # @api private def expects_call_stack_jump? true end diff --git a/lib/rspec/matchers/built_in/yield.rb b/lib/rspec/matchers/built_in/yield.rb index 9f16d1c57..ef4d17093 100644 --- a/lib/rspec/matchers/built_in/yield.rb +++ b/lib/rspec/matchers/built_in/yield.rb @@ -116,6 +116,11 @@ def supports_block_expectations? true end + # @private + def supports_value_expectations? + false + end + private def failure_reason @@ -156,6 +161,11 @@ def supports_block_expectations? true end + # @private + def supports_value_expectations? + false + end + private def positive_failure_reason @@ -218,6 +228,11 @@ def supports_block_expectations? true end + # @private + def supports_value_expectations? + false + end + private def positive_failure_reason @@ -315,6 +330,11 @@ def supports_block_expectations? true end + # @private + def supports_value_expectations? + false + end + private def expected_arg_description diff --git a/lib/rspec/matchers/dsl.rb b/lib/rspec/matchers/dsl.rb index 641a637e9..ef3f5e0ca 100644 --- a/lib/rspec/matchers/dsl.rb +++ b/lib/rspec/matchers/dsl.rb @@ -391,6 +391,10 @@ def supports_block_expectations? false end + def supports_value_expectations? + true + end + # Most matchers do not expect call stack jumps. def expects_call_stack_jump? false diff --git a/lib/rspec/matchers/matcher_protocol.rb b/lib/rspec/matchers/matcher_protocol.rb index c5bc432e1..4a87f6481 100644 --- a/lib/rspec/matchers/matcher_protocol.rb +++ b/lib/rspec/matchers/matcher_protocol.rb @@ -60,6 +60,12 @@ class MatcherProtocol # @return [Boolean] true if this matcher can be used in block expressions. # @note If not defined, RSpec assumes a value of `false` for this method. + # @!method supports_value_expectations? + # Indicates that this matcher can be used in a value expectation expression, + # such as `expect(foo).to eq(bar)`. + # @return [Boolean] true if this matcher can be used in value expressions. + # @note If not defined, RSpec assumes a value of `true` for this method. + # @!method expects_call_stack_jump? # Indicates that when this matcher is used in a block expectation # expression, it expects the block to use a ruby construct that causes diff --git a/spec/rspec/matchers/aliased_matcher_spec.rb b/spec/rspec/matchers/aliased_matcher_spec.rb index ca3c45b6b..c9f16ffe2 100644 --- a/spec/rspec/matchers/aliased_matcher_spec.rb +++ b/spec/rspec/matchers/aliased_matcher_spec.rb @@ -14,7 +14,7 @@ def description end RSpec::Matchers.alias_matcher :alias_of_my_base_matcher, :my_base_matcher - it_behaves_like "an RSpec matcher", :valid_value => 13, :invalid_value => nil do + it_behaves_like "an RSpec value matcher", :valid_value => 13, :invalid_value => nil do let(:matcher) { alias_of_my_base_matcher } end diff --git a/spec/rspec/matchers/built_in/all_spec.rb b/spec/rspec/matchers/built_in/all_spec.rb index 9ef463cec..be7231e37 100644 --- a/spec/rspec/matchers/built_in/all_spec.rb +++ b/spec/rspec/matchers/built_in/all_spec.rb @@ -1,7 +1,7 @@ module RSpec::Matchers::BuiltIn RSpec.describe All do - it_behaves_like 'an RSpec matcher', :valid_value => ['A', 'A', 'A'], :invalid_value => ['A', 'A', 'B'], :disallows_negation => true do + it_behaves_like 'an RSpec value matcher', :valid_value => ['A', 'A', 'A'], :invalid_value => ['A', 'A', 'B'], :disallows_negation => true do let(:matcher) { all( eq('A') ) } end diff --git a/spec/rspec/matchers/built_in/be_between_spec.rb b/spec/rspec/matchers/built_in/be_between_spec.rb index f7aaaf978..c6a218269 100644 --- a/spec/rspec/matchers/built_in/be_between_spec.rb +++ b/spec/rspec/matchers/built_in/be_between_spec.rb @@ -80,7 +80,7 @@ def inspect end end - it_behaves_like "an RSpec matcher", :valid_value => (10), :invalid_value => (11) do + it_behaves_like "an RSpec value matcher", :valid_value => (10), :invalid_value => (11) do let(:matcher) { be_between(1, 10) } end diff --git a/spec/rspec/matchers/built_in/be_instance_of_spec.rb b/spec/rspec/matchers/built_in/be_instance_of_spec.rb index ac2252e73..d3864969c 100644 --- a/spec/rspec/matchers/built_in/be_instance_of_spec.rb +++ b/spec/rspec/matchers/built_in/be_instance_of_spec.rb @@ -2,7 +2,7 @@ module RSpec module Matchers [:be_an_instance_of, :be_instance_of].each do |method| RSpec.describe "expect(actual).to #{method}(expected)" do - it_behaves_like "an RSpec matcher", :valid_value => "a", :invalid_value => 5 do + it_behaves_like "an RSpec value matcher", :valid_value => "a", :invalid_value => 5 do let(:matcher) { send(method, String) } end diff --git a/spec/rspec/matchers/built_in/be_kind_of_spec.rb b/spec/rspec/matchers/built_in/be_kind_of_spec.rb index 4d479c4f9..497525f06 100644 --- a/spec/rspec/matchers/built_in/be_kind_of_spec.rb +++ b/spec/rspec/matchers/built_in/be_kind_of_spec.rb @@ -2,7 +2,7 @@ module RSpec module Matchers [:be_a_kind_of, :be_kind_of].each do |method| RSpec.describe "expect(actual).to #{method}(expected)" do - it_behaves_like "an RSpec matcher", :valid_value => 5, :invalid_value => "a" do + it_behaves_like "an RSpec value matcher", :valid_value => 5, :invalid_value => "a" do let(:matcher) { send(method, Integer) } end diff --git a/spec/rspec/matchers/built_in/be_within_spec.rb b/spec/rspec/matchers/built_in/be_within_spec.rb index fa9109ab4..36826ff8f 100644 --- a/spec/rspec/matchers/built_in/be_within_spec.rb +++ b/spec/rspec/matchers/built_in/be_within_spec.rb @@ -1,7 +1,7 @@ module RSpec module Matchers RSpec.describe "expect(actual).to be_within(delta).of(expected)" do - it_behaves_like "an RSpec matcher", :valid_value => 5, :invalid_value => -5 do + it_behaves_like "an RSpec value matcher", :valid_value => 5, :invalid_value => -5 do let(:matcher) { be_within(2).of(4.0) } end diff --git a/spec/rspec/matchers/built_in/change_spec.rb b/spec/rspec/matchers/built_in/change_spec.rb index eb6172b2c..9117a526f 100644 --- a/spec/rspec/matchers/built_in/change_spec.rb +++ b/spec/rspec/matchers/built_in/change_spec.rb @@ -1026,39 +1026,51 @@ def @instance.send(*_args); raise "DOH! Library developers shouldn't use #send!" }.not_to raise_error end - k = 1 - before { k = 1 } - it_behaves_like "an RSpec matcher", :valid_value => lambda { k += 1 }, - :invalid_value => lambda { } do - let(:matcher) { change { k } } + it_behaves_like "an RSpec block-only matcher" do + let(:matcher) { change { @k } } + before { @k = 1 } + def valid_block + @k += 1 + end + def invalid_block + end end end RSpec.describe RSpec::Matchers::BuiltIn::ChangeRelatively do - k = 0 - before { k = 0 } - it_behaves_like "an RSpec matcher", :valid_value => lambda { k += 1 }, - :invalid_value => lambda { k += 2 }, - :disallows_negation => true do - let(:matcher) { change { k }.by(1) } + it_behaves_like "an RSpec block-only matcher", :disallows_negation => true, :skip_deprecation_check => true do + let(:matcher) { change { @k }.by(1) } + before { @k = 0 } + def valid_block + @k += 1 + end + def invalid_block + @k += 2 + end end end RSpec.describe RSpec::Matchers::BuiltIn::ChangeFromValue do - k = 0 - before { k = 0 } - it_behaves_like "an RSpec matcher", :valid_value => lambda { k += 1 }, - :invalid_value => lambda { } do - let(:matcher) { change { k }.from(0) } + it_behaves_like "an RSpec block-only matcher" do + let(:matcher) { change { @k }.from(0) } + before { @k = 0 } + def valid_block + @k += 1 + end + def invalid_block + end end end RSpec.describe RSpec::Matchers::BuiltIn::ChangeToValue do - k = 0 - before { k = 0 } - it_behaves_like "an RSpec matcher", :valid_value => lambda { k = 2 }, - :invalid_value => lambda { k = 3 }, - :disallows_negation => true do - let(:matcher) { change { k }.to(2) } + it_behaves_like "an RSpec block-only matcher", :disallows_negation => true do + let(:matcher) { change { @k }.to(2) } + before { @k = 0 } + def valid_block + @k = 2 + end + def invalid_block + @k = 3 + end end end diff --git a/spec/rspec/matchers/built_in/compound_spec.rb b/spec/rspec/matchers/built_in/compound_spec.rb index fadf6ed37..0594b6b0a 100644 --- a/spec/rspec/matchers/built_in/compound_spec.rb +++ b/spec/rspec/matchers/built_in/compound_spec.rb @@ -262,12 +262,12 @@ def expect_block describe "expect(...).to matcher.and(other_matcher)" do - it_behaves_like "an RSpec matcher", :valid_value => 3, :invalid_value => 4, :disallows_negation => true do + it_behaves_like "an RSpec value matcher", :valid_value => 3, :invalid_value => 4, :disallows_negation => true do let(:matcher) { eq(3).and be <= 3 } end context 'when using boolean AND `&` alias' do - it_behaves_like "an RSpec matcher", :valid_value => 3, :invalid_value => 4, :disallows_negation => true do + it_behaves_like "an RSpec value matcher", :valid_value => 3, :invalid_value => 4, :disallows_negation => true do let(:matcher) { eq(3) & be_a(Integer) } end end @@ -570,12 +570,12 @@ def expect_block end describe "expect(...).to matcher.or(other_matcher)" do - it_behaves_like "an RSpec matcher", :valid_value => 3, :invalid_value => 5, :disallows_negation => true do + it_behaves_like "an RSpec value matcher", :valid_value => 3, :invalid_value => 5, :disallows_negation => true do let(:matcher) { eq(3).or eq(4) } end context 'when using boolean OR `|` alias' do - it_behaves_like "an RSpec matcher", :valid_value => 3, :invalid_value => 5, :disallows_negation => true do + it_behaves_like "an RSpec value matcher", :valid_value => 3, :invalid_value => 5, :disallows_negation => true do let(:matcher) { eq(3) | eq(4) } end end diff --git a/spec/rspec/matchers/built_in/contain_exactly_spec.rb b/spec/rspec/matchers/built_in/contain_exactly_spec.rb index 075d7469c..ac1996ae8 100644 --- a/spec/rspec/matchers/built_in/contain_exactly_spec.rb +++ b/spec/rspec/matchers/built_in/contain_exactly_spec.rb @@ -148,7 +148,7 @@ def array.send; :sent; end end RSpec.describe "expect(array).to contain_exactly(*other_array)" do - it_behaves_like "an RSpec matcher", :valid_value => [1, 2], :invalid_value => [1] do + it_behaves_like "an RSpec value matcher", :valid_value => [1, 2], :invalid_value => [1] do let(:matcher) { contain_exactly(2, 1) } end diff --git a/spec/rspec/matchers/built_in/cover_spec.rb b/spec/rspec/matchers/built_in/cover_spec.rb index 4e91e9336..ff73fb80c 100644 --- a/spec/rspec/matchers/built_in/cover_spec.rb +++ b/spec/rspec/matchers/built_in/cover_spec.rb @@ -1,6 +1,6 @@ if (1..2).respond_to?(:cover?) RSpec.describe "expect(...).to cover(expected)" do - it_behaves_like "an RSpec matcher", :valid_value => (1..10), :invalid_value => (20..30) do + it_behaves_like "an RSpec value matcher", :valid_value => (1..10), :invalid_value => (20..30) do let(:matcher) { cover(5) } end diff --git a/spec/rspec/matchers/built_in/eq_spec.rb b/spec/rspec/matchers/built_in/eq_spec.rb index 45fa37635..e8bf1373f 100644 --- a/spec/rspec/matchers/built_in/eq_spec.rb +++ b/spec/rspec/matchers/built_in/eq_spec.rb @@ -1,7 +1,7 @@ module RSpec module Matchers RSpec.describe "eq" do - it_behaves_like "an RSpec matcher", :valid_value => 1, :invalid_value => 2 do + it_behaves_like "an RSpec value matcher", :valid_value => 1, :invalid_value => 2 do let(:matcher) { eq(1) } end diff --git a/spec/rspec/matchers/built_in/eql_spec.rb b/spec/rspec/matchers/built_in/eql_spec.rb index 4491fc9e8..d07b14189 100644 --- a/spec/rspec/matchers/built_in/eql_spec.rb +++ b/spec/rspec/matchers/built_in/eql_spec.rb @@ -1,7 +1,7 @@ module RSpec module Matchers RSpec.describe "eql" do - it_behaves_like "an RSpec matcher", :valid_value => 1, :invalid_value => 2 do + it_behaves_like "an RSpec value matcher", :valid_value => 1, :invalid_value => 2 do let(:matcher) { eql(1) } end diff --git a/spec/rspec/matchers/built_in/equal_spec.rb b/spec/rspec/matchers/built_in/equal_spec.rb index f56006d61..94c1db7a9 100644 --- a/spec/rspec/matchers/built_in/equal_spec.rb +++ b/spec/rspec/matchers/built_in/equal_spec.rb @@ -1,7 +1,7 @@ module RSpec module Matchers RSpec.describe "equal" do - it_behaves_like "an RSpec matcher", :valid_value => :a, :invalid_value => :b do + it_behaves_like "an RSpec value matcher", :valid_value => :a, :invalid_value => :b do let(:matcher) { equal(:a) } end diff --git a/spec/rspec/matchers/built_in/exist_spec.rb b/spec/rspec/matchers/built_in/exist_spec.rb index cecfea445..2d2c4e5bc 100644 --- a/spec/rspec/matchers/built_in/exist_spec.rb +++ b/spec/rspec/matchers/built_in/exist_spec.rb @@ -1,6 +1,6 @@ RSpec.describe "exist matcher" do - it_behaves_like "an RSpec matcher", :valid_value => Class.new { def exist?; true; end }.new, - :invalid_value => Class.new { def exist?; false; end }.new do + it_behaves_like "an RSpec value matcher", :valid_value => Class.new { def exist?; true; end }.new, + :invalid_value => Class.new { def exist?; false; end }.new do let(:matcher) { exist } end diff --git a/spec/rspec/matchers/built_in/has_spec.rb b/spec/rspec/matchers/built_in/has_spec.rb index fbb128bc9..1c5f0e619 100644 --- a/spec/rspec/matchers/built_in/has_spec.rb +++ b/spec/rspec/matchers/built_in/has_spec.rb @@ -1,6 +1,6 @@ RSpec.describe "expect(...).to have_sym(*args)" do - it_behaves_like "an RSpec matcher", :valid_value => { :a => 1 }, - :invalid_value => {} do + it_behaves_like "an RSpec value matcher", :valid_value => { :a => 1 }, + :invalid_value => {} do let(:matcher) { have_key(:a) } end diff --git a/spec/rspec/matchers/built_in/have_attributes_spec.rb b/spec/rspec/matchers/built_in/have_attributes_spec.rb index 98ced50f8..b9e024186 100644 --- a/spec/rspec/matchers/built_in/have_attributes_spec.rb +++ b/spec/rspec/matchers/built_in/have_attributes_spec.rb @@ -35,7 +35,7 @@ def respond_to?(method_name) end describe "expect(...).to have_attributes(with_one_attribute)" do - it_behaves_like "an RSpec matcher", :valid_value => Person.new("Correct name", 33), :invalid_value => Person.new("Wrong Name", 11) do + it_behaves_like "an RSpec value matcher", :valid_value => Person.new("Correct name", 33), :invalid_value => Person.new("Wrong Name", 11) do let(:matcher) { have_attributes(:name => "Correct name") } end @@ -146,7 +146,7 @@ def count end describe "expect(...).to have_attributes(with_multiple_attributes)" do - it_behaves_like "an RSpec matcher", :valid_value => Person.new("Correct name", 33), :invalid_value => Person.new("Wrong Name", 11) do + it_behaves_like "an RSpec value matcher", :valid_value => Person.new("Correct name", 33), :invalid_value => Person.new("Wrong Name", 11) do let(:matcher) { have_attributes(:name => "Correct name", :age => 33) } end diff --git a/spec/rspec/matchers/built_in/include_spec.rb b/spec/rspec/matchers/built_in/include_spec.rb index 3d57f97f7..a9d9908aa 100644 --- a/spec/rspec/matchers/built_in/include_spec.rb +++ b/spec/rspec/matchers/built_in/include_spec.rb @@ -108,7 +108,7 @@ def hash.send; :sent; end end describe "expect(...).to include(with_one_arg)" do - it_behaves_like "an RSpec matcher", :valid_value => [1, 2], :invalid_value => [1] do + it_behaves_like "an RSpec value matcher", :valid_value => [1, 2], :invalid_value => [1] do let(:matcher) { include(2) } end diff --git a/spec/rspec/matchers/built_in/match_spec.rb b/spec/rspec/matchers/built_in/match_spec.rb index 3c154b2b8..df0d5464b 100644 --- a/spec/rspec/matchers/built_in/match_spec.rb +++ b/spec/rspec/matchers/built_in/match_spec.rb @@ -1,5 +1,5 @@ RSpec.describe "expect(...).to match(expected)" do - it_behaves_like "an RSpec matcher", :valid_value => 'ab', :invalid_value => 'bc' do + it_behaves_like "an RSpec value matcher", :valid_value => 'ab', :invalid_value => 'bc' do let(:matcher) { match(/a/) } end diff --git a/spec/rspec/matchers/built_in/output_spec.rb b/spec/rspec/matchers/built_in/output_spec.rb index f51f4c562..07f9e8979 100644 --- a/spec/rspec/matchers/built_in/output_spec.rb +++ b/spec/rspec/matchers/built_in/output_spec.rb @@ -2,8 +2,13 @@ include helper_module extend helper_module - it_behaves_like("an RSpec matcher", :valid_value => lambda { print_to_stream('foo') }, :invalid_value => lambda {}) do + it_behaves_like "an RSpec block-only matcher" do let(:matcher) { output(/fo/).send(matcher_method) } + def valid_block + print_to_stream('foo') + end + def invalid_block + end end define_method :matcher do |*args| diff --git a/spec/rspec/matchers/built_in/raise_error_spec.rb b/spec/rspec/matchers/built_in/raise_error_spec.rb index 7e7c264e5..07d3f5d09 100644 --- a/spec/rspec/matchers/built_in/raise_error_spec.rb +++ b/spec/rspec/matchers/built_in/raise_error_spec.rb @@ -1,6 +1,10 @@ RSpec.describe "expect { ... }.to raise_error" do - it_behaves_like("an RSpec matcher", :valid_value => lambda { raise "boom" }, - :invalid_value => lambda { }) do + it_behaves_like "an RSpec block-only matcher" do + def valid_block + raise "boom" + end + def invalid_block + end let(:matcher) { raise_error Exception } end diff --git a/spec/rspec/matchers/built_in/respond_to_spec.rb b/spec/rspec/matchers/built_in/respond_to_spec.rb index fa918b8df..c235978f6 100644 --- a/spec/rspec/matchers/built_in/respond_to_spec.rb +++ b/spec/rspec/matchers/built_in/respond_to_spec.rb @@ -1,5 +1,5 @@ RSpec.describe "expect(...).to respond_to(:sym)" do - it_behaves_like "an RSpec matcher", :valid_value => "s", :invalid_value => 5 do + it_behaves_like "an RSpec value matcher", :valid_value => "s", :invalid_value => 5 do let(:matcher) { respond_to(:upcase) } end diff --git a/spec/rspec/matchers/built_in/satisfy_spec.rb b/spec/rspec/matchers/built_in/satisfy_spec.rb index 6d6b7f174..f7748ecc7 100644 --- a/spec/rspec/matchers/built_in/satisfy_spec.rb +++ b/spec/rspec/matchers/built_in/satisfy_spec.rb @@ -1,5 +1,5 @@ RSpec.describe "expect(...).to satisfy { block }" do - it_behaves_like "an RSpec matcher", :valid_value => true, :invalid_value => false do + it_behaves_like "an RSpec value matcher", :valid_value => true, :invalid_value => false do let(:matcher) { satisfy { |v| v } } end diff --git a/spec/rspec/matchers/built_in/start_and_end_with_spec.rb b/spec/rspec/matchers/built_in/start_and_end_with_spec.rb index 77dcf43b7..b063f199d 100644 --- a/spec/rspec/matchers/built_in/start_and_end_with_spec.rb +++ b/spec/rspec/matchers/built_in/start_and_end_with_spec.rb @@ -1,5 +1,5 @@ RSpec.describe "expect(...).to start_with" do - it_behaves_like "an RSpec matcher", :valid_value => "ab", :invalid_value => "bc" do + it_behaves_like "an RSpec value matcher", :valid_value => "ab", :invalid_value => "bc" do let(:matcher) { start_with("a") } end @@ -207,7 +207,7 @@ def ==(other) end RSpec.describe "expect(...).to end_with" do - it_behaves_like "an RSpec matcher", :valid_value => "ab", :invalid_value => "bc" do + it_behaves_like "an RSpec value matcher", :valid_value => "ab", :invalid_value => "bc" do let(:matcher) { end_with("b") } end diff --git a/spec/rspec/matchers/built_in/throw_symbol_spec.rb b/spec/rspec/matchers/built_in/throw_symbol_spec.rb index 75da4d806..d9300f6dc 100644 --- a/spec/rspec/matchers/built_in/throw_symbol_spec.rb +++ b/spec/rspec/matchers/built_in/throw_symbol_spec.rb @@ -1,7 +1,11 @@ module RSpec::Matchers::BuiltIn RSpec.describe ThrowSymbol do - it_behaves_like("an RSpec matcher", :valid_value => lambda { throw :foo }, - :invalid_value => lambda { }) do + it_behaves_like "an RSpec block-only matcher" do + def valid_block + throw :foo + end + def invalid_block + end let(:matcher) { throw_symbol(:foo) } end diff --git a/spec/rspec/matchers/built_in/yield_spec.rb b/spec/rspec/matchers/built_in/yield_spec.rb index cee5e13e3..d8c9613d9 100644 --- a/spec/rspec/matchers/built_in/yield_spec.rb +++ b/spec/rspec/matchers/built_in/yield_spec.rb @@ -29,15 +29,36 @@ def each_arg(*args, &block) end end +# NOTE: `yield` passes a probe to expect an that probe should be passed +# to expectation target. This is different from the other block matchers. +# Due to strict requirement in Ruby 1.8 to call a block with arguments if +# the block is declared to accept them. To work around this limitation, +# this example group overrides the default definition of expectations +# and lambdas that take the expectation target in a way that they accept +# a probe. +RSpec.shared_examples "an RSpec probe-yielding block-only matcher" do |*options| + include_examples "an RSpec block-only matcher", { :expects_lambda => true }.merge(options.first || {}) do + let(:valid_expectation) { expect { |block| valid_block(&block) } } + let(:invalid_expectation) { expect { |block| invalid_block(&block) } } + + let(:valid_block_lambda) { lambda { |block| valid_block(&block) } } + let(:invalid_block_lambda) { lambda { |block| invalid_block(&block) } } + end +end + RSpec.describe "yield_control matcher" do include YieldHelpers extend YieldHelpers - it_behaves_like "an RSpec matcher", - :valid_value => lambda { |b| _yield_with_no_args(&b) }, - :invalid_value => lambda { |b| _dont_yield(&b) }, + it_behaves_like "an RSpec probe-yielding block-only matcher", :failure_message_uses_no_inspect => true do let(:matcher) { yield_control } + def valid_block(&block) + _yield_with_no_args(&block) + end + def invalid_block(&block) + _dont_yield(&block) + end end it 'has a description' do @@ -244,10 +265,14 @@ def each_arg(*args, &block) include YieldHelpers extend YieldHelpers - it_behaves_like "an RSpec matcher", - :valid_value => lambda { |b| _yield_with_no_args(&b) }, - :invalid_value => lambda { |b| _yield_with_args(1, &b) } do + it_behaves_like "an RSpec probe-yielding block-only matcher" do let(:matcher) { yield_with_no_args } + def valid_block(&block) + _yield_with_no_args(&block) + end + def invalid_block(&block) + _yield_with_args(1, &block) + end end it 'has a description' do @@ -327,10 +352,14 @@ def each_arg(*args, &block) include YieldHelpers extend YieldHelpers - it_behaves_like "an RSpec matcher", - :valid_value => lambda { |b| _yield_with_args(1, &b) }, - :invalid_value => lambda { |b| _yield_with_args(2, &b) } do + it_behaves_like "an RSpec probe-yielding block-only matcher" do let(:matcher) { yield_with_args(1) } + def valid_block(&block) + _yield_with_args(1, &block) + end + def invalid_block(&block) + _yield_with_args(2, &block) + end end it 'has a description' do @@ -587,10 +616,14 @@ def each_arg(*args, &block) include YieldHelpers extend YieldHelpers - it_behaves_like "an RSpec matcher", - :valid_value => lambda { |b| [1, 2].each(&b) }, - :invalid_value => lambda { |b| [3, 4].each(&b) } do + it_behaves_like "an RSpec probe-yielding block-only matcher" do let(:matcher) { yield_successive_args(1, 2) } + def valid_block(&block) + [1, 2].each(&block) + end + def invalid_block(&block) + [3, 4].each(&block) + end end it 'has a description' do diff --git a/spec/rspec/matchers/define_negated_matcher_spec.rb b/spec/rspec/matchers/define_negated_matcher_spec.rb index b6f0e0ad7..3123847e7 100644 --- a/spec/rspec/matchers/define_negated_matcher_spec.rb +++ b/spec/rspec/matchers/define_negated_matcher_spec.rb @@ -45,7 +45,7 @@ def description include_examples "making a copy", :clone RSpec::Matchers.define_negated_matcher :an_array_excluding, :include - it_behaves_like "an RSpec matcher", :valid_value => [1, 3], :invalid_value => [1, 2] do + it_behaves_like "an RSpec value matcher", :valid_value => [1, 3], :invalid_value => [1, 2] do let(:matcher) { an_array_excluding(2) } end diff --git a/spec/rspec/matchers/dsl_spec.rb b/spec/rspec/matchers/dsl_spec.rb index d20607b2b..28190e84c 100644 --- a/spec/rspec/matchers/dsl_spec.rb +++ b/spec/rspec/matchers/dsl_spec.rb @@ -175,7 +175,7 @@ def new_matcher(name, *expected, &block) RSpec::Matchers::DSL::Matcher.new(name, block, self, *expected) end - it_behaves_like "an RSpec matcher", :valid_value => 1, :invalid_value => 2 do + it_behaves_like "an RSpec value matcher", :valid_value => 1, :invalid_value => 2 do let(:matcher) do new_matcher(:equal_to_1) do match { |v| v == 1 } diff --git a/spec/rspec/matchers_spec.rb b/spec/rspec/matchers_spec.rb index 880dfde25..7ebde28a4 100644 --- a/spec/rspec/matchers_spec.rb +++ b/spec/rspec/matchers_spec.rb @@ -112,8 +112,9 @@ module Matchers # This spec is merely to make sure we don't forget to make # a built-in matcher implement `===`. It doesn't check the - # semantics of that. Use the "an RSpec matcher" shared - # example group to actually check the semantics. + # semantics of that. Use the "an RSpec value matcher" and + # "an RSpec block-only matcher" shared example groups to + # actually check the semantics. expect(missing_threequals).to eq([]) end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index e49d7b50f..968383926 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -5,7 +5,7 @@ minimum_coverage 91 end -Dir['./spec/support/**/*'].each do |f| +Dir['./spec/support/**/*.rb'].each do |f| require f.sub(%r{\./spec/}, '') end diff --git a/spec/support/shared_examples.rb b/spec/support/shared_examples.rb deleted file mode 100644 index aaca05806..000000000 --- a/spec/support/shared_examples.rb +++ /dev/null @@ -1,111 +0,0 @@ -RSpec.shared_examples "an RSpec matcher" do |options| - let(:valid_value) { options.fetch(:valid_value) } - let(:invalid_value) { options.fetch(:invalid_value) } - - # Note: do not use `matcher` in 2 expectation expressions in a single - # example here. In some cases (such as `change { }.to(2)`), it will fail - # because using it a second time will apply `x += 2` twice, changing - # the value to 4. - - it 'preserves the symmetric property of `==`' do - expect(matcher).to eq(matcher) - expect(matcher).not_to eq(valid_value) - expect(valid_value).not_to eq(matcher) - end - - it 'matches a valid value when using #=== so it can be composed' do - expect(matcher).to be === valid_value - end - - it 'does not match an invalid value when using #=== so it can be composed' do - expect(matcher).not_to be === invalid_value - end - - matcher :always_passes do - supports_block_expectations - match do |actual| - actual.call if Proc === actual - true - end - end - - matcher :always_fails do - supports_block_expectations - match do |actual| - actual.call if Proc === actual - false - end - end - - it 'allows additional matchers to be chained off it using `and`' do - expect(valid_value).to matcher.and always_passes - end - - it 'can be chained off of an existing matcher using `and`' do - expect(valid_value).to always_passes.and matcher - end - - it 'allows additional matchers to be chained off it using `or`' do - expect(valid_value).to matcher.or always_fails - end - - it 'can be chained off of an existing matcher using `or`' do - expect(valid_value).to always_fails.or matcher - end - - it 'implements the full matcher protocol' do - expect(matcher).to respond_to( - :matches?, - :failure_message, - :description, - :supports_block_expectations?, - :expects_call_stack_jump? - ) - - # We do not require failure_message_when_negated and does_not_match? - # Because some matchers purposefully do not support negation. - end - - it 'fails gracefully when given a value if it is a block matcher' do - if matcher.supports_block_expectations? - expect { - expect(3).to matcher - }.to fail_with(/was not( given)? a block/) - - unless options[:disallows_negation] - expect { - expect(3).not_to matcher - }.to fail_with(/was not( given)? a block/) - end - end - end - - it 'can be used in a composed matcher expression' do - expect([valid_value, invalid_value]).to include(matcher) - - expect { - expect([invalid_value]).to include(matcher) - }.to fail_including("include (#{matcher.description})") - end - - it 'can match negatively properly' do - unless options[:disallows_negation] - expect(invalid_value).not_to matcher - - expect { - expect(valid_value).not_to matcher - }.to fail - end - end - - it 'uses the `ObjectFormatter` for `failure_message`' do - allow(RSpec::Support::ObjectFormatter).to receive(:format).and_return("detailed inspect") - matcher.matches?(invalid_value) - message = matcher.failure_message - - # Undo our stub so it doesn't affect the `include` matcher below. - allow(RSpec::Support::ObjectFormatter).to receive(:format).and_call_original - expect(message).to include("detailed inspect") - end unless options[:failure_message_uses_no_inspect] -end - diff --git a/spec/support/shared_examples/block_matcher.rb b/spec/support/shared_examples/block_matcher.rb new file mode 100644 index 000000000..e5c95aa07 --- /dev/null +++ b/spec/support/shared_examples/block_matcher.rb @@ -0,0 +1,95 @@ +RSpec.shared_examples "an RSpec block-only matcher" do |*options| + # Note: Ruby 1.8 expects you to call a block with arguments if it is + # declared that accept arguments. In this case, some of the specs + # that include examples from this shared example group do not pass + # arguments. A workaround is to use splat and pick the first argument + # if it was passed. + options = options.first || {} + + # Note: do not use `matcher` in 2 expectation expressions in a single + # example here. In some cases (such as `change { x += 2 }.to(2)`), it + # will fail because using it a second time will apply `x += 2` twice, + # changing the value to 4. + + matcher :always_passes do + supports_block_expectations + match do |actual| + actual.call + true + end + end + + matcher :always_fails do + supports_block_expectations + match do |actual| + actual.call + false + end + end + + let(:valid_expectation) { expect { valid_block } } + let(:invalid_expectation) { expect { invalid_block } } + + let(:valid_block_lambda) { lambda { valid_block } } + let(:invalid_block_lambda) { lambda { invalid_block } } + + include_examples "an RSpec matcher", options + + it 'preserves the symmetric property of `==`' do + expect(matcher).to eq(matcher) + expect(matcher).not_to eq(valid_block_lambda) + expect(valid_block_lambda).not_to eq(matcher) + end + + it 'matches a valid block when using #=== so it can be composed' do + expect(matcher).to be === valid_block_lambda + end + + it 'does not match an invalid block when using #=== so it can be composed' do + expect(matcher).not_to be === invalid_block_lambda + end + + it 'matches a valid block when using #=== so it can be composed' do + expect(matcher).to be === valid_block_lambda + end + + it 'does not match an invalid block when using #=== so it can be composed' do + expect(matcher).not_to be === invalid_block_lambda + end + + it 'uses the `ObjectFormatter` for `failure_message`' do + allow(RSpec::Support::ObjectFormatter).to receive(:format).and_return("detailed inspect") + expect { invalid_expectation.to matcher }.to raise_error do |error| + # Undo our stub so it doesn't affect the `include` matcher below. + allow(RSpec::Support::ObjectFormatter).to receive(:format).and_call_original + expect(error.message).to include("detailed inspect") + end + end unless options[:failure_message_uses_no_inspect] + + it 'fails gracefully when given a value' do + expect { + expect(3).to matcher + }.to fail_with(/was not( given)? a block/) + + unless options[:disallows_negation] + expect { + expect(3).not_to matcher + }.to fail_with(/was not( given)? a block/) + end + end + + it 'prints a deprecation warning when given a value' do + expect_warn_deprecation(/The implicit block expectation syntax is deprecated, you should pass/) + expect { expect(3).to matcher }.to fail + end unless options[:skip_deprecation_check] || options[:expects_lambda] + + it 'prints a deprecation warning when given a value and negated' do + expect_warn_deprecation(/The implicit block expectation syntax is deprecated, you should pass/) + expect { expect(3).not_to matcher }.to fail + end unless options[:disallows_negation] || options[:expects_lambda] + + it 'allows lambda expectation target' do + allow_deprecation + expect(valid_block_lambda).to matcher + end +end diff --git a/spec/support/shared_examples/matcher.rb b/spec/support/shared_examples/matcher.rb new file mode 100644 index 000000000..ad255d361 --- /dev/null +++ b/spec/support/shared_examples/matcher.rb @@ -0,0 +1,44 @@ +RSpec.shared_examples "an RSpec matcher" do |options| + # Note: do not use `matcher` in 2 expectation expressions in a single + # example here. In some cases (such as `change { }.to(2)`), it will fail + # because using it a second time will apply `x += 2` twice, changing + # the value to 4. + + it 'allows additional matchers to be chained off it using `and`' do + valid_expectation.to matcher.and always_passes + end + + it 'can be chained off of an existing matcher using `and`' do + valid_expectation.to always_passes.and matcher + end + + it 'allows additional matchers to be chained off it using `or`' do + valid_expectation.to matcher.or always_fails + end + + it 'can be chained off of an existing matcher using `or`' do + valid_expectation.to always_fails.or matcher + end + + it 'implements the full matcher protocol' do + expect(matcher).to respond_to( + :matches?, + :failure_message, + :description, + :supports_block_expectations?, + :supports_value_expectations?, + :expects_call_stack_jump? + ) + + # We do not require failure_message_when_negated and does_not_match? + # Because some matchers purposefully do not support negation. + end + + it 'can match negatively properly' do + invalid_expectation.not_to matcher + + expect { + valid_expectation.not_to matcher + }.to fail + end unless options[:disallows_negation] +end diff --git a/spec/support/shared_examples/value_matcher.rb b/spec/support/shared_examples/value_matcher.rb new file mode 100644 index 000000000..bcdbee9db --- /dev/null +++ b/spec/support/shared_examples/value_matcher.rb @@ -0,0 +1,66 @@ +RSpec.shared_examples "an RSpec value matcher" do |options| + let(:valid_value) { options.fetch(:valid_value) } + let(:invalid_value) { options.fetch(:invalid_value) } + + matcher :always_passes do + match { |_actual| true } + end + + matcher :always_fails do + match { |_actual| false } + end + + def valid_expectation + expect(valid_value) + end + + def invalid_expectation + expect(invalid_value) + end + + include_examples "an RSpec matcher", options + + it 'preserves the symmetric property of `==`' do + expect(matcher).to eq(matcher) + expect(matcher).not_to eq(valid_value) + expect(valid_value).not_to eq(matcher) + end + + it 'matches a valid value when using #=== so it can be composed' do + expect(matcher).to be === valid_value + end + + it 'does not match an invalid value when using #=== so it can be composed' do + expect(matcher).not_to be === invalid_value + end + + it 'can be used in a composed matcher expression' do + expect([valid_value, invalid_value]).to include(matcher) + + expect { + expect([invalid_value]).to include(matcher) + }.to fail_including("include (#{matcher.description})") + end + + it 'uses the `ObjectFormatter` for `failure_message`' do + allow(RSpec::Support::ObjectFormatter).to receive(:format).and_return("detailed inspect") + matcher.matches?(invalid_value) + message = matcher.failure_message + + # Undo our stub so it doesn't affect the `include` matcher below. + allow(RSpec::Support::ObjectFormatter).to receive(:format).and_call_original + expect(message).to include("detailed inspect") + end + + it 'fails when given a block' do + expect { + expect { 2 + 2 }.to matcher + }.to fail_with(/must pass an argument rather than a block/) + + unless options[:disallows_negation] + expect { + expect { 2 + 2 }.not_to matcher + }.to fail_with(/must pass an argument rather than a block/) + end + end +end