mrackwitz/CLIntegracon

View on GitHub
lib/CLIntegracon/file_tree_spec.rb

Summary

Maintainability
A
1 hr
Test Coverage
require 'pathname'
require 'CLIntegracon/diff'
require 'CLIntegracon/formatter'

module CLIntegracon
  # FileTreeSpec represents a single specification, which is mirrored
  # on the file system in the spec directory by a direct children.
  # It contains a before directory (#before_path) and an after
  # directory (#after_path) or if it is initialized with a #base_spec,
  # the before directory of this spec is used. The before directory
  # contents in the #spec_path of the child spec, can contain further
  # files, which overwrite, if given, the inherited contents.
  #
  class FileTreeSpec

    # @return [FileTreeSpecContext]
    #         The context, which configures path and file behaviors
    attr_reader :context

    # @return [String]
    #         The concrete spec folder
    attr_reader :spec_folder

    # @return [Pathname]
    #         The concrete spec path
    def spec_path
      context.spec_path + spec_folder
    end

    # @return [Pathname]
    #         The concrete before directory for this spec
    def before_path
      spec_path + context.before_dir
    end

    # @return [Pathname]
    #         The concrete after directory for this spec
    def after_path
      spec_path + context.after_dir
    end

    # @return [Pathname]
    #         The concrete temp directory for this spec
    def temp_path
      context.temp_path + spec_folder
    end

    # @return [Pathname]
    #         The concrete temp raw directory for this spec
    def temp_raw_path
      temp_path + 'raw'
    end

    # @return [Pathname]
    #         The concrete transformed temp directory for this spec
    def temp_transformed_path
      temp_path + 'transformed'
    end

    # @return [String|NilClass]
    #         The name of an optional #base_spec.
    attr_reader :base_spec_name

    # Return whether this spec is based on another spec.
    #
    # @return  [Bool]
    #
    def has_base?
      !base_spec_name.nil?
    end

    # @return [FileTreeSpec|NilClass]
    #         The spec on whose #after_path will be used as #before_path
    #         for this spec.
    def base_spec
      has_base? ? context.spec(base_spec_name) : nil
    end

    # Init a spec with a given context
    #
    # @param  [FileTreeSpecContext] context
    #         The context, which configures path and file behaviors
    #
    # @param  [String] spec_folder
    #         The concrete spec folder
    #
    # @param  [String] based_on
    #         @see #base_spec_name
    #
    def initialize(context, spec_folder, based_on: nil)
      @context = context
      @spec_folder = spec_folder
      @base_spec_name = based_on
    end

    # Run this spec
    #
    # @param  [Block<(FileTreeSpec)->()>] block
    #         The block, which will be executed after chdir into the created temporary
    #         directory. In this block you will likely run your modifications to the
    #         file system and use the received FileTreeSpec instance to make asserts
    #         with the test framework of your choice.
    #
    def run(&block)
      prepare!

      copy_files!

      Dir.chdir(temp_transformed_path) do
        block.call self
      end
    end

    # Compares the expected and produced directory by using the rules
    # defined in the context
    #
    # @param  [Block<(Diff)->()>] diff_block
    #         The block, where you will likely define a test for each file to compare.
    #         It will receive a Diff of each of the expected and produced files.
    #
    def compare(&diff_block)
      # Get a copy of the outputs before any transformations are applied
      FileUtils.cp_r("#{temp_transformed_path}/.", temp_raw_path)

      transform_paths!

      glob_all(after_path).each do |relative_path|
        expected = after_path + relative_path

        next unless expected.file?
        next if context.ignores?(relative_path)

        block = context.preprocessors_for(relative_path).first
        diff = diff_files(expected, relative_path, &block)

        diff_block.call diff
      end
    end

    # Compares the expected and produced directory by using the rules
    # defined in the context for unexpected files.
    #
    # This is separate because you probably don't want to define an extra
    # test case for each file, which wasn't expected at all. So you can
    # keep your test cases consistent.
    #
    # @param  [Block<(Array)->()>] diff_block
    #         The block, where you will likely define a test that no unexpected files exists.
    #         It will receive an Array.
    #
    def check_unexpected_files(&block)
      expected_files = glob_all after_path
      produced_files = glob_all
      unexpected_files = produced_files - expected_files

      # Select only files
      unexpected_files.select! { |path| path.file? }

      # Filter ignored paths
      unexpected_files.reject! { |path| context.ignores?(path) }

      block.call unexpected_files
    end

    # Return a Formatter
    #
    # @return [Formatter]
    #
    def formatter
      @formatter ||= Formatter.new(self)
    end

    protected

      # Prepare the temporary directory
      #
      def prepare!
        context.prepare!

        temp_path.rmtree if temp_path.exist?
        temp_path.mkdir
        temp_raw_path.mkdir
        temp_transformed_path.mkdir
      end

      # Copies the before subdirectory of the given tests folder in the raw
      # directory.
      #
      def copy_files!
        destination = temp_transformed_path

        if has_base?
          FileUtils.cp_r("#{base_spec.temp_raw_path}/.", destination)
        end

        begin
          FileUtils.cp_r("#{before_path}/.", destination)
        rescue Errno::ENOENT => e
          raise e unless has_base?
        end
      end

      # Applies the in the context configured transformations.
      #
      def transform_paths!
        glob_all.each do |path|
          context.transformers_for(path).each do |transformer|
            transformer.call(path) if path.exist?
          end
          path.rmtree if context.ignores?(path) && path.exist?
        end
      end

      # Searches recursively for all files and take care for including hidden files
      # if this is configured in the context.
      #
      # @param  [String] path
      #         The relative or absolute path to search in (optional)
      #
      # @return [Array<Pathname>]
      #
      def glob_all(path=nil)
        Dir.chdir path || '.' do
          Pathname.glob("**/*", context.include_hidden_files? ? File::FNM_DOTMATCH : 0).sort.reject do |p|
            %w(. ..).include?(p.basename.to_s)
          end
        end
      end

      # Compares two files to check if they are identical and produces a clear diff
      # to highlight the differences.
      #
      # @param  [Pathname] expected
      #         The file in the after directory
      #
      # @param  [Pathname] relative_path
      #         The file in the temp directory
      #
      # @param  [Block<(Pathname)->(to_s)>] block
      #         the block, which transforms the files in a better comparable form
      #
      # @return [Diff]
      #         An object holding a diff
      #
      def diff_files(expected, relative_path, &block)
        produced = temp_transformed_path + relative_path
        Diff.new(expected, produced, relative_path, &block)
      end

  end
end