|
| 1 | +require "shellwords" |
| 2 | +require "rbconfig" |
| 3 | +require "rake/tasklib" |
| 4 | + |
| 5 | +module Minitest # :nodoc: |
| 6 | + |
| 7 | + ## |
| 8 | + # Minitest::TestTask is a rake helper that generates several rake |
| 9 | + # tasks under the main test task's name-space. |
| 10 | + # |
| 11 | + # task <name> :: the main test task |
| 12 | + # task <name>:cmd :: prints the command to use |
| 13 | + # task <name>:deps :: runs each test file by itself to find dependency errors |
| 14 | + # task <name>:slow :: runs the tests and reports the slowest 25 tests. |
| 15 | + # |
| 16 | + # Examples: |
| 17 | + # |
| 18 | + # Minitest::TestTask.create |
| 19 | + # |
| 20 | + # The most basic and default setup. |
| 21 | + # |
| 22 | + # Minitest::TestTask.create :my_tests |
| 23 | + # |
| 24 | + # The most basic/default setup, but with a custom name |
| 25 | + # |
| 26 | + # Minitest::TestTask.create :unit do |t| |
| 27 | + # t.test_globs = ["test/unit/**/*_test.rb"] |
| 28 | + # t.warning = false |
| 29 | + # end |
| 30 | + # |
| 31 | + # Customize the name and only run unit tests. |
| 32 | + |
| 33 | + class TestTask < Rake::TaskLib |
| 34 | + WINDOWS = RbConfig::CONFIG["host_os"] =~ /mswin|mingw/ # :nodoc: |
| 35 | + |
| 36 | + ## |
| 37 | + # Create several test-oriented tasks under +name+. Takes an |
| 38 | + # optional block to customize variables. |
| 39 | + |
| 40 | + def self.create name = :test, &block |
| 41 | + task = new name |
| 42 | + task.instance_eval(&block) if block |
| 43 | + task.process_env |
| 44 | + task.define |
| 45 | + task |
| 46 | + end |
| 47 | + |
| 48 | + ## |
| 49 | + # Extra arguments to pass to the tests. Defaults empty but gets |
| 50 | + # populated by a number of enviroment variables: |
| 51 | + # |
| 52 | + # N (-n flag) :: a string or regexp of tests to run. |
| 53 | + # X (-e flag) :: a string or regexp of tests to exclude. |
| 54 | + # A (arg) :: quick way to inject an arbitrary argument (eg A=--help). |
| 55 | + # |
| 56 | + # See #process_env |
| 57 | + |
| 58 | + attr_accessor :extra_args |
| 59 | + |
| 60 | + ## |
| 61 | + # The code to load the framework. Defaults to requiring |
| 62 | + # minitest/autorun... |
| 63 | + # |
| 64 | + # Why do I have this as an option? |
| 65 | + |
| 66 | + attr_accessor :framework |
| 67 | + |
| 68 | + ## |
| 69 | + # Extra library directories to include. Defaults to %w[lib test |
| 70 | + # .]. Also uses $MT_LIB_EXTRAS allowing you to dynamically |
| 71 | + # override/inject directories for custom runs. |
| 72 | + |
| 73 | + attr_accessor :libs |
| 74 | + |
| 75 | + ## |
| 76 | + # The name of the task and base name for the other tasks generated. |
| 77 | + |
| 78 | + attr_accessor :name |
| 79 | + |
| 80 | + ## |
| 81 | + # File globs to find test files. Defaults to something sensible to |
| 82 | + # find test files under the test directory. |
| 83 | + |
| 84 | + attr_accessor :test_globs |
| 85 | + |
| 86 | + ## |
| 87 | + # Turn on ruby warnings (-w flag). Defaults to true. |
| 88 | + |
| 89 | + attr_accessor :warning |
| 90 | + |
| 91 | + ## |
| 92 | + # Optional: Additional ruby to run before the test framework is loaded. |
| 93 | + |
| 94 | + attr_accessor :test_prelude |
| 95 | + |
| 96 | + ## |
| 97 | + # Print out commands as they run. Defaults to Rake's +trace+ (-t |
| 98 | + # flag) option. |
| 99 | + |
| 100 | + attr_accessor :verbose |
| 101 | + |
| 102 | + ## |
| 103 | + # Use TestTask.create instead. |
| 104 | + |
| 105 | + def initialize name = :test # :nodoc: |
| 106 | + self.extra_args = [] |
| 107 | + self.framework = %(require "minitest/autorun") |
| 108 | + self.libs = %w[lib test .] |
| 109 | + self.name = name |
| 110 | + self.test_globs = ["test/**/test_*.rb", |
| 111 | + "test/**/*_test.rb"] |
| 112 | + self.test_prelude = nil |
| 113 | + self.verbose = Rake.application.options.trace |
| 114 | + self.warning = true |
| 115 | + end |
| 116 | + |
| 117 | + ## |
| 118 | + # Extract variables from the environment and convert them to |
| 119 | + # command line arguments. See #extra_args. |
| 120 | + # |
| 121 | + # Environment Variables: |
| 122 | + # |
| 123 | + # MT_LIB_EXTRAS :: Extra libs to dynamically override/inject for custom runs. |
| 124 | + # N :: Tests to run (string or /regexp/). |
| 125 | + # X :: Tests to exclude (string or /regexp/). |
| 126 | + # A :: Any extra arguments. Honors shell quoting. |
| 127 | + # |
| 128 | + # Deprecated: |
| 129 | + # |
| 130 | + # TESTOPTS :: For argument passing, use +A+. |
| 131 | + # N :: For parallel testing, use +MT_CPU+. |
| 132 | + # FILTER :: Same as +TESTOPTS+. |
| 133 | + |
| 134 | + def process_env |
| 135 | + warn "TESTOPTS is deprecated in Minitest::TestTask. Use A instead" if |
| 136 | + ENV["TESTOPTS"] |
| 137 | + warn "FILTER is deprecated in Minitest::TestTask. Use A instead" if |
| 138 | + ENV["FILTER"] |
| 139 | + warn "N is deprecated in Minitest::TestTask. Use MT_CPU instead" if |
| 140 | + ENV["N"] && ENV["N"].to_i > 0 |
| 141 | + |
| 142 | + lib_extras = (ENV["MT_LIB_EXTRAS"] || "").split File::PATH_SEPARATOR |
| 143 | + self.libs[0,0] = lib_extras |
| 144 | + |
| 145 | + extra_args << "-n" << ENV["N"] if ENV["N"] |
| 146 | + extra_args << "-e" << ENV["X"] if ENV["X"] |
| 147 | + extra_args.concat Shellwords.split(ENV["TESTOPTS"]) if ENV["TESTOPTS"] |
| 148 | + extra_args.concat Shellwords.split(ENV["FILTER"]) if ENV["FILTER"] |
| 149 | + extra_args.concat Shellwords.split(ENV["A"]) if ENV["A"] |
| 150 | + |
| 151 | + ENV.delete "N" if ENV["N"] |
| 152 | + |
| 153 | + # TODO? RUBY_DEBUG = ENV["RUBY_DEBUG"] |
| 154 | + # TODO? ENV["RUBY_FLAGS"] |
| 155 | + |
| 156 | + extra_args.compact! |
| 157 | + end |
| 158 | + |
| 159 | + def define # :nodoc: |
| 160 | + default_tasks = [] |
| 161 | + |
| 162 | + desc "Run the test suite. Use N, X, A, and TESTOPTS to add flags/args." |
| 163 | + task name do |
| 164 | + ruby make_test_cmd, verbose:verbose |
| 165 | + end |
| 166 | + |
| 167 | + desc "Print out the test command. Good for profiling and other tools." |
| 168 | + task "#{name}:cmd" do |
| 169 | + puts "ruby #{make_test_cmd}" |
| 170 | + end |
| 171 | + |
| 172 | + desc "Show which test files fail when run in isolation." |
| 173 | + task "#{name}:isolated" do |
| 174 | + tests = Dir[*self.test_globs].uniq |
| 175 | + |
| 176 | + # 3 seems to be the magic number... (tho not by that much) |
| 177 | + bad, good, n = {}, [], (ENV.delete("K") || 3).to_i |
| 178 | + file = ENV.delete("F") |
| 179 | + times = {} |
| 180 | + |
| 181 | + tt0 = Time.now |
| 182 | + |
| 183 | + n.threads_do tests.sort do |path| |
| 184 | + t0 = Time.now |
| 185 | + output = `#{Gem.ruby} #{make_test_cmd path} 2>&1` |
| 186 | + t1 = Time.now - t0 |
| 187 | + |
| 188 | + times[path] = t1 |
| 189 | + |
| 190 | + if $?.success? |
| 191 | + $stderr.print "." |
| 192 | + good << path |
| 193 | + else |
| 194 | + $stderr.print "x" |
| 195 | + bad[path] = output |
| 196 | + end |
| 197 | + end |
| 198 | + |
| 199 | + puts "done" |
| 200 | + puts "Ran in %.2f seconds" % [ Time.now - tt0 ] |
| 201 | + |
| 202 | + if file then |
| 203 | + require "json" |
| 204 | + File.open file, "w" do |io| |
| 205 | + io.puts JSON.pretty_generate times |
| 206 | + end |
| 207 | + end |
| 208 | + |
| 209 | + unless good.empty? |
| 210 | + puts |
| 211 | + puts "# Good tests:" |
| 212 | + puts |
| 213 | + good.sort.each do |path| |
| 214 | + puts "%.2fs: %s" % [times[path], path] |
| 215 | + end |
| 216 | + end |
| 217 | + |
| 218 | + unless bad.empty? |
| 219 | + puts |
| 220 | + puts "# Bad tests:" |
| 221 | + puts |
| 222 | + bad.keys.sort.each do |path| |
| 223 | + puts "%.2fs: %s" % [times[path], path] |
| 224 | + end |
| 225 | + puts |
| 226 | + puts "# Bad Test Output:" |
| 227 | + puts |
| 228 | + bad.sort.each do |path, output| |
| 229 | + puts |
| 230 | + puts "# #{path}:" |
| 231 | + puts output |
| 232 | + end |
| 233 | + exit 1 |
| 234 | + end |
| 235 | + end |
| 236 | + |
| 237 | + task "#{name}:deps" => "#{name}:isolated" # now just an alias |
| 238 | + |
| 239 | + desc "Show bottom 25 tests wrt time." |
| 240 | + task "#{name}:slow" do |
| 241 | + sh ["rake #{name} TESTOPTS=-v", |
| 242 | + "egrep '#test_.* s = .'", |
| 243 | + "sort -n -k2 -t=", |
| 244 | + "tail -25"].join " | " |
| 245 | + end |
| 246 | + |
| 247 | + default_tasks << name |
| 248 | + |
| 249 | + desc "Run the default task(s)." |
| 250 | + task :default => default_tasks |
| 251 | + end |
| 252 | + |
| 253 | + ## |
| 254 | + # Generate the test command-line. |
| 255 | + |
| 256 | + def make_test_cmd globs = test_globs |
| 257 | + tests = [] |
| 258 | + tests.concat Dir[*globs].sort.shuffle # TODO: SEED -> srand first? |
| 259 | + tests.map! { |f| %(require "#{f}") } |
| 260 | + |
| 261 | + runner = [] |
| 262 | + runner << test_prelude if test_prelude |
| 263 | + runner << framework |
| 264 | + runner.concat tests |
| 265 | + runner = runner.join "; " |
| 266 | + |
| 267 | + args = [] |
| 268 | + args << "-I#{libs.join(File::PATH_SEPARATOR)}" unless libs.empty? |
| 269 | + args << "-w" if warning |
| 270 | + args << '-e' |
| 271 | + args << "'#{runner}'" |
| 272 | + args << '--' |
| 273 | + args << extra_args.map(&:shellescape) |
| 274 | + |
| 275 | + args.join " " |
| 276 | + end |
| 277 | + end |
| 278 | +end |
| 279 | + |
| 280 | +class Work < Queue |
| 281 | + def initialize jobs = [] |
| 282 | + super() |
| 283 | + |
| 284 | + jobs.each do |job| |
| 285 | + self << job |
| 286 | + end |
| 287 | + |
| 288 | + close |
| 289 | + end |
| 290 | +end |
| 291 | + |
| 292 | +class Integer |
| 293 | + def threads_do(jobs) # :nodoc: |
| 294 | + require "thread" |
| 295 | + q = Work.new jobs |
| 296 | + |
| 297 | + self.times.map { |
| 298 | + Thread.new do |
| 299 | + while job = q.pop # go until quit value |
| 300 | + yield job |
| 301 | + end |
| 302 | + end |
| 303 | + }.each(&:join) |
| 304 | + end |
| 305 | +end |
0 commit comments