Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mutex cannot be mocked since 3.9.0 (Stack level too deep) #1574

Open
sergio-bobillier opened this issue Apr 16, 2024 · 7 comments
Open

Mutex cannot be mocked since 3.9.0 (Stack level too deep) #1574

sergio-bobillier opened this issue Apr 16, 2024 · 7 comments

Comments

@sergio-bobillier
Copy link

sergio-bobillier commented Apr 16, 2024

Mutex cannot be mocked since

Since rspec 3.9.0 mocks of the Mutex class result in a SystemStackError: stack level too deep. Error. I unearthed these two issues:

And I thought this had already been fixed and it was something related to my setup or the combination of gems in my project but actually the issue is still there. Reproducible with Ruby 2.x and 3.x

Your environment

  • Tested with: 3.2.2 and 2.7.7
  • rspec-mocks version: Tested with 3.9.0 all the way up to 3.13.0 the issue is visible in all of them.

Steps to reproduce

I created this simple project to show the issue:

writer.rb:

# frozen_string_literal: true

class Writer
  def initialize
    @mutex = Mutex.new
    @output = StringIO.new
  end

  def write(message)
    mutex.synchronize do
      output << message
    end
  end

  private

  attr_reader :mutex
end

spec/writer_spec.rb:

# frozen_string_literal: true

require_relative '../writer'

RSpec.describe Writer do
  subject(:writer) { described_class.new }

  let(:mocked_mutex) do
    instance_double(
      Mutex
    )
  end

  before do
    allow(Mutex).to receive(:new).and_return(mocked_mutex)
    allow(mocked_mutex).to receive(:synchronize).and_yield
  end

  describe '#initialize' do
    it 'creates a new Mutex' do
      expect(Mutex).to receive(:new)
      writer
    end
  end
end

Expected behavior

I would expect my test to pass (like it does with RSpec 3.8.0)

.

Finished in 0.01108 seconds (files took 0.10239 seconds to load)
1 example, 0 failures

Actual behavior

I'm getting the mentioned SystemStackError: stack level too deep with such a backtrace:

     # /Users/sergio/.rbenv/versions/3.2.2/lib/ruby/gems/3.2.0/gems/rspec-mocks-3.13.0/lib/rspec/mocks/method_reference.rb:117:in `method_implemented?'
     # /Users/sergio/.rbenv/versions/3.2.2/lib/ruby/gems/3.2.0/gems/rspec-mocks-3.13.0/lib/rspec/mocks/method_reference.rb:48:in `block in defined?'
     # /Users/sergio/.rbenv/versions/3.2.2/lib/ruby/gems/3.2.0/gems/rspec-mocks-3.13.0/lib/rspec/mocks/object_reference.rb:138:in `when_loaded'
     # /Users/sergio/.rbenv/versions/3.2.2/lib/ruby/gems/3.2.0/gems/rspec-mocks-3.13.0/lib/rspec/mocks/method_reference.rb:47:in `defined?'
     # /Users/sergio/.rbenv/versions/3.2.2/lib/ruby/gems/3.2.0/gems/rspec-mocks-3.13.0/lib/rspec/mocks/method_reference.rb:107:in `block in original_method'
     # /Users/sergio/.rbenv/versions/3.2.2/lib/ruby/gems/3.2.0/gems/rspec-mocks-3.13.0/lib/rspec/mocks/object_reference.rb:138:in `when_loaded'
     # /Users/sergio/.rbenv/versions/3.2.2/lib/ruby/gems/3.2.0/gems/rspec-mocks-3.13.0/lib/rspec/mocks/method_reference.rb:106:in `original_method'
     # /Users/sergio/.rbenv/versions/3.2.2/lib/ruby/gems/3.2.0/gems/rspec-mocks-3.13.0/lib/rspec/mocks/method_reference.rb:53:in `with_signature'
     # /Users/sergio/.rbenv/versions/3.2.2/lib/ruby/gems/3.2.0/gems/rspec-mocks-3.13.0/lib/rspec/mocks/verifying_proxy.rb:166:in `validate_arguments!'

    ... repeats meny hundred times ...

     # /Users/sergio/.rbenv/versions/3.2.2/lib/ruby/gems/3.2.0/gems/rspec-mocks-3.13.0/lib/rspec/mocks/proxy.rb:199:in `message_received'
     # /Users/sergio/.rbenv/versions/3.2.2/lib/ruby/gems/3.2.0/gems/rspec-mocks-3.13.0/lib/rspec/mocks/method_double.rb:98:in `proxy_method_invoked'
     # /Users/sergio/.rbenv/versions/3.2.2/lib/ruby/gems/3.2.0/gems/rspec-mocks-3.13.0/lib/rspec/mocks/verifying_proxy.rb:161:in `proxy_method_invoked'
     # /Users/sergio/.rbenv/versions/3.2.2/lib/ruby/gems/3.2.0/gems/rspec-mocks-3.13.0/lib/rspec/mocks/method_double.rb:74:in `block (2 levels) in define_proxy_method'
     # /Users/sergio/.rbenv/versions/3.2.2/lib/ruby/gems/3.2.0/gems/rspec-mocks-3.13.0/lib/rspec/mocks/proxy.rb:164:in `reset'
     # /Users/sergio/.rbenv/versions/3.2.2/lib/ruby/gems/3.2.0/gems/rspec-mocks-3.13.0/lib/rspec/mocks/proxy.rb:315:in `reset'
     # /Users/sergio/.rbenv/versions/3.2.2/lib/ruby/gems/3.2.0/gems/rspec-mocks-3.13.0/lib/rspec/mocks/space.rb:79:in `block in reset_all'
     # /Users/sergio/.rbenv/versions/3.2.2/lib/ruby/gems/3.2.0/gems/rspec-mocks-3.13.0/lib/rspec/mocks/space.rb:79:in `each_value'
     # /Users/sergio/.rbenv/versions/3.2.2/lib/ruby/gems/3.2.0/gems/rspec-mocks-3.13.0/lib/rspec/mocks/space.rb:79:in `reset_all'
     # /Users/sergio/.rbenv/versions/3.2.2/lib/ruby/gems/3.2.0/gems/rspec-mocks-3.13.0/lib/rspec/mocks.rb:52:in `teardown'
     # /Users/sergio/.rbenv/versions/3.2.2/lib/ruby/gems/3.2.0/gems/rspec-core-3.13.0/lib/rspec/core/mocking_adapters/rspec.rb:27:in `teardown_mocks_for_rspec'
     # /Users/sergio/.rbenv/versions/3.2.2/lib/ruby/gems/3.2.0/gems/rspec-core-3.13.0/lib/rspec/core/example.rb:521:in `run_after_example'
     # /Users/sergio/.rbenv/versions/3.2.2/lib/ruby/gems/3.2.0/gems/rspec-core-3.13.0/lib/rspec/core/example.rb:283:in `block in run'
     # /Users/sergio/.rbenv/versions/3.2.2/lib/ruby/gems/3.2.0/gems/rspec-core-3.13.0/lib/rspec/core/example.rb:511:in `block in with_around_and_singleton_context_hooks'
     # /Users/sergio/.rbenv/versions/3.2.2/lib/ruby/gems/3.2.0/gems/rspec-core-3.13.0/lib/rspec/core/example.rb:468:in `block in with_around_example_hooks'
     # /Users/sergio/.rbenv/versions/3.2.2/lib/ruby/gems/3.2.0/gems/rspec-core-3.13.0/lib/rspec/core/hooks.rb:486:in `block in run'
     # /Users/sergio/.rbenv/versions/3.2.2/lib/ruby/gems/3.2.0/gems/rspec-core-3.13.0/lib/rspec/core/hooks.rb:624:in `run_around_example_hooks_for'
     # /Users/sergio/.rbenv/versions/3.2.2/lib/ruby/gems/3.2.0/gems/rspec-core-3.13.0/lib/rspec/core/hooks.rb:486:in `run'
     # /Users/sergio/.rbenv/versions/3.2.2/lib/ruby/gems/3.2.0/gems/rspec-core-3.13.0/lib/rspec/core/example.rb:468:in `with_around_example_hooks'
     # /Users/sergio/.rbenv/versions/3.2.2/lib/ruby/gems/3.2.0/gems/rspec-core-3.13.0/lib/rspec/core/example.rb:511:in `with_around_and_singleton_context_hooks'
     # /Users/sergio/.rbenv/versions/3.2.2/lib/ruby/gems/3.2.0/gems/rspec-core-3.13.0/lib/rspec/core/example.rb:259:in `run'
     # /Users/sergio/.rbenv/versions/3.2.2/lib/ruby/gems/3.2.0/gems/rspec-core-3.13.0/lib/rspec/core/example_group.rb:646:in `block in run_examples'
     # /Users/sergio/.rbenv/versions/3.2.2/lib/ruby/gems/3.2.0/gems/rspec-core-3.13.0/lib/rspec/core/example_group.rb:642:in `map'
     # /Users/sergio/.rbenv/versions/3.2.2/lib/ruby/gems/3.2.0/gems/rspec-core-3.13.0/lib/rspec/core/example_group.rb:642:in `run_examples'
     # /Users/sergio/.rbenv/versions/3.2.2/lib/ruby/gems/3.2.0/gems/rspec-core-3.13.0/lib/rspec/core/example_group.rb:607:in `run'
     # /Users/sergio/.rbenv/versions/3.2.2/lib/ruby/gems/3.2.0/gems/rspec-core-3.13.0/lib/rspec/core/example_group.rb:608:in `block in run'
     # /Users/sergio/.rbenv/versions/3.2.2/lib/ruby/gems/3.2.0/gems/rspec-core-3.13.0/lib/rspec/core/example_group.rb:608:in `map'
     # /Users/sergio/.rbenv/versions/3.2.2/lib/ruby/gems/3.2.0/gems/rspec-core-3.13.0/lib/rspec/core/example_group.rb:608:in `run'
     # /Users/sergio/.rbenv/versions/3.2.2/lib/ruby/gems/3.2.0/gems/rspec-core-3.13.0/lib/rspec/core/runner.rb:121:in `block (3 levels) in run_specs'
     # /Users/sergio/.rbenv/versions/3.2.2/lib/ruby/gems/3.2.0/gems/rspec-core-3.13.0/lib/rspec/core/runner.rb:121:in `map'
     # /Users/sergio/.rbenv/versions/3.2.2/lib/ruby/gems/3.2.0/gems/rspec-core-3.13.0/lib/rspec/core/runner.rb:121:in `block (2 levels) in run_specs'
     # /Users/sergio/.rbenv/versions/3.2.2/lib/ruby/gems/3.2.0/gems/rspec-core-3.13.0/lib/rspec/core/configuration.rb:2091:in `with_suite_hooks'
     # /Users/sergio/.rbenv/versions/3.2.2/lib/ruby/gems/3.2.0/gems/rspec-core-3.13.0/lib/rspec/core/runner.rb:116:in `block in run_specs'
     # /Users/sergio/.rbenv/versions/3.2.2/lib/ruby/gems/3.2.0/gems/rspec-core-3.13.0/lib/rspec/core/reporter.rb:74:in `report'
     # /Users/sergio/.rbenv/versions/3.2.2/lib/ruby/gems/3.2.0/gems/rspec-core-3.13.0/lib/rspec/core/runner.rb:115:in `run_specs'
     # /Users/sergio/.rbenv/versions/3.2.2/lib/ruby/gems/3.2.0/gems/rspec-core-3.13.0/lib/rspec/core/runner.rb:89:in `run'
     # /Users/sergio/.rbenv/versions/3.2.2/lib/ruby/gems/3.2.0/gems/rspec-core-3.13.0/lib/rspec/core/runner.rb:71:in `run'
     # /Users/sergio/.rbenv/versions/3.2.2/lib/ruby/gems/3.2.0/gems/rspec-core-3.13.0/lib/rspec/core/runner.rb:45:in `invoke'
     # /Users/sergio/.rbenv/versions/3.2.2/lib/ruby/gems/3.2.0/gems/rspec-core-3.13.0/exe/rspec:4:in `<top (required)>'
     # /Users/sergio/.rbenv/versions/3.2.2/bin/rspec:25:in `load'
     # /Users/sergio/.rbenv/versions/3.2.2/bin/rspec:25:in `<main>'
     #
     #   Showing full backtrace because every line was filtered out.
     #   See docs for RSpec::Configuration#backtrace_exclusion_patterns and
     #   RSpec::Configuration#backtrace_inclusion_patterns for more information.
@pirj
Copy link
Member

pirj commented Apr 16, 2024

A wild guess. What if you remove this line?
expect(Mutex).to receive(:new)

@sergio-bobillier
Copy link
Author

The result is the same with or without the expectation. I believe this condition arises from the setup in the before block.

@JonRowe
Copy link
Member

JonRowe commented Apr 26, 2024

This is different to the original bug and weirder. That was about the ability to stub mutex at all.
The call that causes the problem here is the stubbing of the synchronize method, if you remove:

allow(mocked_mutex).to receive(:synchronize).and_yield

The spec runs [or at least the version I set up in rspec-support does]

@sergio-bobillier
Copy link
Author

@JonRowe Thanks, yes, that actually removes the issue, but the thing is that would also keep me from testing what I want.

The code I added was the minimum required to trigger the issue, however I'm interested in also testing what happens inside the synchronize block. In my tests I'm checking that some actions are performed under the "protection" of the Mutex. If I remove the allow then that code will never be executed.

It is not even possible to test that synchronize is being called over the Mutex instance, even without the #and_yield call the "Stack level too deep" error occurs.

Even if I remove the allow completely, any test checking the call to synchronize will trigger the same error, for example:

spec/writer_spec.rb:

# frozen_string_literal: true

require_relative '../writer'

RSpec.describe Writer do
  subject(:writer) { described_class.new }

  let(:mocked_mutex) do
    instance_double(
      Mutex
    )
  end

  before do
    allow(Mutex).to receive(:new).and_return(mocked_mutex)
  end

  describe '#initialize' do
    it 'creates a new Mutex' do
      expect(Mutex).to receive(:new)
      writer
    end
  end

  describe '#write' do
    it 'uses the Mutex to synchronize the writing' do
      expect(mocked_mutex).to receive(:synchronize)
      writer.write("hello world!")
    end
  end
end

Now the test for #initialize passes, but the one for #write triggers the exact same issue.

And note that, just setting the expectation triggers the error, even if I remove the call to the actual method: writer.write("hello world!") the error will still be raised.

@JonRowe
Copy link
Member

JonRowe commented Apr 26, 2024

Apologies, I did not mean to imply you should remove the line, my intent was to highlight that it was the synchronize stub which was the root of the issue (to differnitate it from previous issues), as an aside your updated spec is essentially the same, its (likely) not a combination of allow and expect thats causing it.

@pirj
Copy link
Member

pirj commented Apr 26, 2024

We seem to be using synchronize in our code, and if it’s mocked to return a canned response - the behaviour is unpredictable.
Should we stash it for ourselves, too?

@JonRowe
Copy link
Member

JonRowe commented Apr 27, 2024

I don't think thats the issue, we stash new so that our instances of mutex shouldn't have that stub, and indeed I tried a version of where I did the same treatment to all methods with no changes. So either something is broken with how we stub / capture mutex on this or its a more complicated interaction, my quick investigation looked like it might be method visibility related

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants