rspec-expectations icon indicating copy to clipboard operation
rspec-expectations copied to clipboard

Regression in 3.13: custom matcher hash argument improperly converted to keyword args, results in `ArgumentError`

Open myronmarston opened this issue 1 year ago • 5 comments

Subject of the issue

My project has a custom matcher defined with an optional keyword arg. After upgrading to RSpec 3.13, I get an ArgumentError that indicates that RSpec is converting a hash argument into keyword args for some reason.

Your environment

  • Ruby version: 3.2.2
  • rspec-expectations version: 3.13.0

Steps to reproduce

Create a file named rspec_bug.rb with these contents:

require "bundler/inline"

gemfile do
  source "https://rubygems.org"
  gem "rspec", ENV.fetch("RSPEC_VERSION", "3.13.0")
end

require "rspec"
require "rspec/autorun"

puts "Ruby version: #{RUBY_VERSION}"
puts "RSpec::Expectations version: #{RSpec::Expectations::Version::STRING}"

RSpec::Matchers.define :match_against_with_optional_kwarg do |hash, optional_kwarg: true|
  match { |actual| true }
end

RSpec::Matchers.define :match_against_without_optional_kwarg do |hash|
  match { |actual| true }
end

RSpec.describe "A custom matcher" do
  it "can accept an optional kwarg" do
    expect(:something).to match_against_with_optional_kwarg({5 => "five", "some" => "hash", "of" => "data", [:foo] => 10})
  end

  it "can be used without an optional kwarg" do
    expect(:something).to match_against_without_optional_kwarg({5 => "five", "some" => "hash", "of" => "data", [:foo] => 10})
  end
end

Expected behavior

I expect this spec to pass, as it does on every recent version of RSpec:

 ~/code/ RSPEC_VERSION=3.8.0 ruby rspec_bug.rb -b
Ruby version: 3.2.2
RSpec::Expectations version: 3.8.6
..

Finished in 0.00132 seconds (files took 0.04301 seconds to load)
2 examples, 0 failures

 ~/code/ RSPEC_VERSION=3.9.0 ruby rspec_bug.rb -b
Ruby version: 3.2.2
RSpec::Expectations version: 3.9.4
..

Finished in 0.00194 seconds (files took 0.04708 seconds to load)
2 examples, 0 failures

 ~/code/ RSPEC_VERSION=3.10.0 ruby rspec_bug.rb -b
Ruby version: 3.2.2
RSpec::Expectations version: 3.10.2
..

Finished in 0.00181 seconds (files took 0.04384 seconds to load)
2 examples, 0 failures

 ~/code/ RSPEC_VERSION=3.11.0 ruby rspec_bug.rb -b
Ruby version: 3.2.2
RSpec::Expectations version: 3.11.1
..

Finished in 0.00135 seconds (files took 0.04207 seconds to load)
2 examples, 0 failures

 ~/code/ RSPEC_VERSION=3.12.0 ruby rspec_bug.rb -b
Ruby version: 3.2.2
RSpec::Expectations version: 3.12.3
..

Finished in 0.00166 seconds (files took 0.04384 seconds to load)
2 examples, 0 failures

Actual behavior

On RSpec 3.13, it fails with a very odd error:

 ~/code/ RSPEC_VERSION=3.13.0 ruby rspec_bug.rb -b
Ruby version: 3.2.2
RSpec::Expectations version: 3.13.0
F.

Failures:

  1) A custom matcher can accept an optional kwarg
     Failure/Error:
       RSpec::Matchers.define :match_against_with_optional_kwarg do |hash, optional_kwarg: true|
         match { |actual| true }
       end

     ArgumentError:
       unknown keywords: 5, "some", "of", [:foo]
     # rspec_bug.rb:14:in `block in <main>'
     # /Users/myron/.rvm/gems/ruby-3.2.2/gems/rspec-support-3.13.1/lib/rspec/support/with_keywords_when_needed.rb:20:in `class_exec'
     # /Users/myron/.rvm/gems/ruby-3.2.2/gems/rspec-support-3.13.1/lib/rspec/support/with_keywords_when_needed.rb:20:in `class_exec'
     # /Users/myron/.rvm/gems/ruby-3.2.2/gems/rspec-support-3.13.1/lib/rspec/support/with_keywords_when_needed.rb:19:in `eval'
     # /Users/myron/.rvm/gems/ruby-3.2.2/gems/rspec-support-3.13.1/lib/rspec/support/with_keywords_when_needed.rb:19:in `class_exec'
     # /Users/myron/.rvm/gems/ruby-3.2.2/gems/rspec-expectations-3.13.0/lib/rspec/matchers/dsl.rb:475:in `initialize'
     # /Users/myron/.rvm/gems/ruby-3.2.2/gems/rspec-expectations-3.13.0/lib/rspec/matchers/dsl.rb:76:in `new'
     # /Users/myron/.rvm/gems/ruby-3.2.2/gems/rspec-expectations-3.13.0/lib/rspec/matchers/dsl.rb:76:in `block in define'
     # rspec_bug.rb:24:in `block (2 levels) in <main>'
     # /Users/myron/.rvm/gems/ruby-3.2.2/gems/rspec-core-3.13.0/lib/rspec/core/example.rb:263:in `instance_exec'
     # /Users/myron/.rvm/gems/ruby-3.2.2/gems/rspec-core-3.13.0/lib/rspec/core/example.rb:263:in `block in run'
     # /Users/myron/.rvm/gems/ruby-3.2.2/gems/rspec-core-3.13.0/lib/rspec/core/example.rb:511:in `block in with_around_and_singleton_context_hooks'
     # /Users/myron/.rvm/gems/ruby-3.2.2/gems/rspec-core-3.13.0/lib/rspec/core/example.rb:468:in `block in with_around_example_hooks'
     # /Users/myron/.rvm/gems/ruby-3.2.2/gems/rspec-core-3.13.0/lib/rspec/core/hooks.rb:486:in `block in run'
     # /Users/myron/.rvm/gems/ruby-3.2.2/gems/rspec-core-3.13.0/lib/rspec/core/hooks.rb:624:in `run_around_example_hooks_for'
     # /Users/myron/.rvm/gems/ruby-3.2.2/gems/rspec-core-3.13.0/lib/rspec/core/hooks.rb:486:in `run'
     # /Users/myron/.rvm/gems/ruby-3.2.2/gems/rspec-core-3.13.0/lib/rspec/core/example.rb:468:in `with_around_example_hooks'
     # /Users/myron/.rvm/gems/ruby-3.2.2/gems/rspec-core-3.13.0/lib/rspec/core/example.rb:511:in `with_around_and_singleton_context_hooks'
     # /Users/myron/.rvm/gems/ruby-3.2.2/gems/rspec-core-3.13.0/lib/rspec/core/example.rb:259:in `run'
     # /Users/myron/.rvm/gems/ruby-3.2.2/gems/rspec-core-3.13.0/lib/rspec/core/example_group.rb:646:in `block in run_examples'
     # /Users/myron/.rvm/gems/ruby-3.2.2/gems/rspec-core-3.13.0/lib/rspec/core/example_group.rb:642:in `map'
     # /Users/myron/.rvm/gems/ruby-3.2.2/gems/rspec-core-3.13.0/lib/rspec/core/example_group.rb:642:in `run_examples'
     # /Users/myron/.rvm/gems/ruby-3.2.2/gems/rspec-core-3.13.0/lib/rspec/core/example_group.rb:607:in `run'
     # /Users/myron/.rvm/gems/ruby-3.2.2/gems/rspec-core-3.13.0/lib/rspec/core/runner.rb:121:in `block (3 levels) in run_specs'
     # /Users/myron/.rvm/gems/ruby-3.2.2/gems/rspec-core-3.13.0/lib/rspec/core/runner.rb:121:in `map'
     # /Users/myron/.rvm/gems/ruby-3.2.2/gems/rspec-core-3.13.0/lib/rspec/core/runner.rb:121:in `block (2 levels) in run_specs'
     # /Users/myron/.rvm/gems/ruby-3.2.2/gems/rspec-core-3.13.0/lib/rspec/core/configuration.rb:2091:in `with_suite_hooks'
     # /Users/myron/.rvm/gems/ruby-3.2.2/gems/rspec-core-3.13.0/lib/rspec/core/runner.rb:116:in `block in run_specs'
     # /Users/myron/.rvm/gems/ruby-3.2.2/gems/rspec-core-3.13.0/lib/rspec/core/reporter.rb:74:in `report'
     # /Users/myron/.rvm/gems/ruby-3.2.2/gems/rspec-core-3.13.0/lib/rspec/core/runner.rb:115:in `run_specs'
     # /Users/myron/.rvm/gems/ruby-3.2.2/gems/rspec-core-3.13.0/lib/rspec/core/runner.rb:89:in `run'
     # /Users/myron/.rvm/gems/ruby-3.2.2/gems/rspec-core-3.13.0/lib/rspec/core/runner.rb:71:in `run'
     # /Users/myron/.rvm/gems/ruby-3.2.2/gems/rspec-core-3.13.0/lib/rspec/core/runner.rb:45:in `invoke'
     # /Users/myron/.rvm/gems/ruby-3.2.2/gems/rspec-core-3.13.0/lib/rspec/core/runner.rb:38:in `perform_at_exit'
     # /Users/myron/.rvm/gems/ruby-3.2.2/gems/rspec-core-3.13.0/lib/rspec/core/runner.rb:24:in `block in autorun'

Finished in 0.00177 seconds (files took 0.04443 seconds to load)
2 examples, 1 failure

Failed examples:

rspec rspec_bug.rb:23 # A custom matcher can accept an optional kwarg

The error (ArgumentError: unknown keywords: 5, "some", "of", [:foo]) indicates that RSpec is somehow converting my hash of data into keyword args, even though the keys aren't valid for keyword args, and it's just a hash of data being passed as a positional arg.

This bug appears to be triggered by the presence of an optional keyword arg on the definition of the matcher itself.

myronmarston avatar Feb 24 '24 05:02 myronmarston