Permalink
Browse files

Fix use of const_defined? and const_get so it ignores top-level const…

…ants.

I didn't realize this previously, but these methods can pick up a top-level
constant when you don't intend it (e.g. ::Hash when checking MyGem.const_defined?("Hash")).
  • Loading branch information...
1 parent bb21bc1 commit deec990c93ad857c920a726b1e6633ac375bef50 @myronmarston myronmarston committed Aug 10, 2012
Showing with 53 additions and 7 deletions.
  1. +3 −0 Changelog.md
  2. +46 −7 lib/rspec/mocks/stub_const.rb
  3. +4 −0 spec/rspec/mocks/stub_const_spec.rb
View
@@ -6,6 +6,9 @@ Bug fixes
* Don't modify `dup` on classes that don't support `dup` (David Chelimsky)
* Fix `any_instance` so that it works properly with methods defined on
a superclass. (Daniel Eguzkiza)
+* Fix `stub_const` so that it works properly for nested constants that
+ share a name with a top-level constant (e.g. "MyGem::Hash"). (Myron
+ Marston)
### 2.11.1 / 2012-07-09
[full changelog](http://github.com/rspec/rspec-mocks/compare/v2.11.0...v2.11.1)
@@ -4,15 +4,54 @@ module Mocks
# constant stubbing.
# @api private
module RecursiveConstMethods
+ # We only want to consider constants that are defined directly on a
+ # particular module, and not include top-level/inherited constants.
+ # Unfortunately, the constant API changed between 1.8 and 1.9, so
+ # we need to conditionally define methods to ignore the top-level/inherited
+ # constants.
+ #
+ # Given `class A; end`:
+ #
+ # On 1.8:
+ # - A.const_get("Hash") # => ::Hash
+ # - A.const_defined?("Hash") # => false
+ # - Neither method accepts the extra `inherit` argument
+ # On 1.9:
+ # - A.const_get("Hash") # => ::Hash
+ # - A.const_defined?("Hash") # => true
+ # - A.const_get("Hash", false) # => raises NameError
+ # - A.const_defined?("Hash", false) # => false
+ if Module.method(:const_defined?).arity == 1
+ def const_defined_on?(mod, const_name)
+ mod.const_defined?(const_name)
+ end
+
+ def get_const_defined_on(mod, const_name)
+ if const_defined_on?(mod, const_name)
+ return mod.const_get(const_name)
+ end
+
+ raise NameError, "uninitialized constant #{mod.name}::#{const_name}"
+ end
+ else
+ def const_defined_on?(mod, const_name)
+ mod.const_defined?(const_name, false)
+ end
+
+ def get_const_defined_on(mod, const_name)
+ mod.const_get(const_name, false)
+ end
+ end
+
def recursive_const_get(const_name)
- const_name.split('::').inject(Object) { |mod, name| mod.const_get name }
+ const_name.split('::').inject(Object) { |mod, name| get_const_defined_on(mod, name) }
end
def recursive_const_defined?(const_name)
const_name.split('::').inject([Object, '']) do |(mod, full_name), name|
yield(full_name, name) if block_given? && !mod.is_a?(Module)
- return false unless mod.const_defined?(name)
- [mod.const_get(name), [mod, name].join('::')]
+ return false unless const_defined_on?(mod, name)
+ [get_const_defined_on(mod, name), [mod, name].join('::')]
end
end
end
@@ -139,7 +178,7 @@ def to_constant
class DefinedConstantReplacer < BaseStubber
def stub
@context = recursive_const_get(@context_parts.join('::'))
- @original_value = @context.const_get(@const_name)
+ @original_value = get_const_defined_on(@context, @const_name)
constants_to_transfer = verify_constants_to_transfer!
@@ -160,7 +199,7 @@ def rspec_reset
def transfer_nested_constants(constants)
constants.each do |const|
- @stubbed_value.const_set(const, original_value.const_get(const))
+ @stubbed_value.const_set(const, get_const_defined_on(original_value, const))
end
end
@@ -202,9 +241,9 @@ class UndefinedConstantSetter < BaseStubber
def stub
remaining_parts = @context_parts.dup
@deepest_defined_const = @context_parts.inject(Object) do |klass, name|
- break klass unless klass.const_defined?(name)
+ break klass unless const_defined_on?(klass, name)
remaining_parts.shift
- klass.const_get(name)
+ get_const_defined_on(klass, name)
end
context = remaining_parts.inject(@deepest_defined_const) do |klass, name|
@@ -177,6 +177,10 @@ def change_const_value_to(value)
it_behaves_like "loaded constant stubbing", "TestClass::Nested"
end
+ context 'for an unloaded constant with nested name that matches a top-level constant' do
+ it_behaves_like "unloaded constant stubbing", "TestClass::Hash"
+ end
+
context 'for a loaded deeply nested constant' do
it_behaves_like "loaded constant stubbing", "TestClass::Nested::NestedEvenMore"
end

0 comments on commit deec990

Please sign in to comment.