module RSpec
module Mocks
# Provides recursive constant lookup methods useful for
# 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; B = 1; end
# class C < A; end
#
# On 1.8:
# - C.const_get("Hash") # => ::Hash
# - C.const_defined?("Hash") # => false
# - C.constants # => ["B"]
# - None of these methods accept the extra `inherit` argument
# On 1.9:
# - C.const_get("Hash") # => ::Hash
# - C.const_defined?("Hash") # => true
# - C.const_get("Hash", false) # => raises NameError
# - C.const_defined?("Hash", false) # => false
# - C.constants # => [:B]
# - C.constants(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
def constants_defined_on(mod)
mod.constants.select { |c| const_defined_on?(mod, c) }
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
def constants_defined_on(mod)
mod.constants(false)
end
end
def recursive_const_get(const_name)
normalize_const_name(const_name).split('::').inject(Object) do |mod, name|
get_const_defined_on(mod, name)
end
end
def recursive_const_defined?(const_name)
normalize_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 const_defined_on?(mod, name)
[get_const_defined_on(mod, name), [mod, name].join('::')]
end
end
def normalize_const_name(const_name)
const_name.sub(/\A::/, '')
end
end
# Provides information about constants that may (or may not)
# have been mutated by rspec-mocks.
class Constant
extend RecursiveConstMethods
# @api private
def initialize(name)
@name = name
@previously_defined = false
@stubbed = false
@hidden = false
end
# @return [String] The fully qualified name of the constant.
attr_reader :name
# @return [Object, nil] The original value (e.g. before it
# was mutated by rspec-mocks) of the constant, or
# nil if the constant was not previously defined.
attr_accessor :original_value
# @api private
attr_writer :previously_defined, :stubbed, :hidden
# @return [Boolean] Whether or not the constant was defined
# before the current example.
def previously_defined?
@previously_defined
end
# @return [Boolean] Whether or not rspec-mocks has mutated
# (stubbed or hidden) this constant.
def mutated?
@stubbed || @hidden
end
# @return [Boolean] Whether or not rspec-mocks has stubbed
# this constant.
def stubbed?
@stubbed
end
# @return [Boolean] Whether or not rspec-mocks has hidden
# this constant.
def hidden?
@hidden
end
def to_s
"#<#{self.class.name} #{name}>"
end
alias inspect to_s
# @api private
def self.unmutated(name)
const = new(name)
const.previously_defined = recursive_const_defined?(name)
const.stubbed = false
const.hidden = false
const.original_value = recursive_const_get(name) if const.previously_defined?
const
end
private_class_method :unmutated
# Queries rspec-mocks to find out information about the named constant.
#
# @param [String] name the name of the constant
# @return [Constant] an object contaning information about the named
# constant.
def self.original(name)
mutator = ConstantMutator.find(name)
mutator ? mutator.to_constant : unmutated(name)
end
end
# Provides a means to stub constants.
class ConstantMutator
extend RecursiveConstMethods
# Stubs a constant.
#
# @param (see ExampleMethods#stub_const)
# @option (see ExampleMethods#stub_const)
# @return (see ExampleMethods#stub_const)
#
# @see ExampleMethods#stub_const
# @note It's recommended that you use `stub_const` in your
# examples. This is an alternate public API that is provided
# so you can stub constants in other contexts (e.g. helper
# classes).
def self.stub(constant_name, value, options = {})
space = RSpec::Mocks.space
space.print_out_of_example_deprecation if space.outside_example
mutator = if recursive_const_defined?(constant_name, &raise_on_invalid_const)
DefinedConstantReplacer
else
UndefinedConstantSetter
end
mutate(mutator.new(constant_name, value, options[:transfer_nested_constants]))
value
end
# Hides a constant.
#
# @param (see ExampleMethods#hide_const)
#
# @see ExampleMethods#hide_const
# @note It's recommended that you use `hide_const` in your
# examples. This is an alternate public API that is provided
# so you can hide constants in other contexts (e.g. helper
# classes).
def self.hide(constant_name)
space = RSpec::Mocks.space
space.print_out_of_example_deprecation if space.outside_example
return unless recursive_const_defined?(constant_name)
mutate(ConstantHider.new(constant_name, nil, { }))
nil
end
# Contains common functionality used by all of the constant mutators.
#
# @api private
class BaseMutator
include RecursiveConstMethods
attr_reader :original_value, :full_constant_name
def initialize(full_constant_name, mutated_value, transfer_nested_constants)
@full_constant_name = normalize_const_name(full_constant_name)
@mutated_value = mutated_value
@transfer_nested_constants = transfer_nested_constants
@context_parts = @full_constant_name.split('::')
@const_name = @context_parts.pop
end
def to_constant
const = Constant.new(full_constant_name)
const.original_value = original_value
const
end
end
# Hides a defined constant for the duration of an example.
#
# @api private
class ConstantHider < BaseMutator
def mutate
@context = recursive_const_get(@context_parts.join('::'))
@original_value = get_const_defined_on(@context, @const_name)
@context.__send__(:remove_const, @const_name)
end
def to_constant
const = super
const.hidden = true
const.previously_defined = true
const
end
def rspec_reset
@context.const_set(@const_name, @original_value)
end
end
# Replaces a defined constant for the duration of an example.
#
# @api private
class DefinedConstantReplacer < BaseMutator
def mutate
@context = recursive_const_get(@context_parts.join('::'))
@original_value = get_const_defined_on(@context, @const_name)
constants_to_transfer = verify_constants_to_transfer!
@context.__send__(:remove_const, @const_name)
@context.const_set(@const_name, @mutated_value)
transfer_nested_constants(constants_to_transfer)
end
def to_constant
const = super
const.stubbed = true
const.previously_defined = true
const
end
def rspec_reset
@context.__send__(:remove_const, @const_name)
@context.const_set(@const_name, @original_value)
end
def transfer_nested_constants(constants)
constants.each do |const|
@mutated_value.const_set(const, get_const_defined_on(original_value, const))
end
end
def verify_constants_to_transfer!
return [] unless @transfer_nested_constants
{ @original_value => "the original value", @mutated_value => "the stubbed value" }.each do |value, description|
unless value.respond_to?(:constants)
raise ArgumentError,
"Cannot transfer nested constants for #{@full_constant_name} " +
"since #{description} is not a class or module and only classes " +
"and modules support nested constants."
end
end
if @transfer_nested_constants.is_a?(Array)
@transfer_nested_constants = @transfer_nested_constants.map(&:to_s) if RUBY_VERSION == '1.8.7'
undefined_constants = @transfer_nested_constants - constants_defined_on(@original_value)
if undefined_constants.any?
available_constants = constants_defined_on(@original_value) - @transfer_nested_constants
raise ArgumentError,
"Cannot transfer nested constant(s) #{undefined_constants.join(' and ')} " +
"for #{@full_constant_name} since they are not defined. Did you mean " +
"#{available_constants.join(' or ')}?"
end
@transfer_nested_constants
else
constants_defined_on(@original_value)
end
end
end
# Sets an undefined constant for the duration of an example.
#
# @api private
class UndefinedConstantSetter < BaseMutator
def mutate
remaining_parts = @context_parts.dup
@deepest_defined_const = @context_parts.inject(Object) do |klass, name|
break klass unless const_defined_on?(klass, name)
remaining_parts.shift
get_const_defined_on(klass, name)
end
context = remaining_parts.inject(@deepest_defined_const) do |klass, name|
klass.const_set(name, Module.new)
end
@const_to_remove = remaining_parts.first || @const_name
context.const_set(@const_name, @mutated_value)
end
def to_constant
const = super
const.stubbed = true
const.previously_defined = false
const
end
def rspec_reset
@deepest_defined_const.__send__(:remove_const, @const_to_remove)
end
end
# Uses the mutator to mutate (stub or hide) a constant. Ensures that
# the mutator is correctly registered so it can be backed out at the end
# of the test.
#
# @api private
def self.mutate(mutator)
register_mutator(mutator)
mutator.mutate
end
# Resets all stubbed constants. This is called automatically
# by rspec-mocks when an example finishes.
#
# @api private
def self.reset_all
# We use reverse order so that if the same constant
# was stubbed multiple times, the original value gets
# properly restored.
mutators.reverse.each { |s| s.rspec_reset }
mutators.clear
end
# The list of constant mutators that have been used for
# the current example.
#
# @api private
def self.mutators
@mutators ||= []
end
# @api private
def self.register_mutator(mutator)
mutators << mutator
end
def self.find(name)
mutators.find { |s| s.full_constant_name == name }
end
# Used internally by the constant stubbing to raise a helpful
# error when a constant like "A::B::C" is stubbed and A::B is
# not a module (and thus, it's impossible to define "A::B::C"
# since only modules can have nested constants).
#
# @api private
def self.raise_on_invalid_const
lambda do |const_name, failed_name|
raise "Cannot stub constant #{failed_name} on #{const_name} " +
"since #{const_name} is not a module."
end
end
end
end
end