Skip to content
This repository has been archived by the owner on Nov 30, 2024. It is now read-only.

Commit

Permalink
Handle stubbing IO#write and then calling IO#reopen.
Browse files Browse the repository at this point in the history
  • Loading branch information
myronmarston committed Dec 30, 2014
1 parent 8f03c38 commit 5a0a48c
Show file tree
Hide file tree
Showing 4 changed files with 65 additions and 2 deletions.
4 changes: 4 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ Bug Fixes:
(Myron Marston, #846)
* Prevent message expectations from being modified by customization methods
(e.g. `with`) after they have been invoked. (Sam Phippen and Melanie Gilman, #837)
* Handle cases where a method stub cannot be removed due to something
external to RSpec monkeying with the method definition. This can
happen, for example, when you `file.reopen(io)` after previously
stubbing a method on the `file` object. (Myron Marston, #853)

### 3.1.3 / 2014-10-08
[Full Changelog](http://github.com/rspec/rspec-mocks/compare/v3.1.2...v3.1.3)
Expand Down
27 changes: 25 additions & 2 deletions lib/rspec/mocks/method_double.rb
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,7 @@ def restore_original_method
return show_frozen_warning if object_singleton_class.frozen?
return unless @method_is_proxied

definition_target.__send__(:remove_method, @method_name)

remove_method_from_definition_target
@method_stasher.restore if @method_stasher.method_is_stashed?
restore_original_visibility

Expand Down Expand Up @@ -255,6 +254,30 @@ def definition_target
end

end

private

def remove_method_from_definition_target
definition_target.__send__(:remove_method, @method_name)
rescue NameError
# This can happen when the method has been monkeyed with by
# something outside RSpec. This happens, for example, when
# `file.write` has been stubbed, and then `file.reopen(other_io)`
# is later called, as `File#reopen` appears to redefine `write`.
#
# Note: we could avoid rescuing this by checking
# `definition_target.instance_method(@method_name).owner == definition_target`,
# saving us from the cost of the expensive exception, but this error is
# extremely rare (it was discovered on 2014-12-30, only happens on
# RUBY_VERSION < 2.0 and our spec suite only hits this condition once),
# so we'd rather avoid the cost of that check for every method double,
# and risk the rare situation where this exception will get raised.
RSpec.warn_with(
"WARNING: RSpec could not fully restore #{@object.inspect}." \
"#{@method_name}, possibly because the method has been redefined " \
"by something outside of RSpec."
)
end
end
end
end
36 changes: 36 additions & 0 deletions spec/rspec/mocks/partial_double_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,42 @@ def self.inspect
end
end

RSpec.describe "Using a reopened file object as a partial mock" do
let(:file_1) { File.open(File.join("tmp", "file_1"), "w").tap { |f| f.sync = true } }
let(:file_2) { File.open(File.join("tmp", "file_2"), "w").tap { |f| f.sync = true } }

def read_file(file)
File.open(file.path, "r", &:read)
end

after do
file_1.close
file_2.close
end

def expect_output_warning_on_ruby_lt_2
if RUBY_VERSION >= '2.0'
yield
else
expect { yield }.to output(a_string_including(
"RSpec could not fully restore", file_1.inspect, "write"
)).to_stderr
end
end

it "allows `write` to be stubbed and reset" do
allow(file_1).to receive(:write)
file_1.reopen(file_2)
expect_output_warning_on_ruby_lt_2 { reset file_1 }

expect {
# Writing to a file that's been reopened to a 2nd file writes to both files.
file_1.write("foo")
}.to change { read_file(file_1) }.from("").to("foo").
and change { read_file(file_2) }.from("").to("foo")
end
end

RSpec.describe "Using a partial mock on a proxy object", :if => defined?(::BasicObject) do
let(:proxy_class) do
Class.new(::BasicObject) do
Expand Down
Empty file added tmp/.gitkeep
Empty file.

0 comments on commit 5a0a48c

Please sign in to comment.