rspec / rspec-rails

RSpec for Rails 6+
https://rspec.info
MIT License
5.14k stars 1.03k forks source link

Railtie inclusion fails but only from RSpec. (undefined method '+' for class 'Date') #2772

Closed hakunin closed 3 days ago

hakunin commented 4 days ago

What Ruby, Rails and RSpec versions are you using?

Ruby version: 2.7.6 (confirmed also fails under 2.7.8) Rails version: 6.1.5 RSpec version: 3.13.0

Observed behaviour

I just added rspec-rails to an existing project which previously used minitest. Throws an error during require rails_helper but deep from within railties.

Expected behaviour

Not an error.

Can you provide an example reproduction?

Going to work on a repro this week.

This is a bit of a shot in the dark, but I wanted to file this as I didn't even find any mention of this error on the web.

The error comes from the second line config/application.rb, which contains:

require_relative 'boot' # commenting this out does not change anything
require 'rails/all' # this is where it fail from

The error happens before any gems (apart from rspec-core and rspec) are loaded, so no initializers are run either. I tried the latest ruby 2.7.8 patch version and still getting the same error. This could potentially be just an issue with the order of files being loaded? Hoping someone who had a similar issue sees this, I am going to try to reproduce this during this week/end.

➜ DISABLE_SPRING=1 rspec --backtrace

An error occurred while loading ./spec/services/company_queries/earnings_spec.rb.
Failure/Error: require_relative '../config/environment'

NameError:
  undefined method `+' for class `Date'
# ./.bundle/ruby/2.7.0/gems/activesupport-6.1.5/lib/active_support/core_ext/date/calculations.rb:97:in `alias_method'
# ./.bundle/ruby/2.7.0/gems/activesupport-6.1.5/lib/active_support/core_ext/date/calculations.rb:97:in `<class:Date>'
# ./.bundle/ruby/2.7.0/gems/activesupport-6.1.5/lib/active_support/core_ext/date/calculations.rb:10:in `<top (required)>'
# ./.bundle/ruby/2.7.0/gems/activesupport-6.1.5/lib/active_support/dependencies.rb:332:in `require'
# ./.bundle/ruby/2.7.0/gems/activesupport-6.1.5/lib/active_support/dependencies.rb:332:in `block in require'
# ./.bundle/ruby/2.7.0/gems/activesupport-6.1.5/lib/active_support/dependencies.rb:299:in `load_dependency'
# ./.bundle/ruby/2.7.0/gems/activesupport-6.1.5/lib/active_support/dependencies.rb:332:in `require'
# ./.bundle/ruby/2.7.0/gems/activesupport-6.1.5/lib/active_support/core_ext/time/calculations.rb:8:in `<top (required)>'
# ./.bundle/ruby/2.7.0/gems/activesupport-6.1.5/lib/active_support/dependencies.rb:332:in `require'
# ./.bundle/ruby/2.7.0/gems/activesupport-6.1.5/lib/active_support/dependencies.rb:332:in `block in require'
# ./.bundle/ruby/2.7.0/gems/activesupport-6.1.5/lib/active_support/dependencies.rb:299:in `load_dependency'
# ./.bundle/ruby/2.7.0/gems/activesupport-6.1.5/lib/active_support/dependencies.rb:332:in `require'
# ./.bundle/ruby/2.7.0/gems/activesupport-6.1.5/lib/active_support/core_ext/numeric/time.rb:4:in `<top (required)>'
# ./.bundle/ruby/2.7.0/gems/activesupport-6.1.5/lib/active_support/dependencies.rb:332:in `require'
# ./.bundle/ruby/2.7.0/gems/activesupport-6.1.5/lib/active_support/dependencies.rb:332:in `block in require'
# ./.bundle/ruby/2.7.0/gems/activesupport-6.1.5/lib/active_support/dependencies.rb:299:in `load_dependency'
# ./.bundle/ruby/2.7.0/gems/activesupport-6.1.5/lib/active_support/dependencies.rb:332:in `require'
# ./.bundle/ruby/2.7.0/gems/activesupport-6.1.5/lib/active_support/core_ext/integer/time.rb:4:in `<top (required)>'
# ./.bundle/ruby/2.7.0/gems/activesupport-6.1.5/lib/active_support/dependencies.rb:332:in `require'
# ./.bundle/ruby/2.7.0/gems/activesupport-6.1.5/lib/active_support/dependencies.rb:332:in `block in require'
# ./.bundle/ruby/2.7.0/gems/activesupport-6.1.5/lib/active_support/dependencies.rb:299:in `load_dependency'
# ./.bundle/ruby/2.7.0/gems/activesupport-6.1.5/lib/active_support/dependencies.rb:332:in `require'
# ./.bundle/ruby/2.7.0/gems/globalid-1.0.0/lib/global_id/railtie.rb:8:in `<top (required)>'
# ./.bundle/ruby/2.7.0/gems/activesupport-6.1.5/lib/active_support/dependencies.rb:332:in `require'
# ./.bundle/ruby/2.7.0/gems/activesupport-6.1.5/lib/active_support/dependencies.rb:332:in `block in require'
# ./.bundle/ruby/2.7.0/gems/activesupport-6.1.5/lib/active_support/dependencies.rb:299:in `load_dependency'
# ./.bundle/ruby/2.7.0/gems/activesupport-6.1.5/lib/active_support/dependencies.rb:332:in `require'
# ./.bundle/ruby/2.7.0/gems/activejob-6.1.5/lib/active_job/railtie.rb:3:in `<top (required)>'
# ./.bundle/ruby/2.7.0/gems/activesupport-6.1.5/lib/active_support/dependencies.rb:332:in `require'
# ./.bundle/ruby/2.7.0/gems/activesupport-6.1.5/lib/active_support/dependencies.rb:332:in `block in require'
# ./.bundle/ruby/2.7.0/gems/activesupport-6.1.5/lib/active_support/dependencies.rb:299:in `load_dependency'
# ./.bundle/ruby/2.7.0/gems/activesupport-6.1.5/lib/active_support/dependencies.rb:332:in `require'
# ./.bundle/ruby/2.7.0/gems/activestorage-6.1.5/lib/active_storage/engine.rb:5:in `<top (required)>'
# ./.bundle/ruby/2.7.0/gems/activesupport-6.1.5/lib/active_support/dependencies.rb:332:in `require'
# ./.bundle/ruby/2.7.0/gems/activesupport-6.1.5/lib/active_support/dependencies.rb:332:in `block in require'
# ./.bundle/ruby/2.7.0/gems/activesupport-6.1.5/lib/active_support/dependencies.rb:299:in `load_dependency'
# ./.bundle/ruby/2.7.0/gems/activesupport-6.1.5/lib/active_support/dependencies.rb:332:in `require'
# ./.bundle/ruby/2.7.0/gems/railties-6.1.5/lib/rails/all.rb:21:in `block in <top (required)>'
# ./.bundle/ruby/2.7.0/gems/railties-6.1.5/lib/rails/all.rb:19:in `each'
# ./.bundle/ruby/2.7.0/gems/railties-6.1.5/lib/rails/all.rb:19:in `<top (required)>'
# ./config/application.rb:2:in `require'
# ./config/application.rb:2:in `<top (required)>'
# ./config/environment.rb:2:in `require_relative'
# ./config/environment.rb:2:in `<top (required)>'
# ./spec/rails_helper.rb:5:in `require_relative'
# ./spec/rails_helper.rb:5:in `<top (required)>'
# ./spec/services/company_queries/earnings_spec.rb:16:in `require'
# ./spec/services/company_queries/earnings_spec.rb:16:in `<top (required)>'
# ./.bundle/ruby/2.7.0/gems/rspec-core-3.13.0/lib/rspec/core/configuration.rb:2138:in `load'
# ./.bundle/ruby/2.7.0/gems/rspec-core-3.13.0/lib/rspec/core/configuration.rb:2138:in `load_file_handling_errors'
# ./.bundle/ruby/2.7.0/gems/rspec-core-3.13.0/lib/rspec/core/configuration.rb:1638:in `block in load_spec_files'
# ./.bundle/ruby/2.7.0/gems/rspec-core-3.13.0/lib/rspec/core/configuration.rb:1636:in `each'
# ./.bundle/ruby/2.7.0/gems/rspec-core-3.13.0/lib/rspec/core/configuration.rb:1636:in `load_spec_files'
# ./.bundle/ruby/2.7.0/gems/rspec-core-3.13.0/lib/rspec/core/runner.rb:102:in `setup'
# ./.bundle/ruby/2.7.0/gems/rspec-core-3.13.0/lib/rspec/core/runner.rb:86:in `run'
# ./.bundle/ruby/2.7.0/gems/rspec-core-3.13.0/lib/rspec/core/runner.rb:71:in `run'
# ./.bundle/ruby/2.7.0/gems/rspec-core-3.13.0/lib/rspec/core/runner.rb:45:in `invoke'
# ./.bundle/ruby/2.7.0/gems/rspec-core-3.13.0/exe/rspec:4:in `<top (required)>'
# ./bin/rspec:27:in `load'
# ./bin/rspec:27:in `<main>'
No examples found.

Finished in 0.00002 seconds (files took 0.57334 seconds to load)
0 examples, 0 failures, 1 error occurred outside of examples
pirj commented 4 days ago

That’s weird. On 2.7.8 in irb -f after require 'date' I can Date.today + 1. Can you? Wondering what you’ll be able to find.

pirj commented 4 days ago

Just for the reference https://github.com/rails/rails/blob/53410537594be0c0a528cff53dce433dd386cb6a/activesupport/lib/active_support/core_ext/date/calculations.rb#L97

hakunin commented 4 days ago

irb -f

Yep, works fine in irb:

➜ irb -f
irb(main):001:0> require 'date'
=> true
irb(main):002:0> Date.today + 1
=> #<Date: 2024-07-02 ((2460494j,0s,0n),+0s,2299161j)>

The error happens at:

  alias_method :plus_without_duration, :+ # <-- here
  alias_method :+, :plus_with_duration

Which is strange.

hakunin commented 3 days ago

Grepped through our gems, disabled all the gems that extend the Date class, but didn't help. Checked if the file where it fails is being loaded twice - and it isn't :upside_down_face:

pirj commented 3 days ago

Can you set a binding.irb breakpoint just before that alias_method to see if new + 1 works?

hakunin commented 3 days ago

Can you set a binding.irb breakpoint just before that alias_method to see if new + 1 works?

Yes, it fails on the missing plus.

I grepped the whole codebase and gems for the alias_method with a :+ and the only thing I am getting is:

grep -r "alias_method :+" . -n
./.bundle/ruby/2.7.0/gems/nokogiri-1.15.5-x86_64-linux/lib/nokogiri/xml/node_set.rb:431:      alias_method :+, :|
./.bundle/ruby/2.7.0/gems/activesupport-6.1.5/lib/active_support/core_ext/time/calculations.rb:289:  alias_method :+, :plus_with_duration
./.bundle/ruby/2.7.0/gems/activesupport-6.1.5/lib/active_support/core_ext/date/calculations.rb:101:  alias_method :+, :plus_with_duration
./.bundle/ruby/2.7.0/gems/xpath-3.2.0/lib/xpath/dsl.rb:65:    alias_method :+, :union
./.bundle/ruby/2.7.0/gems/addressable-2.8.5/lib/addressable/uri.rb:1971:    alias_method :+, :join

So what's happening doesn't make a lot of sense. I checked RUBY_VERSION => "2.7.6"

Solution

Finally, I found out that we had a lib/date.rb file, which somehow loaded the first time require 'date' was run. (even before railties?)

But this only happens when running rspec, not when the rails app is run. Thank you so much for helping me figure this out!

hakunin commented 3 days ago

Turns out the project I work on has lib/<gem>/<ext-file-with-same-name-as-original>.rb in a few places even. And Rails has no problem with it, because it's only loaded explicitly from initializers. Makes me wonder, what is the difference that makes RSpec load these files specifically from require statements?

pirj commented 3 days ago

Got it. Happy to have helped. Is there anything actionable left?

JonRowe commented 3 days ago

Makes me wonder, what is the difference that makes RSpec load these files specifically from require statements?

RSpec adds ./lib and (a configurable) spec path to the load path, this allows people working on normal ruby project to do require 'name' in specs, and similarly allows require 'spec_helper' to work.

This convention is sometimes also used for monkey patching as you say like lib/<gem>/<ext-file-with-same-name-as-original>.rb to override or backport a fix, which typically isn't an issue either, as these files usually have the same contents as the original.

If it works from Rails, I take it you were requiring relatively or with full path names? As e.g. require 'date' will only work once. Personally I would avoid shaddowing file names unless you are doing a full monkey patch with original contents, to ensure it will work reliably across tools.