From 771da6d491cd4bf66c3e3d91e68f253b3408af41 Mon Sep 17 00:00:00 2001 From: Phil Pirozhkov Date: Fri, 26 Jul 2019 19:27:41 +0300 Subject: [PATCH] Prevent block-only matchers from being used with value expectation target 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 Ruby 1.8-specific workarounds: multiple values for a block parameter (0 for 1) If a block expects an argument, it ought to be provided an argument in 1.8 --- Changelog.md | 7 +- lib/rspec/expectations/expectation_target.rb | 50 ++++++-- lib/rspec/expectations/syntax.rb | 2 +- lib/rspec/matchers/built_in/base_matcher.rb | 5 + lib/rspec/matchers/built_in/change.rb | 25 ++-- lib/rspec/matchers/built_in/compound.rb | 14 +++ lib/rspec/matchers/built_in/output.rb | 9 +- lib/rspec/matchers/built_in/raise_error.rb | 7 +- lib/rspec/matchers/built_in/throw_symbol.rb | 9 +- lib/rspec/matchers/built_in/yield.rb | 50 ++++---- lib/rspec/matchers/dsl.rb | 4 + lib/rspec/matchers/matcher_protocol.rb | 6 + .../expectations/extensions/kernel_spec.rb | 4 +- 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 | 110 ------------------ spec/support/shared_examples/block_matcher.rb | 80 +++++++++++++ spec/support/shared_examples/matcher.rb | 44 +++++++ spec/support/shared_examples/value_matcher.rb | 66 +++++++++++ 46 files changed, 460 insertions(+), 233 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 e4e78a46a..20d8b52ed 100644 --- a/Changelog.md +++ b/Changelog.md @@ -15,6 +15,11 @@ Bug Fixes: * The `change` matcher now recognises an object has changed when its instance attributes have changed. (Jon Rowe, #1132) +Bug Fixes: + +* Prevent unsupported implicit block expectation syntax from being used. + (Phil Pirozhkov, #1125) + ### 3.8.4 / 2019-06-10 [Full Changelog](http://github.com/rspec/rspec-expectations/compare/v3.8.3...v3.8.4) @@ -31,7 +36,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..77d17bc91 100644 --- a/lib/rspec/expectations/expectation_target.rb +++ b/lib/rspec/expectations/expectation_target.rb @@ -33,16 +33,16 @@ def initialize(value) end # @private - def self.for(value, block) + def self.for(value, &block) if UndefinedValue.equal?(value) - unless block + unless block_given? raise ArgumentError, "You must pass either an argument or a block to `expect`." end BlockExpectationTarget.new(block) - elsif block + elsif block_given? raise ArgumentError, "You cannot pass both an argument and a block to `expect`." else - new(value) + ValueExpectationTarget.new(value) end end @@ -90,6 +90,40 @@ 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) + + raise ExpectationNotMetError, "You must pass a block rather than an argument to `expect` to use the provided " \ + "block expectation matcher (#{RSpec::Support::ObjectFormatter.format(matcher)})." + end + + def supports_value_expectations?(matcher) + if matcher.respond_to?(:supports_value_expectations?) + matcher.supports_value_expectations? + else + true + end + end + end + # @private # Validates the provided matcher to ensure it supports block # expectations, in order to avoid user confusion when they @@ -118,9 +152,11 @@ def enforce_block_expectation(matcher) end def supports_block_expectations?(matcher) - matcher.supports_block_expectations? - rescue NoMethodError - false + if matcher.respond_to?(:supports_block_expectations?) + matcher.supports_block_expectations? + else + false + end end end end diff --git a/lib/rspec/expectations/syntax.rb b/lib/rspec/expectations/syntax.rb index b8430346f..3906d8520 100644 --- a/lib/rspec/expectations/syntax.rb +++ b/lib/rspec/expectations/syntax.rb @@ -70,7 +70,7 @@ def enable_expect(syntax_host=::RSpec::Matchers) syntax_host.module_exec do def expect(value=::RSpec::Expectations::ExpectationTarget::UndefinedValue, &block) - ::RSpec::Expectations::ExpectationTarget.for(value, block) + ::RSpec::Expectations::ExpectationTarget.for(value, &block) end end end diff --git a/lib/rspec/matchers/built_in/base_matcher.rb b/lib/rspec/matchers/built_in/base_matcher.rb index 7699c92ac..ec957be9d 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 bb68a43b6..44ac7f07d 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) @@ -107,12 +112,10 @@ def raise_block_syntax_error end def positive_failure_reason - return "was not given a block" unless Proc === @event_proc "is still #{@actual_before_description}" end def negative_failure_reason - return "was not given a block" unless Proc === @event_proc "did change from #{@actual_before_description} " \ "to #{description_of change_details.actual_after}" end @@ -158,10 +161,14 @@ def supports_block_expectations? true end + # @private + def supports_value_expectations? + false + end + private def failure_reason - return "was not given a block" unless Proc === @event_proc "was changed by #{description_of @change_details.actual_delta}" end end @@ -190,7 +197,6 @@ def description # @private def failure_message - return not_given_a_block_failure unless Proc === @event_proc return before_value_failure unless @matches_before return did_not_change_failure unless @change_details.changed? after_value_failure @@ -201,6 +207,11 @@ def supports_block_expectations? true end + # @private + def supports_value_expectations? + false + end + private def perform_change(event_proc) @@ -242,11 +253,6 @@ def did_change_failure "did change from #{@actual_before_description} " \ "to #{description_of @change_details.actual_after}" end - - def not_given_a_block_failure - "expected #{@change_details.value_representation} to have changed " \ - "#{change_description}, but was not given a block" - end end # @api private @@ -278,7 +284,6 @@ def does_not_match?(event_proc) # @private def failure_message_when_negated - return not_given_a_block_failure unless Proc === @event_proc return before_value_failure unless @matches_before did_change_failure end 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..449e44b54 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? @@ -101,13 +108,11 @@ def captured? end def positive_failure_reason - return "was not a block" unless Proc === @block return "output #{actual_output_description}" if @expected "did not" end def negative_failure_reason - return "was not a block" unless Proc === @block "output #{actual_output_description}" end diff --git a/lib/rspec/matchers/built_in/raise_error.rb b/lib/rspec/matchers/built_in/raise_error.rb index 6427d030a..0136e2603 100644 --- a/lib/rspec/matchers/built_in/raise_error.rb +++ b/lib/rspec/matchers/built_in/raise_error.rb @@ -76,6 +76,12 @@ def supports_block_expectations? true end + # @private + def supports_value_expectations? + false + end + + # @private def expects_call_stack_jump? true end @@ -199,7 +205,6 @@ def format_backtrace(backtrace) end def given_error - return " but was not given a block" unless Proc === @given_proc return " but nothing was raised" unless @actual_error backtrace = format_backtrace(@actual_error.backtrace) diff --git a/lib/rspec/matchers/built_in/throw_symbol.rb b/lib/rspec/matchers/built_in/throw_symbol.rb index 1b6b8bcb4..e94792a1f 100644 --- a/lib/rspec/matchers/built_in/throw_symbol.rb +++ b/lib/rspec/matchers/built_in/throw_symbol.rb @@ -88,12 +88,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 @@ -101,7 +105,6 @@ def expects_call_stack_jump? private def actual_result - return "but was not a block" unless Proc === @block "got #{caught}" end diff --git a/lib/rspec/matchers/built_in/yield.rb b/lib/rspec/matchers/built_in/yield.rb index 929fef1ec..cac2bb433 100644 --- a/lib/rspec/matchers/built_in/yield.rb +++ b/lib/rspec/matchers/built_in/yield.rb @@ -10,7 +10,6 @@ module BuiltIn class YieldProbe def self.probe(block, &callback) probe = new(block, &callback) - return probe unless probe.has_block? probe.probe end @@ -24,10 +23,6 @@ def initialize(block, &callback) self.yielded_args = [] end - def has_block? - Proc === @block - end - def probe assert_valid_expect_block! @block.call(self) @@ -152,14 +147,12 @@ def times # @private def matches?(block) @probe = YieldProbe.probe(block) - return false unless @probe.has_block? - @probe.num_yields.__send__(@expectation_type, @expected_yields_count) end # @private def does_not_match?(block) - !matches?(block) && @probe.has_block? + !matches?(block) end # @api private @@ -179,6 +172,11 @@ def supports_block_expectations? true end + # @private + def supports_value_expectations? + false + end + private def set_expected_yields_count(relativity, n) @@ -192,7 +190,6 @@ def set_expected_yields_count(relativity, n) end def failure_reason - return ' but was not a block' unless @probe.has_block? return '' unless @expected_yields_count " #{human_readable_expectation_type}#{human_readable_count(@expected_yields_count)}" \ " but yielded #{human_readable_count(@probe.num_yields)}" @@ -222,13 +219,12 @@ class YieldWithNoArgs < BaseMatcher # @private def matches?(block) @probe = YieldProbe.probe(block) - return false unless @probe.has_block? @probe.yielded_once?(:yield_with_no_args) && @probe.single_yield_args.empty? end # @private def does_not_match?(block) - !matches?(block) && @probe.has_block? + !matches?(block) end # @private @@ -246,16 +242,19 @@ def supports_block_expectations? true end + # @private + def supports_value_expectations? + false + end + private def positive_failure_reason - return 'was not a block' unless @probe.has_block? return 'did not yield' if @probe.num_yields.zero? "yielded with arguments: #{description_of @probe.single_yield_args}" end def negative_failure_reason - return 'was not a block' unless @probe.has_block? 'did' end end @@ -276,14 +275,13 @@ def matches?(block) @actual_formatted = actual_formatted @args_matched_when_yielded &&= args_currently_match? end - return false unless @probe.has_block? @probe.probe @probe.yielded_once?(:yield_with_args) && @args_matched_when_yielded end # @private def does_not_match?(block) - !matches?(block) && @probe.has_block? + !matches?(block) end # @private @@ -308,10 +306,14 @@ def supports_block_expectations? true end + # @private + def supports_value_expectations? + false + end + private def positive_failure_reason - return 'was not a block' unless @probe.has_block? return 'did not yield' if @probe.num_yields.zero? @positive_args_failure end @@ -321,9 +323,7 @@ def expected_arg_description end def negative_failure_reason - if !@probe.has_block? - 'was not a block' - elsif @args_matched_when_yielded && !@expected.empty? + if @args_matched_when_yielded && !@expected.empty? 'yielded with expected arguments' \ "\nexpected not: #{surface_descriptions_in(@expected).inspect}" \ "\n got: #{@actual_formatted}" @@ -375,12 +375,11 @@ def matches?(block) yield_count += 1 end - return false unless @probe.has_block? args_matched_when_yielded && yield_count == @expected.length end def does_not_match?(block) - !matches?(block) && @probe.has_block? + !matches?(block) end # @private @@ -405,6 +404,11 @@ def supports_block_expectations? true end + # @private + def supports_value_expectations? + false + end + private def expected_arg_description @@ -412,16 +416,12 @@ def expected_arg_description end def positive_failure_reason - return 'was not a block' unless @probe.has_block? - 'yielded with unexpected arguments' \ "\nexpected: #{surface_descriptions_in(@expected).inspect}" \ "\n got: [#{@actual_formatted.join(", ")}]" end def negative_failure_reason - return 'was not a block' unless @probe.has_block? - 'yielded with expected arguments' \ "\nexpected not: #{surface_descriptions_in(@expected).inspect}" \ "\n got: [#{@actual_formatted.join(", ")}]" diff --git a/lib/rspec/matchers/dsl.rb b/lib/rspec/matchers/dsl.rb index f185d9117..df5c49c0f 100644 --- a/lib/rspec/matchers/dsl.rb +++ b/lib/rspec/matchers/dsl.rb @@ -401,6 +401,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/expectations/extensions/kernel_spec.rb b/spec/rspec/expectations/extensions/kernel_spec.rb index 00c68a8c4..a57e7de1c 100644 --- a/spec/rspec/expectations/extensions/kernel_spec.rb +++ b/spec/rspec/expectations/extensions/kernel_spec.rb @@ -1,7 +1,7 @@ RSpec.describe Object, "#should" do before(:example) do @target = "target" - @matcher = double("matcher") + @matcher = double("matcher", :supports_value_expectations? => true) allow(@matcher).to receive(:matches?).and_return(true) allow(@matcher).to receive(:failure_message) end @@ -59,7 +59,7 @@ def method_missing(name, *args) RSpec.describe Object, "#should_not" do before(:example) do @target = "target" - @matcher = double("matcher") + @matcher = double("matcher", :supports_value_expectations? => true) end it "accepts and interacts with a matcher" do diff --git a/spec/rspec/matchers/aliased_matcher_spec.rb b/spec/rspec/matchers/aliased_matcher_spec.rb index 0902ecfeb..612f3a4d5 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 72ca624cb..ecf8b86e9 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 73752d3f8..29e185654 100644 --- a/spec/rspec/matchers/built_in/change_spec.rb +++ b/spec/rspec/matchers/built_in/change_spec.rb @@ -1048,39 +1048,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 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 48cc4576f..fd7886d03 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 fb740f7dd..8a6b58836 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 f97b64b98..f92680c3f 100644 --- a/spec/rspec/matchers/built_in/have_attributes_spec.rb +++ b/spec/rspec/matchers/built_in/have_attributes_spec.rb @@ -22,7 +22,7 @@ def parent(parent_name) 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 @@ -132,7 +132,7 @@ def count 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 7179e1529..da1fe6c90 100644 --- a/spec/rspec/matchers/built_in/include_spec.rb +++ b/spec/rspec/matchers/built_in/include_spec.rb @@ -103,7 +103,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 1f88ef69d..4373d4489 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 55e0904c1..6929bf9d2 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 4ee1b0ce3..5837dd3b1 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 ec9523c40..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 1b70c8c8f..d6fe1ef33 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", *options 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 @@ -202,10 +223,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 @@ -285,10 +310,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 @@ -545,10 +574,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 d34f2ee87..4063df930 100644 --- a/spec/rspec/matchers/dsl_spec.rb +++ b/spec/rspec/matchers/dsl_spec.rb @@ -143,7 +143,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 a6e7e1b0f..681704fb0 100644 --- a/spec/rspec/matchers_spec.rb +++ b/spec/rspec/matchers_spec.rb @@ -119,8 +119,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 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 3173e3106..6b4efa71a 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -5,7 +5,7 @@ minimum_coverage 97 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 ce50cdba9..000000000 --- a/spec/support/shared_examples.rb +++ /dev/null @@ -1,110 +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..1e3c4207c --- /dev/null +++ b/spec/support/shared_examples/block_matcher.rb @@ -0,0 +1,80 @@ +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 when given a value' do + expect { + expect(3).to matcher + }.to fail_with(/must pass a block rather than an argument/) + + unless options[:disallows_negation] + expect { + expect(3).not_to matcher + }.to fail_with(/must pass a block rather than an argument/) + end + 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