Class: Minitest::Utils::CLI

Inherits:
Object
  • Object
show all
Defined in:
lib/minitest/utils/cli.rb

Constant Summary collapse

MATCHER =
/^(\s+(?:(?<short>-[a-zA-Z]+), )?(?<long>[^ ]+) +)(?<description>.*?)$/
<<~TEXT
  A better test runner for Minitest.

  You can run specific files by using `file:number`.

  $ mt test/models/user_test.rb:42

  You can also run files by the test name (caveat: you need to underscore the name):

  $ mt test/models/user_test.rb --name /validations/

  You can also run specific directories:

  $ mt test/models

  To exclude tests by name, use --exclude:

  $ mt test/models --exclude /validations/
TEXT

Class Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(args) ⇒ CLI

Returns a new instance of CLI.



19
20
21
# File 'lib/minitest/utils/cli.rb', line 19

def initialize(args)
  @args = args
end

Class Attribute Details

.loaded_via_bundle_execObject

Returns the value of attribute loaded_via_bundle_exec.



16
17
18
# File 'lib/minitest/utils/cli.rb', line 16

def loaded_via_bundle_exec
  @loaded_via_bundle_exec
end

Instance Method Details

#expand_entry(entry) ⇒ Object



253
254
255
256
257
258
259
260
261
262
263
264
# File 'lib/minitest/utils/cli.rb', line 253

def expand_entry(entry)
  entry = extract_entry(entry)

  if File.directory?(entry)
    Dir[
      File.join(entry, "**", "*_test.rb"),
      File.join(entry, "**", "*_spec.rb")
    ]
  else
    Dir[entry]
  end
end

#extract_entry(entry) ⇒ Object



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
# File 'lib/minitest/utils/cli.rb', line 266

def extract_entry(entry)
  entry = File.expand_path(entry)
  return entry unless entry.match?(/:\d+$/)

  entry, line = entry.split(":")
  line = line.to_i
  return entry unless File.file?(entry)

  content = File.read(entry)
  text = content.lines[line - 1].chomp.strip

  method_name = if text =~ /^\s*test\s+(['"])(.*?)\1\s+do\s*$/
                  Test.test_method_name(::Regexp.last_match(2))
                elsif text =~ /^def\s+(test_.+)$/
                  ::Regexp.last_match(1)
                end

  if method_name
    class_names =
      content.scan(/^\s*class\s+([^<]+)/).flatten.map(&:strip)

    class_name = class_names.find do |name|
      name.end_with?("Test")
    end

    only << "#{class_name}##{method_name}" if class_name
  end

  entry
end

#filesObject



224
225
226
227
228
229
230
231
232
# File 'lib/minitest/utils/cli.rb', line 224

def files
  @files ||= begin
    files = @args
    files += %w[test spec] if files.empty?
    files
      .flat_map { expand_entry(_1) }
      .reject { ignored_file?(_1) }
  end
end

#ignored_file?(file) ⇒ Boolean

Returns:

  • (Boolean)


245
246
247
# File 'lib/minitest/utils/cli.rb', line 245

def ignored_file?(file)
  ignored_files.any? { file.include?(_1) }
end

#ignored_filesObject



234
235
236
237
238
239
240
241
242
243
# File 'lib/minitest/utils/cli.rb', line 234

def ignored_files
  @ignored_files ||= if File.file?(".minitestignore")
                       File.read(".minitestignore")
                           .lines
                           .map(&:strip)
                           .reject { _1.start_with?("#") }
                     else
                       []
                     end
end

#indent(text) ⇒ Object



23
24
25
# File 'lib/minitest/utils/cli.rb', line 23

def indent(text)
  text.gsub(/^/, "  ")
end

#lib_dirObject



89
90
91
# File 'lib/minitest/utils/cli.rb', line 89

def lib_dir
  File.join(Dir.pwd, "lib")
end

#lib_dir?Boolean

Returns:

  • (Boolean)


101
102
103
# File 'lib/minitest/utils/cli.rb', line 101

def lib_dir?
  File.directory?(lib_dir)
end

#minitest_argsObject



179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
# File 'lib/minitest/utils/cli.rb', line 179

def minitest_args
  args = []
  args += ["--seed", options[:seed]]
  args += ["--exclude", options[:exclude]] if options[:exclude]
  args += ["--slow", options[:slow]] if options[:slow]
  args += ["--name", "/#{only.join('|')}/"] unless only.empty?
  args += ["--hide-slow"] if options[:hide_slow]
  args += ["--no-color"] if options[:no_color]

  if options[:slow_threshold]
    threshold = options[:slow_threshold].to_s
    threshold = threshold.gsub(/\.0+$/, "").delete_suffix(".")
    args += ["--slow-threshold", threshold]
  end

  args.map(&:to_s)
end

#minitest_optionsObject



206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
# File 'lib/minitest/utils/cli.rb', line 206

def minitest_options
  args = {}
  args[:seed] = options[:seed]
  args[:exclude] = options[:exclude] if options[:exclude]
  args[:slow] = options[:slow] if options[:slow]
  args[:name] = "/#{only.join('|')}/" unless only.empty?
  args[:hide_slow] = options[:hide_slow] if options[:hide_slow]
  args[:no_color] = options[:no_color] if options[:no_color]

  if options[:slow_threshold]
    threshold = options[:slow_threshold].to_s
    threshold = threshold.gsub(/\.0+$/, "").delete_suffix(".")
    args[:slow_threshold] = threshold
  end

  args
end

#new_seedObject



301
302
303
# File 'lib/minitest/utils/cli.rb', line 301

def new_seed
  (ENV["SEED"] || srand).to_i % 0xFFFF
end

#onlyObject



249
250
251
# File 'lib/minitest/utils/cli.rb', line 249

def only
  @only ||= []
end

#optionsObject



297
298
299
# File 'lib/minitest/utils/cli.rb', line 297

def options
  @options ||= {seed: new_seed}
end


335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
# File 'lib/minitest/utils/cli.rb', line 335

def print_help(matches)
  io = StringIO.new
  matches.sort_by! { _1["long"] }
  short_size = matches.map { _1[:short].to_s.size }.max
  long_size = matches.map { _1[:long].to_s.size }.max

  io << indent(color("Usage:", :green))
  io << indent(color("mt [OPTIONS] [FILES|DIR]...", :blue))
  io << "\n\n"
  io << indent("A better test runner for Minitest.")
  io << "\n\n"
  file_line = color("file:number", :yellow)
  io << indent("You can run specific files by using #{file_line}.")
  io << "\n\n"
  io << indent(color("$ mt test/models/user_test.rb:42", :yellow))
  io << "\n\n"
  io << indent("You can run files by the test name.")
  io << "\n"
  io << indent("Caveat: you need to underscore the name.")
  io << "\n\n"
  io << indent(
    color("$ mt test/models/user_test.rb --name /validations/", :yellow)
  )
  io << "\n\n"
  io << indent("You can also run specific directories:")
  io << "\n\n"
  io << indent(color("To exclude tests by name, use --exclude:", :yellow))
  io << "\n\n"
  io << indent("To ignore files, you can use a `.minitestignore`.")
  io << "\n"
  io << indent("Each line can be a partial file/dir name.")
  io << "\n"
  io << indent("Lines startin with # are ignored.")
  io << "\n\n"
  io << indent(color("# This is a comment", :yellow))
  io << "\n"
  io << indent(color("test/fixtures", :yellow))
  io << "\n\n"
  io << indent(color("Options:", :green))
  io << "\n"

  matches.each do |match|
    match => { short:, long:, description: }

    io << "  "
    io << (" " * (short_size - short.to_s.size))
    io << color(short, :blue) if short
    io << "  " unless short
    io << ", " if short
    io << color(long.to_s, :blue)
    io << (" " * (long_size - long.to_s.size + 4))
    io << description
    io << "\n"
  end

  puts io.tap(&:rewind).read
end

#runObject



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
# File 'lib/minitest/utils/cli.rb', line 105

def run
  $LOAD_PATH << lib_dir if lib_dir?
  $LOAD_PATH << test_dir if test_dir?
  $LOAD_PATH << spec_dir if spec_dir?

  puts "\nNo tests found." if files.empty?

  files.each {|file| require(file) }

  bundler = "bundle exec " if self.class.loaded_via_bundle_exec

  ENV["MT_TEST_COMMAND"] =
    "#{bundler}mt %{location}:%{line} #{color('# %{description}', :blue)}"

  ARGV.clear
  ARGV.push(*to_shell(minitest_options))

  if options[:watch]
    gem "listen"
    require "listen"
    pid = nil

    listen =
      Listen.to(Dir.pwd, only: /(\.rb|Gemfile\.lock)$/) do |*changed, _|
        next if pid

        $stdout.clear_screen

        # Make a list of test files that have been changed.
        changed = changed.flatten.filter_map do |file|
          if file.end_with?("_test.rb")
            Pathname(file).relative_path_from(Dir.pwd).to_s
          end
        end

        options = minitest_options
                  .slice(:slow, :hide_slow, :no_color, :slow_threshold)

        # Load the list of failures from the last run.
        failures = JSON.load_file(".minitestfailures") rescue [] # rubocop:disable Style/RescueModifier
        options[:name] = "/^#{failures.join('|')}$/" if failures.any?

        # If there are no failures, run the changed files.
        changed = [] if failures.any?

        pid = Process.spawn(
          $PROGRAM_NAME,
          *to_shell(options),
          *changed,
          chdir: Dir.pwd
        )
        Process.wait(pid)
        pid = nil
      end
  end

  if options[:watch]
    pid = Process.spawn(
      $PROGRAM_NAME,
      *to_shell(minitest_options),
      chdir: Dir.pwd
    )
    Process.wait(pid)
    pid = nil
    listen.start
    sleep
  else
    Minitest.autorun
  end
rescue Interrupt
  Process.kill("INT", pid) if pid
  puts "Exiting..."
end

#spec_dirObject



85
86
87
# File 'lib/minitest/utils/cli.rb', line 85

def spec_dir
  File.join(Dir.pwd, "spec")
end

#spec_dir?Boolean

Returns:

  • (Boolean)


97
98
99
# File 'lib/minitest/utils/cli.rb', line 97

def spec_dir?
  File.directory?(spec_dir)
end

#startObject



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
# File 'lib/minitest/utils/cli.rb', line 27

def start
  OptionParser.new do |parser|
    parser.banner = ""

    parser.on("-n", "--name=NAME",
              "Run tests that match this name") do |v|
      options[:name] = v
    end

    parser.on("-s", "--seed=SEED", "Sets fixed seed.") do |v|
      options[:seed] = v
    end

    parser.on("--slow", "Run slow tests.") do |v|
      options[:slow] = v
    end

    parser.on("--hide-slow", "Hide list of slow tests.") do |v|
      options[:hide_slow] = v
    end

    parser.on("--slow-threshold=THRESHOLD",
              "Set the slow threshold (in seconds)") do |v|
      options[:slow_threshold] = v.to_f
    end

    parser.on("--no-color", "Disable colored output.") do
      options[:no_color] = true
    end

    parser.on("--watch", "Watch for changes, and re-run tests.") do
      options[:watch] = true
    end

    parser.on(
      "-e",
      "--exclude=PATTERN",
      "Exclude /regexp/ or string from run."
    ) do |v|
      options[:exclude] = v
    end

    parser.on_tail("-h", "--help", "Show this message") do
      matches = parser.to_a.map do |line|
        line.match(MATCHER).named_captures.transform_keys(&:to_sym)
      end
      print_help(matches)
      exit
    end
  end.parse!(@args)

  run
end

#test_dirObject



81
82
83
# File 'lib/minitest/utils/cli.rb', line 81

def test_dir
  File.join(Dir.pwd, "test")
end

#test_dir?Boolean

Returns:

  • (Boolean)


93
94
95
# File 'lib/minitest/utils/cli.rb', line 93

def test_dir?
  File.directory?(test_dir)
end

#to_shell(args) ⇒ Object



197
198
199
200
201
202
203
204
# File 'lib/minitest/utils/cli.rb', line 197

def to_shell(args)
  args
    .transform_keys {|key| "--#{key.to_s.tr('_', '-')}" }
    .to_a
    .flatten
    .reject { _1&.is_a?(TrueClass) }
    .map(&:to_s)
end