require 'tap/test/env_vars' require 'tap/test/assertions' require 'tap/test/regexp_escape' require 'tap/test/file_test_class' module Tap module Test # FileTest facilitates access and utilization of test-specific files and # directories. FileTest provides each test method with a Tap::Root # (method_root) specific for the method, and defines a new assertion method # (assert_files) to facilitate tests which involve the production and/or # modification of files. # # [file_test_doc_test.rb] # class FileTestDocTest < Test::Unit::TestCase # acts_as_file_test # # def test_something # # each test class has a class test root (ctr) # # and each test method has a method-specific # # root (method_root) # # ctr.root # => File.expand_path(__FILE__.chomp('_test.rb')) # method_root.root # => File.join(ctr.root, "/test_something") # method_root[:input] # => File.join(ctr.root, "/test_something/input") # # # files in the :output and :tmp directories are cleared # # before and after each test; this passes each time the # # test is run with no additional cleanup: # # assert !File.exists?(method_root[:tmp]) # # tmp_file = method_root.prepare(:tmp, 'sample.txt') {|file| file << "content" } # assert_equal "content", File.read(tmp_file) # # # the assert_files method compares files produced # # by the block the expected files, ensuring they # # are the same (see the documentation for the # # simplest use of assert_files) # # expected_file = method_root.prepare(:expected, 'output.txt') {|file| file << 'expected output' } # # # passes # assert_files do # method_root.prepare(:output, 'output.txt') {|file| file << 'expected output' } # end # end # end # # See {Test::Unit::TestCase}[link:classes/Test/Unit/TestCase.html] and # FileTestClass for more information. module FileTest include Tap::Test::EnvVars include Tap::Test::Assertions def self.included(base) # :nodoc: super base.extend FileTestClass base.cleanup_dirs = [:output, :tmp] end # Convenience method to access the class_test_root. def ctr self.class.class_test_root or raise "setup failure: no class_test_root has been set for #{self.class}" end # The test-method-specific Tap::Root which may be used to # access test files. method_root is a duplicate of ctr # reconfigured so that method_root.root is ctr[method_name.to_sym] attr_reader :method_root # Sets up method_root and calls cleanup. Be sure to call super when # overriding this method. def setup super @method_root = ctr.dup.reconfigure(:root => ctr[method_name.to_sym]) cleanup end # Cleans up the method_root.root directory by removing the class # cleanup_dirs (by default :tmp and :output). The root directory # will also be removed if it is empty. # # Override as necessary in subclasses. def cleanup self.class.cleanup_dirs.each do |dir| Utils.clear_dir(method_root[dir]) end Utils.try_remove_dir(method_root.root) end # Calls cleanup unless flagged otherwise by an ENV variable. To prevent # cleanup (when debugging for example), set the 'KEEP_OUTPUTS' or # 'KEEP_FAILURES' ENV variables: # # % rap test KEEP_OUTPUTS=true # % rap test KEEP_FAILURES=true # # Cleanup is only suppressed for failing tests when KEEP_FAILURES is # specified. Be sure to call super when overriding this method. def teardown # check that method_root still exists (nil may # indicate setup was overridden without super) unless method_root raise "teardown failure: method_root is nil (check a class_test_root has been set and ensure setup calls super)" end # clear out the output folder if it exists, unless flagged otherwise unless env_var("KEEP_OUTPUTS") || (!passed? && env_var("KEEP_FAILURES")) begin cleanup rescue raise("cleanup failure: #{$!.message}") end end Utils.try_remove_dir(ctr.root) end # Returns method_name as a string (Ruby 1.9 symbolizes method_name) def method_name_str method_name.to_s end # Runs a file-based test that compares files created by the block with # files in an expected directory. The block receives files from the # input directory, and should return a list of files relative to the # output directory. Only the files returned by the block are compared; # additional files in the output directory are effectively ignored. # # === Example # Lets define a test that transforms input files into output files in a # trivial way, simply by replacing 'input' with 'output' in the file. # # class FileTestDocTest < Test::Unit::TestCase # acts_as_file_test # # def test_assert_files # assert_files do |input_files| # input_files.collect do |filepath| # input = File.read(filepath) # output_file = method_root.filepath(:output, File.basename(filepath)) # # File.open(output_file, "w") do |f| # f << input.gsub(/input/, "output") # end # # output_file # end # end # end # end # # Now say you had some input and expected files for test_assert_files: # # file_test_doc/test_assert_files # |- expected # | |- one.txt # | `- two.txt # `- input # |- one.txt # `- two.txt # # [input/one.txt] # test input 1 # # [input/two.txt] # test input 2 # # [expected/one.txt] # test output 1 # # [expected/two.txt] # test output 2 # # When you run the test, the assert_files passes the input files to the # block. When the block completes, assert_files compares the output # files returned by the block with the files in the expected directory. # In this case, the files are equal and the test passes. # # Say you changed the content of one of the expected files: # # [expected/one.txt] # test flunk 1 # # Now the test fails because the output files aren't equal to the # expected files. The test also fails if there are missing or extra # files. # # === Options # A variety of options adjust the behavior of assert_files: # # :input_dir specify the directory to glob for input files # (default method_root[:input]) # :output_dir specify the output directory # (default method_root[:output]) # :expected_dir specify the directory to glob for expected files # (default method_root[:expected]) # :input_files directly specify the input files for the block # :expected_files directly specify the expected files for comparison # :include_input_directories specifies directories to be included in the # input_files array (by default dirs are excluded) # :include_expected_directories specifies directories to be included in the # expected-output file list comparison # (by default dirs are excluded) # # assert_files will fail if :expected_files was not specified # in the options and no files were found in :expected_dir. This # check tries to prevent silent false-positive results when you forget to # put expected files in their place. # # === File References # # Sometimes the same files will get used across multiple tests. To allow # separate management of test files and prevent duplication, file # references can be provided in place of test files. For instance, with a # test directory like: # # method_root # |- expected # | |- one.txt.ref # | `- two.txt.ref # |- input # | |- one.txt.ref # | `- two.txt.ref # `- ref # |- one.txt # `- two.txt # # The input and expected files (all references in this case) can be # dereferenced to the 'ref' filepaths like so: # # assert_files :reference_dir => method_root[:ref] do |input_files| # input_files # => ['method_root/ref/one.txt', 'method_root/ref/two.txt'] # # input_files.collect do |input_file| # output_file = method_root.filepath(:output, File.basename(input_file) # FileUtils.cp(input_file, output_file) # output_file # end # end # # Dereferencing occurs relative to the input_dir/expected_dir # configurations; a reference_dir must be specified for dereferencing to # occur (see Utils.dereference for more details). # # === Keeping Outputs # # By default FileTest cleans up everything under method_root except the # input and expected directories. For ease in debugging, ENV variable # flags can be specified to prevent cleanup for all tests (KEEP_OUTPUTS) # or just tests that fail (KEEP_FAILURES). These flags can be specified # from the command line if you're running the tests with rake or rap: # # % rake test keep_outputs=true # % rap test keep_failures=true # #-- # TODO: # * add debugging information to indicate, for instance, # when dereferencing is going on. def assert_files(options={}, &block) # :yields: input_files transform_test(block, options) do |expected_file, output_file| unless FileUtils.cmp(expected_file, output_file) flunk "<#{expected_file}> not equal to\n<#{output_file}>" end end end def assert_files_alike(options={}, &block) # :yields: input_files transform_test(block, options) do |expected_file, output_file| regexp = RegexpEscape.new(File.read(expected_file)) str = File.read(output_file) assert_alike(regexp, str, "<#{expected_file}> not equal to\n<#{output_file}>") end end # The default assert_files options def default_assert_files_options { :input_dir => method_root[:input], :output_dir => method_root[:output], :expected_dir => method_root[:expected], :input_files => nil, :expected_files => nil, :include_input_directories => false, :include_expected_directories => false, :reference_dir => nil, :reference_extname => '.ref' } end private def transform_test(block, options={}) # :yields: expected_files, output_files options = default_assert_files_options.merge(options) input_dir = options[:input_dir] output_dir = options[:output_dir] expected_dir = options[:expected_dir] reference_dir = options[:reference_dir] reference_pattern = options[:reference_pattern] Utils.dereference([input_dir, expected_dir], reference_dir, reference_pattern || '**/*.ref') do # Get the input and expected files in this manner: # - look for manually specified files # - glob for files if none were specified # - expand paths and sort # - remove directories unless specified not to do so input_files, expected_files = [:input, :expected].collect do |key| files = options["#{key}_files".to_sym] if files.nil? pattern = File.join(options["#{key}_dir".to_sym], "**/*") files = Dir.glob(pattern) end files = [files].flatten.collect {|file| File.expand_path(file) }.sort unless options["include_#{key}_directories".to_sym] files.delete_if {|file| File.directory?(file)} end files end # check at least one expected file was found if expected_files.empty? && options[:expected_files] == nil flunk "No expected files specified." end # get output files from the block, expand and sort output_files = [*block.call(input_files)].collect do |output_file| File.expand_path(output_file) end.sort # check that the expected and output filepaths are the same translated_expected_files = expected_files.collect do |expected_file| Tap::Root.translate(expected_file, expected_dir, output_dir) end assert_equal translated_expected_files, output_files, "Missing, extra, or unexpected output files" # check that the expected and output file contents are equal errors = [] Utils.each_pair(expected_files, output_files) do |expected_file, output_file| unless (File.directory?(expected_file) && File.directory?(output_file)) || FileUtils.cmp(expected_file, output_file) begin yield(expected_file, output_file) rescue errors << $! end end end flunk "File compare failed:\n" + errors.join("\n") unless errors.empty? end end end end end