minitest 5.27.0 → 6.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/History.rdoc +108 -0
  4. data/Manifest.txt +13 -4
  5. data/README.rdoc +8 -90
  6. data/Rakefile +7 -15
  7. data/bin/minitest +5 -0
  8. data/lib/minitest/assertions.rb +26 -55
  9. data/lib/minitest/autorun.rb +0 -1
  10. data/lib/minitest/benchmark.rb +1 -1
  11. data/lib/minitest/bisect.rb +304 -0
  12. data/lib/minitest/complete.rb +56 -0
  13. data/lib/minitest/find_minimal_combination.rb +127 -0
  14. data/lib/minitest/manual_plugins.rb +4 -16
  15. data/lib/minitest/parallel.rb +3 -3
  16. data/lib/minitest/path_expander.rb +432 -0
  17. data/lib/minitest/pride.rb +1 -1
  18. data/lib/minitest/server.rb +49 -0
  19. data/lib/minitest/server_plugin.rb +88 -0
  20. data/lib/minitest/spec.rb +2 -31
  21. data/lib/minitest/sprint.rb +105 -0
  22. data/lib/minitest/sprint_plugin.rb +39 -0
  23. data/lib/minitest/test.rb +5 -11
  24. data/lib/minitest/test_task.rb +16 -9
  25. data/lib/minitest.rb +69 -87
  26. data/test/minitest/metametameta.rb +1 -1
  27. data/test/minitest/test_bisect.rb +249 -0
  28. data/test/minitest/test_find_minimal_combination.rb +138 -0
  29. data/test/minitest/test_minitest_assertions.rb +36 -44
  30. data/test/minitest/test_minitest_benchmark.rb +14 -0
  31. data/test/minitest/test_minitest_spec.rb +38 -102
  32. data/test/minitest/test_minitest_test.rb +20 -99
  33. data/test/minitest/test_path_expander.rb +229 -0
  34. data/test/minitest/test_server.rb +146 -0
  35. data.tar.gz.sig +0 -0
  36. metadata +87 -41
  37. metadata.gz.sig +2 -3
  38. data/.autotest +0 -34
  39. data/lib/minitest/mock.rb +0 -327
  40. data/lib/minitest/unit.rb +0 -42
  41. data/test/minitest/test_minitest_mock.rb +0 -1213
@@ -0,0 +1,432 @@
1
+ require "prism"
2
+ require "pathname" # for ruby 3
3
+
4
+ module Minitest; end # :nodoc:
5
+
6
+ ##
7
+ # PathExpander helps pre-process command-line arguments expanding
8
+ # directories into their constituent files. It further helps by
9
+ # providing additional mechanisms to make specifying subsets easier
10
+ # with path subtraction and allowing for command-line arguments to be
11
+ # saved in a file.
12
+ #
13
+ # NOTE: this is NOT an options processor. It is a path processor
14
+ # (basically everything else besides options). It does provide a
15
+ # mechanism for pre-filtering cmdline options, but not with the intent
16
+ # of actually processing them in PathExpander. Use OptionParser to
17
+ # deal with options either before or after passing ARGV through
18
+ # PathExpander.
19
+
20
+ class Minitest::VendoredPathExpander
21
+ # extracted version = "2.0.0"
22
+
23
+ ##
24
+ # The args array to process.
25
+
26
+ attr_accessor :args
27
+
28
+ ##
29
+ # The glob used to expand dirs to files.
30
+
31
+ attr_accessor :glob
32
+
33
+ ##
34
+ # The path to scan if no paths are found in the initial scan.
35
+
36
+ attr_accessor :path
37
+
38
+ ##
39
+ # Create a new path expander that operates on args and expands via
40
+ # glob as necessary. Takes an optional +path+ arg to fall back on if
41
+ # no paths are found on the initial scan (see #process_args).
42
+
43
+ def initialize args, glob, path = "."
44
+ self.args = args
45
+ self.glob = glob
46
+ self.path = path
47
+ end
48
+
49
+ ##
50
+ # Takes an array of paths and returns an array of paths where all
51
+ # directories are expanded to all files found via the glob provided
52
+ # to PathExpander.
53
+ #
54
+ # Paths are normalized to not have a leading "./".
55
+
56
+ def expand_dirs_to_files *dirs
57
+ dirs.flatten.map { |p|
58
+ if File.directory? p then
59
+ Dir[File.join(p, glob)].find_all { |f| File.file? f }
60
+ else
61
+ p
62
+ end
63
+ }.flatten.sort.map { |s| _normalize s }
64
+ end
65
+
66
+ def _normalize(f) = Pathname.new(f).cleanpath.to_s # :nodoc:
67
+
68
+ ##
69
+ # Process a file into more arguments. Override this to add
70
+ # additional capabilities.
71
+
72
+ def process_file path
73
+ File.readlines(path).map(&:chomp)
74
+ end
75
+
76
+ ##
77
+ # Enumerate over args passed to PathExpander and return a list of
78
+ # files and flags to process. Arguments are processed as:
79
+ #
80
+ # @file_of_args :: Read the file and append to args.
81
+ # -file_path :: Subtract path from file to be processed.
82
+ # -dir_path :: Expand and subtract paths from files to be processed.
83
+ # -not_a_path :: Add to flags to be processed.
84
+ # dir_path :: Expand and add to files to be processed.
85
+ # file_path :: Add to files to be processed.
86
+ # - :: Add "-" (stdin) to files to be processed.
87
+ #
88
+ # See expand_dirs_to_files for details on how expansion occurs.
89
+ #
90
+ # Subtraction happens last, regardless of argument ordering.
91
+ #
92
+ # If no files are found (which is not the same as having an empty
93
+ # file list after subtraction), then fall back to expanding on the
94
+ # default #path given to initialize.
95
+
96
+ def process_args
97
+ pos_files = []
98
+ neg_files = []
99
+ flags = []
100
+ clean = true
101
+
102
+ root_dir = File.expand_path "/" # needed for windows paths
103
+
104
+ args.each do |arg|
105
+ case arg
106
+ when /^@(.*)/ then # push back on, so they can have dirs/-/@ as well
107
+ clean = false
108
+ args.concat process_file $1
109
+ when "-" then
110
+ pos_files << arg
111
+ when /^-(.*)/ then
112
+ if File.exist? $1 then
113
+ clean = false
114
+ neg_files += expand_dirs_to_files($1)
115
+ else
116
+ flags << arg
117
+ end
118
+ else
119
+ root_path = File.expand_path(arg) == root_dir # eg: -n /./
120
+ if File.exist? arg and not root_path then
121
+ clean = false
122
+ pos_files += expand_dirs_to_files(arg)
123
+ else
124
+ flags << arg
125
+ end
126
+ end
127
+ end
128
+
129
+ files = pos_files - neg_files
130
+ files += expand_dirs_to_files(self.path) if files.empty? && clean
131
+
132
+ [files, flags]
133
+ end
134
+
135
+ ##
136
+ # Process over flags and treat any special ones here. Returns an
137
+ # array of the flags you haven't processed.
138
+ #
139
+ # This version does nothing. Subclass and override for
140
+ # customization.
141
+
142
+ def process_flags flags
143
+ flags
144
+ end
145
+
146
+ ##
147
+ # Top-level method processes args. If no block is given, immediately
148
+ # returns with an Enumerator for further chaining.
149
+ #
150
+ # Otherwise, it calls +pre_process+, +process_args+ and
151
+ # +process_flags+, enumerates over the files, and then calls
152
+ # +post_process+, returning self for any further chaining.
153
+ #
154
+ # Most of the time, you're going to provide a block to process files
155
+ # and do nothing more with the result. Eg:
156
+ #
157
+ # PathExpander.new(ARGV).process do |f|
158
+ # puts "./#{f}"
159
+ # end
160
+ #
161
+ # or:
162
+ #
163
+ # PathExpander.new(ARGV).process # => Enumerator
164
+
165
+ def process(&b)
166
+ return enum_for(:process) unless block_given?
167
+
168
+ pre_process
169
+
170
+ files, flags = process_args
171
+
172
+ args.replace process_flags flags
173
+
174
+ files.uniq.each(&b)
175
+
176
+ post_process
177
+
178
+ self
179
+ end
180
+
181
+ ##
182
+ # Hook to run before process
183
+
184
+ def pre_process = nil
185
+
186
+ ##
187
+ # Hook to run after process
188
+
189
+ def post_process = nil
190
+
191
+ ##
192
+ # A file filter mechanism similar to, but not as extensive as,
193
+ # .gitignore files:
194
+ #
195
+ # + If a pattern does not contain a slash, it is treated as a shell glob.
196
+ # + If a pattern ends in a slash, it matches on directories (and contents).
197
+ # + Otherwise, it matches on relative paths.
198
+ #
199
+ # File.fnmatch is used throughout, so glob patterns work for all 3 types.
200
+ #
201
+ # Takes a list of +files+ and either an io or path of +ignore+ data
202
+ # and returns a list of files left after filtering.
203
+
204
+ def filter_files files, ignore
205
+ ignore_paths = if ignore.respond_to? :read then
206
+ ignore.read
207
+ elsif File.exist? ignore then
208
+ File.read ignore
209
+ end
210
+
211
+ if ignore_paths then
212
+ nonglobs, globs = ignore_paths.split("\n").partition { |p| p.include? "/" }
213
+ dirs, ifiles = nonglobs.partition { |p| p.end_with? "/" }
214
+ dirs = dirs.map { |s| s.chomp "/" }
215
+
216
+ dirs.map! { |i| File.expand_path i }
217
+ globs.map! { |i| File.expand_path i }
218
+ ifiles.map! { |i| File.expand_path i }
219
+
220
+ only_paths = File::FNM_PATHNAME
221
+ files = files.reject { |f|
222
+ f = File.expand_path(f)
223
+ dirs.any? { |i| File.fnmatch?(i, File.dirname(f), only_paths) } ||
224
+ globs.any? { |i| File.fnmatch?(i, f) } ||
225
+ ifiles.any? { |i| File.fnmatch?(i, f, only_paths) }
226
+ }
227
+ end
228
+
229
+ files
230
+ end
231
+ end # VendoredPathExpander
232
+
233
+ ##
234
+ # Minitest's PathExpander to find and filter tests.
235
+
236
+ class Minitest::PathExpander < Minitest::VendoredPathExpander
237
+ attr_accessor :by_line # :nodoc:
238
+
239
+ TEST_GLOB = "**/{test_*,*_test,spec_*,*_spec}.rb" # :nodoc:
240
+
241
+ def initialize args = ARGV # :nodoc:
242
+ super args, TEST_GLOB, "test"
243
+ self.by_line = {}
244
+ end
245
+
246
+ def process_args # :nodoc:
247
+ args.reject! { |arg| # this is a good use of overriding
248
+ case arg
249
+ when /^(.*):([\d,-]+)$/ then
250
+ f, ls = $1, $2
251
+ ls = ls
252
+ .split(/,/)
253
+ .map { |l|
254
+ case l
255
+ when /^\d+$/ then
256
+ l.to_i
257
+ when /^(\d+)-(\d+)$/ then
258
+ $1.to_i..$2.to_i
259
+ else
260
+ raise "unhandled argument format: %p" % [l]
261
+ end
262
+ }
263
+ next unless File.exist? f
264
+ f = _normalize f
265
+ args << f # push path on lest it run whole dir
266
+ by_line[f] = ls # implies rejection
267
+ end
268
+ }
269
+
270
+ super
271
+ end
272
+
273
+ ##
274
+ # Overrides PathExpander#process_flags to filter out ruby flags
275
+ # from minitest flags. Only supports -I<paths>, -d, and -w for
276
+ # ruby.
277
+
278
+ def process_flags flags
279
+ flags.reject { |flag| # all hits are truthy, so this works out well
280
+ case flag
281
+ when /^-I(.*)/ then
282
+ $LOAD_PATH.prepend(*$1.split(/:/))
283
+ when /^-d/ then
284
+ $DEBUG = true
285
+ when /^-w/ then
286
+ $VERBOSE = true
287
+ else
288
+ false
289
+ end
290
+ }
291
+ end
292
+
293
+ ##
294
+ # Add additional arguments to args to handle path:line argument filtering
295
+
296
+ def post_process
297
+ return if by_line.empty?
298
+
299
+ tests = tests_by_class
300
+
301
+ exit! 1 if handle_missing_tests? tests
302
+
303
+ test_res = tests_to_regexp tests
304
+ self.args << "-n" << "/#{test_res.join "|"}/"
305
+ end
306
+
307
+ ##
308
+ # Find and return all known tests as a hash of klass => [TM...]
309
+ # pairs.
310
+
311
+ def all_tests
312
+ Minitest.seed = 42 # minor hack to deal with runnable_methods shuffling
313
+ Minitest::Runnable.runnables
314
+ .to_h { |k|
315
+ ms = k.runnable_methods
316
+ .sort
317
+ .map { |m| TM.new k, m.to_sym }
318
+ .sort_by { |t| [t.path, t.line_s] }
319
+ [k, ms]
320
+ }
321
+ .reject { |k, v| v.empty? }
322
+ end
323
+
324
+ ##
325
+ # Returns a hash mapping Minitest runnable classes to TMs
326
+
327
+ def tests_by_class
328
+ all_tests
329
+ .transform_values { |ms|
330
+ ms.select { |m|
331
+ bl = by_line[m.path]
332
+ not bl or bl.any? { |l| m.include? l }
333
+ }
334
+ }
335
+ .reject { |k, v| v.empty? }
336
+ end
337
+
338
+ ##
339
+ # Converts +tests+ to an array of "klass#(methods+)" regexps to be
340
+ # used for test selection.
341
+
342
+ def tests_to_regexp tests
343
+ tests # { k1 => [Test(a), ...}
344
+ .transform_values { |tms| tms.map(&:name) } # { k1 => %w[a, b], ...}
345
+ .map { |k, ns| # [ "k1#(?:a|b)", "k2#c", ...]
346
+ if ns.size > 1 then
347
+ ns.map! { |n| Regexp.escape n }
348
+ "%s#\(?:%s\)" % [Regexp.escape(k.name), ns.join("|")]
349
+ else
350
+ "%s#%s" % [Regexp.escape(k.name), ns.first]
351
+ end
352
+ }
353
+ end
354
+
355
+ ##
356
+ # Handle the case where a line number doesn't match any known tests.
357
+ # Returns true to signal that running should stop.
358
+
359
+ def handle_missing_tests? tests
360
+ _tests = tests.values.flatten
361
+ not_found = by_line
362
+ .flat_map { |f, ls| ls.map { |l| [f, l] } }
363
+ .reject { |f, l|
364
+ _tests.any? { |t| t.path == f and t.include? l }
365
+ }
366
+
367
+ unless not_found.empty? then
368
+ by_path = all_tests.values.flatten.group_by(&:path)
369
+
370
+ puts
371
+ puts "ERROR: test(s) not found at:"
372
+ not_found.each do |f, l|
373
+ puts " %s:%s" % [f, l]
374
+ puts
375
+ puts "Did you mean?"
376
+ puts
377
+ l = l.begin if l.is_a? Range
378
+ by_path[f] and
379
+ by_path[f]
380
+ .sort_by { |m| (m.line_s - l).abs }
381
+ .first(2)
382
+ .each do |m|
383
+ puts " %-30s (dist=%+d) (%s)" % [m, m.line_s - l, m.name]
384
+ end
385
+ puts
386
+ end
387
+ $stdout.flush
388
+ $stderr.flush
389
+ true
390
+ end
391
+ end
392
+
393
+ ##
394
+ # Simple TestMethod (abbr TM) Data object.
395
+
396
+ TM = Data.define :klass, :name, :path, :lines do
397
+ def initialize klass:, name:
398
+ method = klass.instance_method name
399
+ path, line_s = method.source_location
400
+
401
+ path = path.delete_prefix "#{Dir.pwd}/"
402
+
403
+ line_e = line_s + TM.source_for(method).lines.size - 1
404
+
405
+ lines = line_s..line_e
406
+
407
+ super klass:, name:, path:, lines:
408
+ end
409
+
410
+ def self.source_for method
411
+ path, line = method.source_location
412
+ file = cache[path] ||= File.readlines(path)
413
+
414
+ ruby = +""
415
+
416
+ file[line-1..].each do |l|
417
+ ruby << l
418
+ return ruby if Prism.parse_success? ruby
419
+ end
420
+
421
+ nil
422
+ end
423
+
424
+ def self.cache = @cache ||= {}
425
+
426
+ def include?(o) = o.is_a?(Integer) ? lines.include?(o) : lines.overlap?(o)
427
+
428
+ def to_s = "%s:%d-%d" % [path, lines.begin, lines.end]
429
+
430
+ def line_s = lines.begin
431
+ end
432
+ end
@@ -1,4 +1,4 @@
1
1
  require_relative "../minitest"
2
2
 
3
- Minitest.load_plugins
3
+ Minitest.load :pride
4
4
  Minitest::PrideIO.pride!
@@ -0,0 +1,49 @@
1
+ require "drb"
2
+ require "tmpdir"
3
+ require_relative "../minitest"
4
+
5
+ # :stopdoc:
6
+
7
+ class Minitest::Server
8
+ # extracted version = "1.0.10"
9
+
10
+ TOPDIR = Dir.pwd + "/"
11
+
12
+ def self.path pid = $$
13
+ "drbunix:#{Dir.tmpdir}/minitest.#{pid}"
14
+ end
15
+
16
+ def self.run client
17
+ DRb.start_service path, new(client)
18
+ end
19
+
20
+ def self.stop
21
+ DRb.stop_service
22
+ end
23
+
24
+ attr_accessor :client
25
+
26
+ def initialize client
27
+ self.client = client
28
+ end
29
+
30
+ def quit
31
+ self.class.stop
32
+ end
33
+
34
+ def start
35
+ client.minitest_start
36
+ end
37
+
38
+ def result file, klass, method, fails, assertions, time
39
+ file = file.sub(/^#{TOPDIR}/, "")
40
+
41
+ client.minitest_result file, klass, method, fails, assertions, time
42
+ end
43
+
44
+ def report
45
+ # do nothing
46
+ end
47
+ end
48
+
49
+ # :startdoc:
@@ -0,0 +1,88 @@
1
+ require_relative "../minitest"
2
+
3
+ # :stopdoc:
4
+
5
+ module Minitest
6
+ @server = false
7
+
8
+ def self.plugin_server_options opts, options # :nodoc:
9
+ opts.on "--server=pid", Integer, "Connect to minitest server w/ pid." do |s|
10
+ @server = s
11
+ end
12
+ end
13
+
14
+ def self.plugin_server_init options
15
+ if @server then
16
+ require_relative "server"
17
+ self.reporter << Minitest::ServerReporter.new(@server)
18
+ end
19
+ end
20
+ end
21
+
22
+ class Minitest::ServerReporter < Minitest::AbstractReporter
23
+ def initialize pid
24
+ uri = Minitest::Server.path(pid)
25
+ @mt_server = DRbObject.new_with_uri uri
26
+ super()
27
+ end
28
+
29
+ def start
30
+ @mt_server.start
31
+ end
32
+
33
+ def record result
34
+ r = result
35
+ c = r.class
36
+
37
+ case r
38
+ when Minitest::Result then
39
+ file, = r.source_location
40
+ cn = r.klass
41
+ else
42
+ # TODO: remove? when is this used?
43
+ file, = r.method(r.name).source_location
44
+ cn = c.name
45
+ end
46
+
47
+ sanitize r.failures
48
+
49
+ @mt_server.result file, cn, r.name, r.failures, r.assertions, r.time
50
+ end
51
+
52
+ def sanitize failures
53
+ failures.map! { |e|
54
+ case e
55
+ when Minitest::UnexpectedError then
56
+ # embedded exception might not be able to be marshaled.
57
+ bt = e.error.backtrace
58
+
59
+ ex = RuntimeError.new(e.error.message)
60
+ e.error = ex
61
+ ex.set_backtrace bt
62
+
63
+ e = Minitest::UnexpectedError.new ex # ugh. some rails plugin. ugh.
64
+
65
+ if ex.instance_variables.include? :@bindings then # web-console is Evil
66
+ ex.instance_variable_set :@bindings, nil
67
+ e.instance_variable_set :@bindings, nil
68
+ end
69
+ when Minitest::Skip then
70
+ # do nothing
71
+ when Minitest::Assertion then
72
+ bt = e.backtrace
73
+ e = e.class.new(e.message)
74
+ e.set_backtrace bt
75
+ else
76
+ warn "Unhandled exception type: #{e.class}\n\n#{e.inspect}"
77
+ end
78
+
79
+ e
80
+ }
81
+ end
82
+
83
+ def report
84
+ @mt_server.report
85
+ end
86
+ end
87
+
88
+ # :startdoc:
data/lib/minitest/spec.rb CHANGED
@@ -4,24 +4,12 @@ class Module # :nodoc:
4
4
  def infect_an_assertion meth, new_name, dont_flip = false # :nodoc:
5
5
  block = dont_flip == :block
6
6
  dont_flip = false if block
7
- target_obj = block ? "_{obj.method}" : "_(obj)"
8
7
 
9
8
  # https://eregon.me/blog/2021/02/13/correct-delegation-in-ruby-2-27-3.html
10
9
  # Drop this when we can drop ruby 2.6 (aka after rails 6.1 EOL, ~2024-06)
11
10
  kw_extra = "ruby2_keywords %p" % [new_name] if respond_to? :ruby2_keywords, true
12
11
 
13
- # warn "%-22p -> %p %p" % [meth, new_name, dont_flip]
14
12
  self.class_eval <<-EOM, __FILE__, __LINE__ + 1
15
- def #{new_name} *args
16
- where = Minitest.filter_backtrace(caller).first
17
- where = where.split(/:in /, 2).first # clean up noise
18
- Kernel.warn "DEPRECATED: global use of #{new_name} from #\{where}. Use #{target_obj}.#{new_name} instead. This will fail in Minitest 6."
19
- Minitest::Expectation.new(self, Minitest::Spec.current).#{new_name}(*args)
20
- end
21
- #{kw_extra}
22
- EOM
23
-
24
- Minitest::Expectation.class_eval <<-EOM, __FILE__, __LINE__ + 1
25
13
  def #{new_name} *args
26
14
  raise "Calling ##{new_name} outside of test." unless ctx
27
15
  case
@@ -110,15 +98,6 @@ end
110
98
 
111
99
  class Minitest::Spec < Minitest::Test
112
100
 
113
- def self.current # :nodoc:
114
- Thread.current[:current_spec]
115
- end
116
-
117
- def initialize name # :nodoc:
118
- super
119
- Thread.current[:current_spec] = self
120
- end
121
-
122
101
  ##
123
102
  # Oh look! A Minitest::Spec::DSL module! Eat your heart out DHH.
124
103
 
@@ -313,9 +292,6 @@ class Minitest::Spec < Minitest::Test
313
292
  # straight-expectation methods (on Object) because it stores its
314
293
  # test context, bypassing our hacky use of thread-local variables.
315
294
  #
316
- # NOTE: At some point, the methods on Object will be deprecated
317
- # and then removed.
318
- #
319
295
  # It is also aliased to #value and #expect for your aesthetic
320
296
  # pleasure:
321
297
  #
@@ -329,11 +305,6 @@ class Minitest::Spec < Minitest::Test
329
305
 
330
306
  alias value _
331
307
  alias expect _
332
-
333
- def before_setup # :nodoc:
334
- super
335
- Thread.current[:current_spec] = self
336
- end
337
308
  end
338
309
 
339
310
  def self.extended obj # :nodoc:
@@ -348,6 +319,6 @@ end
348
319
 
349
320
  require_relative "expectations"
350
321
 
351
- class Object # :nodoc:
352
- include Minitest::Expectations unless ENV["MT_NO_EXPECTATIONS"]
322
+ class Minitest::Expectation
323
+ include Minitest::Expectations
353
324
  end