require "shellwords" require "rbconfig" require "rake/tasklib" warn <<~EOM minitest/test_task.rb is now packaged with minitest. If you see this, you are getting it from hoe instead. If you're NOT able to upgrade minitest to pick this up, please drop an issue on seattlerb/hoe and let me know. Required from #{caller[2]} EOM 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,spec}_*.rb", "test/**/*_{test,spec}.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