diff --git a/Manifest.txt b/Manifest.txt index 8e096ff..5c9dd1e 100644 --- a/Manifest.txt +++ b/Manifest.txt @@ -17,6 +17,7 @@ lib/minitest/pride.rb lib/minitest/pride_plugin.rb lib/minitest/spec.rb lib/minitest/test.rb +lib/minitest/test_task.rb lib/minitest/unit.rb test/minitest/metametameta.rb test/minitest/test_minitest_assertions.rb @@ -25,3 +26,4 @@ test/minitest/test_minitest_mock.rb test/minitest/test_minitest_reporter.rb test/minitest/test_minitest_spec.rb test/minitest/test_minitest_test.rb +test/minitest/test_minitest_test_task.rb diff --git a/README.rdoc b/README.rdoc index cf0be89..41240c6 100644 --- a/README.rdoc +++ b/README.rdoc @@ -70,6 +70,7 @@ extract-method refactorings still apply. * minitest/mock - a simple and clean mock/stub system. * minitest/benchmark - an awesome way to assert your algorithm's performance. * minitest/pride - show your pride in testing! +* minitest/test_task - a full-featured and clean rake task generator. * Incredibly small and fast runner, but no bells and whistles. * Written by squishy human beings. Software can never be perfect. We will all eventually die. @@ -264,9 +265,8 @@ new non-existing method: === Running Your Tests -Ideally, you'll use a rake task to run your tests, either piecemeal or -all at once. Both rake and rails ship with rake tasks for running your -tests. BUT! You don't have to: +Ideally, you'll use a rake task to run your tests (see below), either +piecemeal or all at once. BUT! You don't have to: % ruby -Ilib:test test/minitest/test_minitest_test.rb Run options: --seed 37685 @@ -294,18 +294,45 @@ provided via plugins. To see them, simply run with +--help+: -p, --pride Pride. Show your testing pride! -a, --autotest Connect to autotest server. +=== Rake Tasks + You can set up a rake task to run all your tests by adding this to your Rakefile: - require "rake/testtask" + require "minitest/test_task" - Rake::TestTask.new(:test) do |t| + Minitest::TestTask.create # named test, sensible defaults + + # or more explicitly: + + Minitest::TestTask.create(:test) do |t| t.libs << "test" t.libs << "lib" - t.test_files = FileList["test/**/test_*.rb"] + t.warning = false + t.test_globs = ["test/**/*_test.rb"] end task :default => :test +Each of these will generate 4 tasks: + + rake test :: Run the test suite. + rake test:cmd :: Print out the test command. + rake test:isolated :: Show which test files fail when run separately. + rake test:slow :: Show bottom 25 tests sorted by time. + +=== Rake Task Variables + +There are a bunch of variables you can supply to rake to modify the run. + + MT_LIB_EXTRAS :: Extra libs to dynamically override/inject for custom runs. + N :: -n: Tests to run (string or /regexp/). + X :: -x: Tests to exclude (string or /regexp/). + A :: Any extra arguments. Honors shell quoting. + MT_CPU :: How many threads to use for parallel test runs + SEED :: -s --seed Sets random seed. + TESTOPTS :: Deprecated, same as A + FILTER :: Deprecated, same as A + == Writing Extensions To define a plugin, add a file named minitest/XXX_plugin.rb to your diff --git a/lib/minitest/test_task.rb b/lib/minitest/test_task.rb new file mode 100644 index 0000000..a2ac948 --- /dev/null +++ b/lib/minitest/test_task.rb @@ -0,0 +1,305 @@ +require "shellwords" +require "rbconfig" +require "rake/tasklib" + +module Minitest # :nodoc: + + ## + # Minitest::TestTask is a rake helper that generates several rake + # tasks under the main test task's name-space. + # + # task :: the main test task + # task :cmd :: prints the command to use + # task :deps :: runs each test file by itself to find dependency errors + # task :slow :: runs the tests and reports the slowest 25 tests. + # + # Examples: + # + # Minitest::TestTask.create + # + # The most basic and default setup. + # + # Minitest::TestTask.create :my_tests + # + # The most basic/default setup, but with a custom name + # + # Minitest::TestTask.create :unit do |t| + # t.test_globs = ["test/unit/**/*_test.rb"] + # t.warning = false + # end + # + # Customize the name and only run unit tests. + + class TestTask < Rake::TaskLib + WINDOWS = RbConfig::CONFIG["host_os"] =~ /mswin|mingw/ # :nodoc: + + ## + # Create several test-oriented tasks under +name+. Takes an + # optional block to customize variables. + + def self.create name = :test, &block + task = new name + task.instance_eval(&block) if block + task.process_env + task.define + task + end + + ## + # Extra arguments to pass to the tests. Defaults empty but gets + # populated by a number of enviroment variables: + # + # N (-n flag) :: a string or regexp of tests to run. + # X (-e flag) :: a string or regexp of tests to exclude. + # A (arg) :: quick way to inject an arbitrary argument (eg A=--help). + # + # See #process_env + + attr_accessor :extra_args + + ## + # The code to load the framework. Defaults to requiring + # minitest/autorun... + # + # Why do I have this as an option? + + attr_accessor :framework + + ## + # Extra library directories to include. Defaults to %w[lib test + # .]. Also uses $MT_LIB_EXTRAS allowing you to dynamically + # override/inject directories for custom runs. + + attr_accessor :libs + + ## + # The name of the task and base name for the other tasks generated. + + attr_accessor :name + + ## + # File globs to find test files. Defaults to something sensible to + # find test files under the test directory. + + attr_accessor :test_globs + + ## + # Turn on ruby warnings (-w flag). Defaults to true. + + attr_accessor :warning + + ## + # Optional: Additional ruby to run before the test framework is loaded. + + attr_accessor :test_prelude + + ## + # Print out commands as they run. Defaults to Rake's +trace+ (-t + # flag) option. + + attr_accessor :verbose + + ## + # Use TestTask.create instead. + + def initialize name = :test # :nodoc: + self.extra_args = [] + self.framework = %(require "minitest/autorun") + self.libs = %w[lib test .] + self.name = name + self.test_globs = ["test/**/test_*.rb", + "test/**/*_test.rb"] + self.test_prelude = nil + self.verbose = Rake.application.options.trace + self.warning = true + end + + ## + # Extract variables from the environment and convert them to + # command line arguments. See #extra_args. + # + # Environment Variables: + # + # MT_LIB_EXTRAS :: Extra libs to dynamically override/inject for custom runs. + # N :: Tests to run (string or /regexp/). + # X :: Tests to exclude (string or /regexp/). + # A :: Any extra arguments. Honors shell quoting. + # + # Deprecated: + # + # TESTOPTS :: For argument passing, use +A+. + # N :: For parallel testing, use +MT_CPU+. + # FILTER :: Same as +TESTOPTS+. + + def process_env + warn "TESTOPTS is deprecated in Minitest::TestTask. Use A instead" if + ENV["TESTOPTS"] + warn "FILTER is deprecated in Minitest::TestTask. Use A instead" if + ENV["FILTER"] + warn "N is deprecated in Minitest::TestTask. Use MT_CPU instead" if + ENV["N"] && ENV["N"].to_i > 0 + + lib_extras = (ENV["MT_LIB_EXTRAS"] || "").split File::PATH_SEPARATOR + self.libs[0,0] = lib_extras + + extra_args << "-n" << ENV["N"] if ENV["N"] + extra_args << "-e" << ENV["X"] if ENV["X"] + extra_args.concat Shellwords.split(ENV["TESTOPTS"]) if ENV["TESTOPTS"] + extra_args.concat Shellwords.split(ENV["FILTER"]) if ENV["FILTER"] + extra_args.concat Shellwords.split(ENV["A"]) if ENV["A"] + + ENV.delete "N" if ENV["N"] + + # TODO? RUBY_DEBUG = ENV["RUBY_DEBUG"] + # TODO? ENV["RUBY_FLAGS"] + + extra_args.compact! + end + + def define # :nodoc: + default_tasks = [] + + desc "Run the test suite. Use N, X, A, and TESTOPTS to add flags/args." + task name do + ruby make_test_cmd, verbose:verbose + end + + desc "Print out the test command. Good for profiling and other tools." + task "#{name}:cmd" do + puts "ruby #{make_test_cmd}" + end + + desc "Show which test files fail when run in isolation." + task "#{name}:isolated" do + tests = Dir[*self.test_globs].uniq + + # 3 seems to be the magic number... (tho not by that much) + bad, good, n = {}, [], (ENV.delete("K") || 3).to_i + file = ENV.delete("F") + times = {} + + tt0 = Time.now + + n.threads_do tests.sort do |path| + t0 = Time.now + output = `#{Gem.ruby} #{make_test_cmd path} 2>&1` + t1 = Time.now - t0 + + times[path] = t1 + + if $?.success? + $stderr.print "." + good << path + else + $stderr.print "x" + bad[path] = output + end + end + + puts "done" + puts "Ran in %.2f seconds" % [ Time.now - tt0 ] + + if file then + require "json" + File.open file, "w" do |io| + io.puts JSON.pretty_generate times + end + end + + unless good.empty? + puts + puts "# Good tests:" + puts + good.sort.each do |path| + puts "%.2fs: %s" % [times[path], path] + end + end + + unless bad.empty? + puts + puts "# Bad tests:" + puts + bad.keys.sort.each do |path| + puts "%.2fs: %s" % [times[path], path] + end + puts + puts "# Bad Test Output:" + puts + bad.sort.each do |path, output| + puts + puts "# #{path}:" + puts output + end + exit 1 + end + end + + task "#{name}:deps" => "#{name}:isolated" # now just an alias + + desc "Show bottom 25 tests wrt time." + task "#{name}:slow" do + sh ["rake #{name} TESTOPTS=-v", + "egrep '#test_.* s = .'", + "sort -n -k2 -t=", + "tail -25"].join " | " + end + + default_tasks << name + + desc "Run the default task(s)." + task :default => default_tasks + end + + ## + # Generate the test command-line. + + def make_test_cmd globs = test_globs + tests = [] + tests.concat Dir[*globs].sort.shuffle # TODO: SEED -> srand first? + tests.map! { |f| %(require "#{f}") } + + runner = [] + runner << test_prelude if test_prelude + runner << framework + runner.concat tests + runner = runner.join "; " + + args = [] + args << "-I#{libs.join(File::PATH_SEPARATOR)}" unless libs.empty? + args << "-w" if warning + args << '-e' + args << "'#{runner}'" + args << '--' + args << extra_args.map(&:shellescape) + + args.join " " + end + end +end + +class Work < Queue + def initialize jobs = [] + super() + + jobs.each do |job| + self << job + end + + close + end +end + +class Integer + def threads_do(jobs) # :nodoc: + require "thread" + q = Work.new jobs + + self.times.map { + Thread.new do + while job = q.pop # go until quit value + yield job + end + end + }.each(&:join) + end +end diff --git a/test/minitest/test_minitest_test_task.rb b/test/minitest/test_minitest_test_task.rb new file mode 100644 index 0000000..11b2618 --- /dev/null +++ b/test/minitest/test_minitest_test_task.rb @@ -0,0 +1,46 @@ +require "minitest/autorun" +require "hoe" + +require "minitest/test_task" + +Hoe.load_plugins # make sure Hoe::Test is loaded + +class TestHoeTest < Minitest::Test + PATH = "test/minitest/test_minitest_test_task.rb" + + mt_path = %w[lib test .].join File::PATH_SEPARATOR + + MT_EXPECTED = %W[-I#{mt_path} -w + -e '%srequire "#{PATH}"' + --].join(" ") + " " + + def test_make_test_cmd_for_minitest + skip "Using TESTOPTS... skipping" if ENV["TESTOPTS"] + + require "minitest/test_task" + + framework = %(require "minitest/autorun"; ) + + @tester = Minitest::TestTask.create :test do |t| + t.test_globs = [PATH] + end + + assert_equal MT_EXPECTED % [framework].join("; "), @tester.make_test_cmd + end + + def test_make_test_cmd_for_minitest_prelude + skip "Using TESTOPTS... skipping" if ENV["TESTOPTS"] + + require "minitest/test_task" + + prelude = %(require "other/file") + framework = %(require "minitest/autorun"; ) + + @tester = Minitest::TestTask.create :test do |t| + t.test_prelude = prelude + t.test_globs = [PATH] + end + + assert_equal MT_EXPECTED % [prelude, framework].join("; "), @tester.make_test_cmd + end +end