module RSpec module Core # Each ExampleGroup class and Example instance owns an instance of # Metadata, which is Hash extended to support lazy evaluation of values # associated with keys that may or may not be used by any example or group. # # In addition to metadata that is used internally, this also stores # user-supplied metadata, e.g. # # describe Something, :type => :ui do # it "does something", :slow => true do # # ... # end # end # # `:type => :ui` is stored in the Metadata owned by the example group, and # `:slow => true` is stored in the Metadata owned by the example. These can # then be used to select which examples are run using the `--tag` option on # the command line, or several methods on `Configuration` used to filter a # run (e.g. `filter_run_including`, `filter_run_excluding`, etc). # # @see Example#metadata # @see ExampleGroup.metadata # @see FilterManager # @see Configuration#filter_run_including # @see Configuration#filter_run_excluding module Metadata # @api private # # @param line [String] current code line # @return [String] relative path to line def self.relative_path(line) # Matches strings either at the beginning of the input or prefixed with a whitespace, # containing the current path, either postfixed with the separator, or at the end of the string. # Match groups are the character before and the character after the string if any. # # http://rubular.com/r/fT0gmX6VJX # http://rubular.com/r/duOrD4i3wb # http://rubular.com/r/sbAMHFrOx1 # regex = /(\A|\s)#{File.expand_path('.')}(#{File::SEPARATOR}|\s|\Z)/ line = line.sub(regex, "\\1.\\2") line = line.sub(/\A([^:]+:\d+)$/, '\\1') return nil if line == '-e:1' line rescue SecurityError nil end # @private # Used internally to build a hash from an args array. # Symbols are converted into hash keys with a value of `true`. # This is done to support simple tagging using a symbol, rather # than needing to do `:symbol => true`. def self.build_hash_from(args, warn_about_example_group_filtering=false) hash = args.last.is_a?(Hash) ? args.pop : {} hash[args.pop] = true while args.last.is_a?(Symbol) if warn_about_example_group_filtering && hash.key?(:example_group) RSpec.deprecate("Filtering by an `:example_group` subhash", :replacement => "the subhash to filter directly") end hash end # @private def self.backtrace_from(block) return caller unless block.respond_to?(:source_location) [block.source_location.join(':')] end # @private # Used internally to populate metadata hashes with computed keys # managed by RSpec. class HashPopulator attr_reader :metadata, :user_metadata, :description_args, :block def initialize(metadata, user_metadata, description_args, block) @metadata = metadata @user_metadata = user_metadata @description_args = description_args @block = block end def populate ensure_valid_user_keys metadata[:execution_result] = Example::ExecutionResult.new metadata[:block] = block metadata[:description_args] = description_args metadata[:description] = build_description_from(*metadata[:description_args]) metadata[:full_description] = full_description metadata[:described_class] = described_class populate_location_attributes metadata.update(user_metadata) RSpec.configuration.apply_derived_metadata_to(metadata) end private def populate_location_attributes backtrace = user_metadata.delete(:caller) file_path, line_number = if backtrace file_path_and_line_number_from(backtrace) elsif block.respond_to?(:source_location) block.source_location else file_path_and_line_number_from(caller) end file_path = Metadata.relative_path(file_path) metadata[:file_path] = file_path metadata[:line_number] = line_number.to_i metadata[:location] = "#{file_path}:#{line_number}" end def file_path_and_line_number_from(backtrace) first_caller_from_outside_rspec = backtrace.find { |l| l !~ CallerFilter::LIB_REGEX } first_caller_from_outside_rspec ||= backtrace.first /(.+?):(\d+)(?:|:\d+)/.match(first_caller_from_outside_rspec).captures end def description_separator(parent_part, child_part) if parent_part.is_a?(Module) && child_part =~ /^(#|::|\.)/ '' else ' ' end end def build_description_from(parent_description=nil, my_description=nil) return parent_description.to_s unless my_description separator = description_separator(parent_description, my_description) parent_description.to_s + separator + my_description.to_s end def ensure_valid_user_keys RESERVED_KEYS.each do |key| next unless user_metadata.key?(key) raise <<-EOM.gsub(/^\s+\|/, '') |#{"*" * 50} |:#{key} is not allowed | |RSpec reserves some hash keys for its own internal use, |including :#{key}, which is used on: | | #{CallerFilter.first_non_rspec_line}. | |Here are all of RSpec's reserved hash keys: | | #{RESERVED_KEYS.join("\n ")} |#{"*" * 50} EOM end end end # @private class ExampleHash < HashPopulator def self.create(group_metadata, user_metadata, description, block) example_metadata = group_metadata.dup group_metadata = Hash.new(&ExampleGroupHash.backwards_compatibility_default_proc do |hash| hash[:parent_example_group] end) group_metadata.update(example_metadata) example_metadata[:example_group] = group_metadata example_metadata.delete(:parent_example_group) hash = new(example_metadata, user_metadata, [description].compact, block) hash.populate hash.metadata end private def described_class metadata[:example_group][:described_class] end def full_description build_description_from( metadata[:example_group][:full_description], metadata[:description] ) end end # @private class ExampleGroupHash < HashPopulator def self.create(parent_group_metadata, user_metadata, *args, &block) group_metadata = hash_with_backwards_compatibility_default_proc if parent_group_metadata group_metadata.update(parent_group_metadata) group_metadata[:parent_example_group] = parent_group_metadata end hash = new(group_metadata, user_metadata, args, block) hash.populate hash.metadata end def self.hash_with_backwards_compatibility_default_proc Hash.new(&backwards_compatibility_default_proc { |hash| hash }) end def self.backwards_compatibility_default_proc(&example_group_selector) Proc.new do |hash, key| case key when :example_group # We commonly get here when rspec-core is applying a previously configured # filter rule, such as when a gem configures: # # RSpec.configure do |c| # c.include MyGemHelpers, :example_group => { :file_path => /spec\/my_gem_specs/ } # end # # It's confusing for a user to get a deprecation at this point in the code, so instead # we issue a deprecation from the config APIs that take a metadata hash, and MetadataFilter # sets this thread local to silence the warning here since it would be so confusing. unless RSpec.thread_local_metadata[:silence_metadata_example_group_deprecations] RSpec.deprecate("The `:example_group` key in an example group's metadata hash", :replacement => "the example group's hash directly for the " \ "computed keys and `:parent_example_group` to access the parent " \ "example group metadata") end group_hash = example_group_selector.call(hash) LegacyExampleGroupHash.new(group_hash) if group_hash when :example_group_block RSpec.deprecate("`metadata[:example_group_block]`", :replacement => "`metadata[:block]`") hash[:block] when :describes RSpec.deprecate("`metadata[:describes]`", :replacement => "`metadata[:described_class]`") hash[:described_class] end end end private def described_class candidate = metadata[:description_args].first return candidate unless NilClass === candidate || String === candidate parent_group = metadata[:parent_example_group] parent_group && parent_group[:described_class] end def full_description description = metadata[:description] parent_example_group = metadata[:parent_example_group] return description unless parent_example_group parent_description = parent_example_group[:full_description] separator = description_separator(parent_example_group[:description_args].last, metadata[:description_args].first) parent_description + separator + description end end # @private RESERVED_KEYS = [ :description, :example_group, :parent_example_group, :execution_result, :file_path, :full_description, :line_number, :location, :block ] end # Mixin that makes the including class imitate a hash for backwards # compatibility. The including class should use `attr_accessor` to # declare attributes. # @private module HashImitatable def self.included(klass) klass.extend ClassMethods end def to_h hash = extra_hash_attributes.dup self.class.hash_attribute_names.each do |name| hash[name] = __send__(name) end hash end (Hash.public_instance_methods - Object.public_instance_methods).each do |method_name| next if [:[], :[]=, :to_h].include?(method_name.to_sym) define_method(method_name) do |*args, &block| issue_deprecation(method_name, *args) hash = hash_for_delegation self.class.hash_attribute_names.each do |name| hash.delete(name) unless instance_variable_defined?(:"@#{name}") end hash.__send__(method_name, *args, &block).tap do # apply mutations back to the object hash.each do |name, value| if directly_supports_attribute?(name) set_value(name, value) else extra_hash_attributes[name] = value end end end end end def [](key) issue_deprecation(:[], key) if directly_supports_attribute?(key) get_value(key) else extra_hash_attributes[key] end end def []=(key, value) issue_deprecation(:[]=, key, value) if directly_supports_attribute?(key) set_value(key, value) else extra_hash_attributes[key] = value end end private def extra_hash_attributes @extra_hash_attributes ||= {} end def directly_supports_attribute?(name) self.class.hash_attribute_names.include?(name) end def get_value(name) __send__(name) end def set_value(name, value) __send__(:"#{name}=", value) end def hash_for_delegation to_h end def issue_deprecation(_method_name, *_args) # no-op by default: subclasses can override end # @private module ClassMethods def hash_attribute_names @hash_attribute_names ||= [] end def attr_accessor(*names) hash_attribute_names.concat(names) super end end end # @private # Together with the example group metadata hash default block, # provides backwards compatibility for the old `:example_group` # key. In RSpec 2.x, the computed keys of a group's metadata # were exposed from a nested subhash keyed by `[:example_group]`, and # then the parent group's metadata was exposed by sub-subhash # keyed by `[:example_group][:example_group]`. # # In RSpec 3, we reorganized this to that the computed keys are # exposed directly of the group metadata hash (no nesting), and # `:parent_example_group` returns the parent group's metadata. # # Maintaining backwards compatibility was difficult: we wanted # `:example_group` to return an object that: # # * Exposes the top-level metadata keys that used to be nested # under `:example_group`. # * Supports mutation (rspec-rails, for example, assigns # `metadata[:example_group][:described_class]` when you use # anonymous controller specs) such that changes are written # back to the top-level metadata hash. # * Exposes the parent group metadata as `[:example_group][:example_group]`. class LegacyExampleGroupHash include HashImitatable def initialize(metadata) @metadata = metadata parent_group_metadata = metadata.fetch(:parent_example_group) { {} }[:example_group] self[:example_group] = parent_group_metadata if parent_group_metadata end def to_h super.merge(@metadata) end private def directly_supports_attribute?(name) name != :example_group end def get_value(name) @metadata[name] end def set_value(name, value) @metadata[name] = value end end end end