-
-
Notifications
You must be signed in to change notification settings - Fork 358
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
Exclude stubbed classes from subclasses after teardown #1570
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It’s nice that you’ve found a reliable solution.
I’m skeptical, however, to include it for everyone.
There might be some performance impact, and since this can introduce flakiness due to the GC race condition and an exception is not reassuring.
Still, this should be kept as a reference for those who experience issues with subclasses
.
lib/rspec/mocks.rb
Outdated
@@ -101,6 +101,23 @@ def self.with_temporary_scope | |||
end | |||
end | |||
|
|||
@@excluded_subclasses = [] | |||
|
|||
def self.excluded_subclasses |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it necessary to getobj?
There’s a potential race condition, and the weakref itself should quack just like the object itself (including the === probably to allow to remove from the list.
https://stackoverflow.com/questions/69185508/ruby-weakref-has-implicit-race-condition
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, otherwise the substraction wasn't working.
I changed to rescue RefError.
lib/rspec/mocks.rb
Outdated
@excluded_subclasses = [] | ||
|
||
def self.excluded_subclasses | ||
@excluded_subclasses.select(&:weakref_alive?).map do |ref| |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not just do this in the actual method, then you don't have to build the list.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You mean with ?
diff --git a/lib/rspec/mocks.rb b/lib/rspec/mocks.rb
index a82055dd..ad697b59 100644
--- a/lib/rspec/mocks.rb
+++ b/lib/rspec/mocks.rb
@@ -104,11 +104,7 @@ module RSpec
@excluded_subclasses = []
def self.excluded_subclasses
- @excluded_subclasses.select(&:weakref_alive?).map do |ref|
- ref.__getobj__
- rescue RefError
- nil
- end.compact
+ @excluded_subclasses
end
def self.exclude_subclass(constant)
@@ -117,7 +113,11 @@ module RSpec
module ExcludeClassesFromSubclasses
def subclasses
- super - RSpec::Mocks.excluded_subclasses
+ super - RSpec::Mocks.excluded_subclasses.select(&:weakref_alive?).map do |ref|
+ ref.__getobj__
+ rescue RefError
+ nil
+ end.compact
end
end
Class.prepend(ExcludeClassesFromSubclasses)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just placing a block on this as I don't want it merged without final say so @pirj I'm concerned about the require.
I understand your worries. I would like to find a better solution but wasn't able. Maybe we can use a config as |
The more I think, the more I'm in favor of a config flag. Maybe a warning message that say the flag have to be switched should be displayed when there is an error. |
I support your idea of opting in for this with a flag. |
We could conditionally require weakref if this option is on. |
Should we have the option on by default ? |
No. But we’re working in RSpec 4, and we can think about enabling it there unless performance considerations come up. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I refactored, added a config flag and specs. I tried to respect the existing code. Let me know what you think.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I took RSpec::Mocks::MarshalExtension
for example.
I change to run specs only for Ruby 3.1 and ahead as in https://github.com/rails/rails/blob/v7.0.8/activesupport/lib/active_support/ruby_features.rb |
Sorry, my latest commit was completely broke. I fixed it. |
I fixed the code for Ruby <= 2.4 |
|
||
Class.class_eval do | ||
undef subclasses_with_rspec_mocks | ||
alias subclasses subclasses_without_rspec_mocks |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rubocop triggers this warning :
lib/rspec/mocks/exclude_stubbed_classes_from_subclasses.rb:47:11: W: Lint/DuplicateMethods: Method RSpec::Mocks::ExcludeStubbedClassesFromSubclasses::Class#subclasses is defined at both lib/rspec/mocks/exclude_stubbed_classes_from_subclasses.rb:38 and lib/rspec/mocks/exclude_stubbed_classes_from_subclasses.rb:47.
alias subclasses subclasses_without_rspec_mocks
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
It's wrong because the method is undefined at line 46. I disabled this warning.
The CI pass except for "ruby-head" which is not to my commit |
I fixed a flap on specs. Now the CI should pass. |
I finally succeeded to install Ruby 3.4-dev locally and confirm that the error on the CI is not related to the PR. It also fail on the main branch. |
Hello, I don't want to bother you but is it possible to have a quick review, at least on the global behavior? I'm waiting for this to be able to move forward on the project I'm working on. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please accept my apologies for the wait. The code is mostly good, thanks!
I’d love to hear what @JonRowe says.
Do we need a benchmark to measure the overhead in reset
calls?
spec/rspec/mocks/exclude_stubbed_classes_from_subclasses_spec.rb
Outdated
Show resolved
Hide resolved
spec/rspec/mocks/exclude_stubbed_classes_from_subclasses_spec.rb
Outdated
Show resolved
Hide resolved
No problem, it's open source. You don't even have to reply. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks solid, thank you! 🙌
Just a few optional, subjective and cosmetic notes - please feel free to ignore.
def exclude_subclass(constant) | ||
require 'weakref' unless defined?(::WeakRef) | ||
|
||
@excluded_subclasses ||= [] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is in a few places. Might be it belongs to enable!
, too?
end | ||
|
||
def excluded_subclasses | ||
require 'weakref' unless defined?(::WeakRef) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok to move this line into enable!
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure if it's a problem but now we can do RSpec::Mocks::ExcludeStubbedClassesFromSubclasses.excluded_subclasses
without ExcludeStubbedClassesFromSubclasses.enable!
first. If we move this line to enable!
, it will raise an error.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As you wish, I can do :
diff --git a/lib/rspec/mocks/exclude_stubbed_classes_from_subclasses.rb b/lib/rspec/mocks/exclude_stubbed_classes_from_subclasses.rb
index bb72101e..129ff0d2 100644
--- a/lib/rspec/mocks/exclude_stubbed_classes_from_subclasses.rb
+++ b/lib/rspec/mocks/exclude_stubbed_classes_from_subclasses.rb
@@ -8,6 +8,8 @@ module RSpec
def enable!
return unless RUBY_VERSION >= "3.1"
return if Class.respond_to?(:subclasses_with_rspec_mocks)
+ @excluded_subclasses = []
+ require 'weakref' unless defined?(::WeakRef)
Class.class_eval do
def subclasses_with_rspec_mocks
@@ -32,9 +34,6 @@ module RSpec
end
def excluded_subclasses
- require 'weakref' unless defined?(::WeakRef)
-
- @excluded_subclasses ||= []
@excluded_subclasses.select(&:weakref_alive?).map do |ref|
begin
ref.__getobj__
@@ -45,9 +44,6 @@ module RSpec
end
def exclude_subclass(constant)
- require 'weakref' unless defined?(::WeakRef)
-
- @excluded_subclasses ||= []
@excluded_subclasses << ::WeakRef.new(constant)
end
end
diff --git a/spec/rspec/mocks/exclude_stubbed_classes_from_subclasses_spec.rb b/spec/rspec/mocks/exclude_stubbed_classes_from_subclasses_spec.rb
index 3b5ded94..16454922 100644
--- a/spec/rspec/mocks/exclude_stubbed_classes_from_subclasses_spec.rb
+++ b/spec/rspec/mocks/exclude_stubbed_classes_from_subclasses_spec.rb
@@ -63,6 +63,10 @@ if RUBY_VERSION >= '3.1'
end
describe '.excluded_subclasses' do
+ before do
+ described_class.enable!
+ end
+
it 'returns excluded subclasses' do
subclass = Class.new
described_class.exclude_subclass(subclass)
@@ -75,14 +79,12 @@ if RUBY_VERSION >= '3.1'
described_class.exclude_subclass(subclass)
subclass = nil
-
GC.start
expect(described_class.excluded_subclasses).to eq([])
end
it 'does not return excluded subclasses that raises a ::WeakRef::RefError' do
- require 'weakref'
subclass = double(:weakref_alive? => true)
described_class.instance_variable_set(:@excluded_subclasses, [subclass])
|
||
@excluded_subclasses ||= [] | ||
@excluded_subclasses.select(&:weakref_alive?).map do |ref| | ||
begin # rubocop:disable Style/RedundantBegin |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It slipped my mind, isn’t it possible to squash that so that rescue is on the map itself?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, nevermind, Ruby 1.8 probably doesn’t support that. Do you mind making a note to indicate that?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I removed the comment because the CI was failing because of that (I don't have the same behavior locally with the same version of Ruby/Rubocop).
@JonRowe can you give a look at this request ? |
I'd like to apologise for leaving this hanging for so long, its been on my review list for ages but I just haven't been able to get around to it, I had a couple of concerns about weak ref and other such behaviour but following on from the discussion in #1568 I'm going to close this. Sorry to have wasted your time. |
This fix #1568.
Stubbed classes are excluded from parent subclasses after each spec.
The original issue :
Now,
Something.subclasses
always return[B, A]
.