sinisterchipmunk/genspec

View on GitHub
lib/genspec/generator_example_group.rb

Summary

Maintainability
A
35 mins
Test Coverage
module GenSpec
  module GeneratorExampleGroup
    include RSpec::Matchers
    include GenSpec::Matchers

    def self.included(base)
      base.send(:extend, GenSpec::GeneratorExampleGroup::ClassMethods)
      base.send(:subject) { generator_descriptor }
    end
    
    def within_source_root(&block)
      generator_init_blocks << block
    end
    
    def generator_init_blocks
      @generator_init_blocks ||= self.class.generator_init_blocks.dup
    end
    
    # A hash containing the following:
    #
    #   :described   - the generator to be tested, or the string/symbol representing it
    #   :args        - any arguments to be used when invoking the generator
    #   :input       - a string to be used as an input stream, or nil
    #   :init_blocks - an array of blocks to be invoked prior to running the generator
    #   :generator_options - a hash of options to be passed into the generator
    #
    # This hash represents the +subject+ of the spec and this is the object that will
    # ultimately be passed into the GenSpec matchers.
    #
    def generator_descriptor
      {
        :described => self.class.generator,
        :args => self.class.generator_args,
        :input => self.class.generator_input,
        :output => self.class.generator_output,
        :init_blocks => generator_init_blocks,
        :generator_options => self.class.generator_options
      }
    end
    
    module ClassMethods
      # Sets the list of arguments for this generator.
      #
      # * All arguments will be converted to Strings, because that's how
      #   they'd enter the generator from a command line. To avoid this,
      #   pass :object => true at the end;
      #
      # Ex:
      #
      #   with_args '--orm', 'active_record' do
      #     it "should use activerecord" do
      #       # . . .
      #     end
      #   end
      #
      #   with_args '--size', 5, :object => true do
      #     # . . .
      #   end
      #
      def with_args(*args, &block)
        options = args.last.kind_of?(Hash) ? args.pop : {}
        args = args.flatten.collect { |c| c.to_s } unless options[:object]
        
        if block_given?
          context "with arguments #{args.inspect}" do
            with_args(generator_args + args, options)
            instance_eval(&block)
          end
        else
          metadata[:generator_args] = args
        end
      end
      
      # Allows you to pass options directly into the generator, such as
      # :shell, :behavior, etc.
      #
      # Ex:
      #
      #   # simulate a destroy generator, per `rails destroy controller ...`
      #   with_generator_options :behavior => :revoke do
      #     # . . .
      #   end
      #
      def with_generator_options(options, &block)
        if block_given?
          context "with generator options #{options.inspect}" do
            with_generator_options options
            instance_eval &block
          end
        else
          generator_options.merge! options
        end
      end
      
      # Sets the input stream for this generator.
      #
      # Ex:
      #
      #   with_input <<-end_input do
      #     y
      #     n
      #     a
      #   end_input
      #     it "should overwrite, then skip, then overwrite all" do
      #       # . . .
      #     end
      #   end
      #
      def with_input(string, &block)
        if block_given?
          context "with input string #{string.inspect}" do
            with_input string
            instance_eval &block
          end
        else
          metadata[:generator_input] = string
        end
      end
      
      # Executes some code within the generator's source root
      # prior to the generator actually running. Useful for
      # setting up fixtures.
      #
      # Ex:
      #
      #   within_source_root do
      #     touch "Gemfile"
      #   end
      #
      # Optionally, the block may receive a single argument,
      # which is the full path to the temporary directory
      # representing the source root:
      #
      #   within_source_root do |tempdir|
      #     # . . .
      #   end
      #
      def within_source_root(&block)
        metadata[:generator_init_block] = block
      end
      
      # Returns an array of all init blocks from the topmost context down to this
      # one, in that order. These blocks will be executed sequentially prior to
      # each run of the generator.
      def generator_init_blocks
        result = []
        result.concat superclass.generator_init_blocks if genspec_subclass?
        result << metadata[:generator_init_block] if metadata[:generator_init_block]
        result
      end
      
      # Returns the generator arguments to be used for this context. If this context doesn't
      # have any generator arguments, its superclass is checked, and so on until either the
      # parent isn't a GenSpec or a set of arguments is found. Only the closest argument
      # set is used; any sets specified above the discovered argument set are
      # ignored.
      def generator_args
        return metadata[:generator_args] if metadata[:generator_args]
        
        metadata[:generator_args] = if genspec_subclass?
          superclass.generator_args
        else
          []
        end
      end
      
      # Returns the hash of options to be passed into the generator in this context.
      def generator_options
        return metadata[:generator_options] if metadata[:generator_options]
        
        metadata[:generator_options] = if genspec_subclass?
          superclass.generator_options.dup
        else
          { }
        end
      end
      
      # Returns the generator output string or IO, or nil.
      def generator_output
        return metadata[:generator_output] if metadata[:generator_output]
        
        metadata[:generator_output] = if genspec_subclass?
          superclass.generator_output
        else
          nil
        end
      end
      
      # Returns the input stream to be used for this context. If this context doesn't
      # have an input stream, its superclass is checked, and so on until either the
      # parent isn't a GenSpec or an input stream is found. Only the closest input
      # stream is used; any streams specified above the discovered input stream are
      # ignored.
      def generator_input
        return metadata[:generator_input] if metadata[:generator_input]
        
        metadata[:generator_input] = if genspec_subclass?
          superclass.generator_input
        else
          nil
        end
      end
      
      alias before_generation   within_source_root
      alias with_arguments      with_args
      alias generator_arguments generator_args
      
      # Traverses up the context tree to find the topmost description, which represents
      # the controller to be tested or the string/symbol representing it.
      #
      # If name is specified, it will be used instead and subsequent calls to this method
      # will return the specified name.
      def generator(name = nil)
        metadata[:generator_name] = name.to_s if name
        return metadata[:generator_name] if metadata[:generator_name]
        
        if genspec_subclass?
          superclass.generator
        else
          described_class || description
        end
      end

      # Returns true if this object's superclass is also a GenSpec.
      #
      # When a context is created, rspec creates a class inheriting from the context's
      # parent. Therefore, this method can be used to recurse up to the highest-level
      # spec that still tests a generator.
      def genspec_subclass?
        superclass.include?(GenSpec::GeneratorExampleGroup)
      end
    end
  end
end