From fdc2d26d8bb18095acb51fe62af4d0ae01fd9d2c Mon Sep 17 00:00:00 2001 From: Benjamin Clayman Date: Wed, 13 Oct 2021 10:50:39 -0400 Subject: [PATCH] Update StartWith, EndWith to use native methods if available This addresses issue #1025. With this change, the StartWith matcher will rely on an object's start_with? method if available. Similarly, the EndWith matcher will rely on an object's end_with? method if available. This is especially useful when a class implements start_with? but not the indexing operator, or end_with? but not the indexing operator. --- .../matchers/built_in/start_or_end_with.rb | 15 +++++- .../built_in/start_and_end_with_spec.rb | 47 +++++++++++++++---- 2 files changed, 50 insertions(+), 12 deletions(-) diff --git a/lib/rspec/matchers/built_in/start_or_end_with.rb b/lib/rspec/matchers/built_in/start_or_end_with.rb index 81f06c288..53a22ebc1 100644 --- a/lib/rspec/matchers/built_in/start_or_end_with.rb +++ b/lib/rspec/matchers/built_in/start_or_end_with.rb @@ -13,11 +13,12 @@ def initialize(*expected) # @api private # @return [String] def failure_message + response_msg = ", but it does not respond to #{method} and cannot be indexed using #[]" super.tap do |msg| if @actual_does_not_have_ordered_elements msg << ", but it does not have ordered elements" elsif !actual.respond_to?(:[]) - msg << ", but it cannot be indexed using #[]" + msg << response_msg end end end @@ -33,7 +34,9 @@ def description private - def match(_expected, actual) + def match(expected, actual) + # use an object's start_with? or end_with? as appropriate + return actual.send(method, expected) if actual.respond_to?(method) return false unless actual.respond_to?(:[]) begin @@ -73,6 +76,10 @@ def subset_matches? def element_matches? values_match?(expected, actual[0]) end + + def method + :start_with? + end end # @api private @@ -88,6 +95,10 @@ def subset_matches? def element_matches? values_match?(expected, actual[-1]) end + + def method + :end_with? + end end end 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 b063f199d..02bed68c9 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 @@ -102,11 +102,25 @@ def ==(other) end context "with an object that does not respond to :[]" do - it "fails with a useful message" do - actual = Object.new - expect { - expect(actual).to start_with 0 - }.to fail_with("expected #{actual.inspect} to start with 0, but it cannot be indexed using #[]") + context "with an object that responds to start_with?" do + it "relies on start_with?" do + my_struct = Struct.new(:foo) do + def start_with?(elem) + true + end + end + + expect(my_struct.new("foo")).to start_with(0) + end + end + + context "with an object that does not respond to start_with?" do + it "fails with a useful message" do + actual = Object.new + expect { + expect(actual).to start_with 0 + }.to fail_with("expected #{actual.inspect} to start with 0, but it does not respond to start_with? and cannot be indexed using #[]") + end end end @@ -310,11 +324,24 @@ def ==(other) end context "with an object that does not respond to :[]" do - it "fails with a useful message" do - actual = Object.new - expect { - expect(actual).to end_with 0 - }.to fail_with("expected #{actual.inspect} to end with 0, but it cannot be indexed using #[]") + context "with an object that responds to end_with?" do + it "relies on end_with?" do + my_struct = Struct.new(:foo) do + def end_with?(elem) + true + end + end + expect(my_struct.new("foo")).to end_with(0) + end + end + + context "with an object that does not respond to end_with?" do + it "fails with a useful message" do + actual = Object.new + expect { + expect(actual).to end_with 0 + }.to fail_with("expected #{actual.inspect} to end with 0, but it does not respond to end_with? and cannot be indexed using #[]") + end end end