summaryrefslogtreecommitdiff
path: root/lib/minitest/test_task.rb
blob: cee8c4e20b45b4de02ece907494c247497d3504c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
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 <name>      :: the main test task
  #   task <name>:cmd  :: prints the command to use
  #   task <name>:deps :: runs each test file by itself to find dependency errors
  #   task <name>: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