lib/rspec/core/shared_example_group.rb



module RSpec
  module Core
    module SharedExampleGroup
      # @overload shared_examples(name, &block)
      # @overload shared_examples(name, tags, &block)
      #
      # Wraps the `block` in a module which can then be included in example
      # groups using `include_examples`, `include_context`, or
      # `it_behaves_like`.
      #
      # @param [String] name to match when looking up this shared group
      # @param block to be eval'd in a nested example group generated by `it_behaves_like`
      #
      # @example
      #
      #   shared_examples "auditable" do
      #     it "stores an audit record on save!" do
      #       lambda { auditable.save! }.should change(Audit, :count).by(1)
      #     end
      #   end
      #
      #   class Account do
      #     it_behaves_like "auditable" do
      #       def auditable; Account.new; end
      #     end
      #   end
      #
      # @see ExampleGroup.it_behaves_like
      # @see ExampleGroup.include_examples
      # @see ExampleGroup.include_context
      def shared_examples(*args, &block)
        SharedExampleGroup.registry.add_group(self, *args, &block)
      end

      alias_method :shared_context,      :shared_examples
      alias_method :share_examples_for,  :shared_examples
      alias_method :shared_examples_for, :shared_examples

      def shared_example_groups
        SharedExampleGroup.registry.shared_example_groups_for('main', *ancestors[0..-1])
      end

      module TopLevelDSL
        def self.definitions
          proc do
            def shared_examples(*args, &block)
              SharedExampleGroup.registry.add_group('main', *args, &block)
            end
            alias :shared_context      :shared_examples
            alias :share_examples_for  :shared_examples
            alias :shared_examples_for :shared_examples
            def shared_example_groups
              SharedExampleGroup.registry.shared_example_groups_for('main')
            end
          end
        end

        # @private
        def self.exposed_globally?
          @exposed_globally ||= false
        end

        def self.expose_globally!
          return if exposed_globally?
          Core::DSL.change_global_dsl(&definitions)
          @exposed_globally = true
        end

        def self.remove_globally!
          return unless exposed_globally?

          Core::DSL.change_global_dsl do
            undef shared_examples
            undef shared_context
            undef share_examples_for
            undef shared_examples_for
            undef shared_example_groups
          end

          @exposed_globally = false
        end

      end

      def self.registry
        @registry ||= Registry.new
      end

      # @private
      #
      # Used internally to manage the shared example groups and
      # constants. We want to limit the number of methods we add
      # to objects we don't own (main and Module) so this allows
      # us to have helper methods that don't get added to those
      # objects.
      class Registry
        def add_group(source, *args, &block)
          ensure_block_has_source_location(block, CallerFilter.first_non_rspec_line)

          if key? args.first
            key = args.shift
            warn_if_key_taken source, key, block
            add_shared_example_group source, key, block
          end

          unless args.empty?
            mod = Module.new
            (class << mod; self; end).__send__(:define_method, :included) do |host|
              host.class_eval(&block)
            end
            RSpec.configuration.include mod, *args
          end
        end

        def shared_example_groups_for(*sources)
          Collection.new(sources, shared_example_groups)
        end

        def shared_example_groups
          @shared_example_groups ||= Hash.new { |hash, key| hash[key] = Hash.new }
        end

        def clear
          shared_example_groups.clear
        end

      private

        def add_shared_example_group(source, key, block)
          shared_example_groups[source][key] = block
        end

        def key?(candidate)
          [String, Symbol, Module].any? { |cls| cls === candidate }
        end

        def warn_if_key_taken(source, key, new_block)
          return unless existing_block = example_block_for(source, key)

          RSpec.warn_with <<-WARNING.gsub(/^ +\|/, ''), :call_site => nil
            |WARNING: Shared example group '#{key}' has been previously defined at:
            |  #{formatted_location existing_block}
            |...and you are now defining it at:
            |  #{formatted_location new_block}
            |The new definition will overwrite the original one.
          WARNING
        end

        def formatted_location(block)
          block.source_location.join ":"
        end

        def example_block_for(source, key)
          shared_example_groups[source][key]
        end

        if Proc.method_defined?(:source_location)
          def ensure_block_has_source_location(block, caller_line); end
        else # for 1.8.7
          def ensure_block_has_source_location(block, caller_line)
            block.extend Module.new {
              define_method :source_location do
                caller_line.split(':')
              end
            }
          end
        end
      end
    end
  end

  instance_eval(&Core::SharedExampleGroup::TopLevelDSL.definitions)
end