Skip to content

Commit de0f104

Browse files
committed
Merge pull request #265 from undees/undees/73-avoid-infinite-loop-on-mocked-is-a
Avoid infinite loop when classes mock #is_a?
1 parent 88abcac commit de0f104

File tree

3 files changed

+68
-1
lines changed

3 files changed

+68
-1
lines changed

rspec-mocks/Changelog.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
### Development
22
[Full Changelog](https://github.com/rspec/rspec/compare/rspec-mocks-v3.13.5...3-13-maintenance)
33

4+
Bug Fixes:
5+
6+
* Work around possible infinite loop when stubbing `is_a?`. (Erin Paget, rspec/rspec#265)
7+
48
### 3.13.5 / 2025-05-27
59
[Full Changelog](https://github.com/rspec/rspec/compare/rspec-mocks-v3.13.4...rspec-mocks-v3.13.5)
610

rspec-mocks/lib/rspec/mocks/proxy.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ def initialize(object, order_group, options={})
3232

3333
# @private
3434
def ensure_can_be_proxied!(object)
35-
return unless object.is_a?(Symbol)
35+
return unless Symbol === object
3636

3737
msg = "Cannot proxy frozen objects. Symbols such as #{object} cannot be mocked or stubbed."
3838
raise ArgumentError, msg

rspec-mocks/spec/rspec/mocks/matchers/receive_spec.rb

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,56 @@ def receiver.method_missing(*); end # a poor man's stub...
411411
# rubocop:enable Lint/StructNewOverride
412412
end
413413

414+
shared_examples "handles stubbed #is_a? cleanly for a single instance" do
415+
let(:klass) do
416+
Class.new do
417+
def call_stubbed_method
418+
is_a?(self.class)
419+
end
420+
end
421+
end
422+
423+
let(:object) { klass.new }
424+
425+
it "safely checks that the instance can be proxied" do
426+
stubbed_answer = double("stubbed on an instance")
427+
target.to receive(:is_a?).and_return(stubbed_answer)
428+
expect(object.call_stubbed_method).to eq(stubbed_answer)
429+
end
430+
end
431+
432+
shared_examples "handles stubbed #is_a? cleanly for any_instance_of" do
433+
let(:superclass) { Class.new }
434+
435+
let(:klass) do
436+
Class.new(superclass) do
437+
def call_stubbed_method
438+
is_a?(self.class)
439+
end
440+
end
441+
end
442+
443+
context "the class under test" do
444+
let(:stubbed_class) { klass }
445+
446+
it "safely checks that the instance can be proxied" do
447+
stubbed_answer = double("stubbed on the class under test")
448+
target.to receive(:is_a?).and_return(stubbed_answer)
449+
expect(klass.new.call_stubbed_method).to eq(stubbed_answer)
450+
end
451+
end
452+
453+
context "the parent of the class under test" do
454+
let(:stubbed_class) { superclass }
455+
456+
it "safely checks that the instance can be proxied" do
457+
stubbed_answer = double("stubbed on the superclass")
458+
target.to receive(:is_a?).and_return(stubbed_answer)
459+
expect(klass.new.call_stubbed_method).to eq(stubbed_answer)
460+
end
461+
end
462+
end
463+
414464
describe "allow(...).to receive" do
415465
it_behaves_like "an expect syntax allowance" do
416466
let(:receiver) { double }
@@ -422,6 +472,9 @@ def receiver.method_missing(*); end # a poor man's stub...
422472
it_behaves_like "handles frozen objects cleanly" do
423473
let(:target) { allow(object) }
424474
end
475+
it_behaves_like "handles stubbed #is_a? cleanly for a single instance" do
476+
let(:target) { allow(object) }
477+
end
425478

426479
context 'ordered with receive counts' do
427480
specify 'is not supported' do
@@ -486,6 +539,10 @@ def receiver.method_missing(*); end # a poor man's stub...
486539
it_behaves_like "resets partial mocks of any instance cleanly" do
487540
let(:target) { allow_any_instance_of(klass) }
488541
end
542+
543+
it_behaves_like "handles stubbed #is_a? cleanly for any_instance_of" do
544+
let(:target) { allow_any_instance_of(stubbed_class) }
545+
end
489546
end
490547

491548
describe "allow_any_instance_of(...).not_to receive" do
@@ -532,6 +589,9 @@ def receiver.method_missing(*); end # a poor man's stub...
532589
it_behaves_like "handles frozen objects cleanly" do
533590
let(:target) { expect(object) }
534591
end
592+
it_behaves_like "handles stubbed #is_a? cleanly for a single instance" do
593+
let(:target) { allow(object) }
594+
end
535595

536596
context "ordered with receive counts" do
537597
let(:dbl) { double(:one => 1, :two => 2) }
@@ -631,6 +691,9 @@ def receiver.method_missing(*); end # a poor man's stub...
631691
it_behaves_like "resets partial mocks of any instance cleanly" do
632692
let(:target) { expect_any_instance_of(klass) }
633693
end
694+
it_behaves_like "handles stubbed #is_a? cleanly for any_instance_of" do
695+
let(:target) { expect_any_instance_of(stubbed_class) }
696+
end
634697
end
635698

636699
describe "expect(...).not_to receive" do

0 commit comments

Comments
 (0)