minitest 5.25.5 → 6.0.1

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 (46) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/History.rdoc +132 -0
  4. data/Manifest.txt +13 -4
  5. data/README.rdoc +18 -98
  6. data/Rakefile +7 -2
  7. data/bin/minitest +5 -0
  8. data/design_rationale.rb +21 -19
  9. data/lib/hoe/minitest.rb +2 -1
  10. data/lib/minitest/assertions.rb +37 -74
  11. data/lib/minitest/autorun.rb +3 -4
  12. data/lib/minitest/benchmark.rb +3 -3
  13. data/lib/minitest/bisect.rb +306 -0
  14. data/lib/minitest/complete.rb +56 -0
  15. data/lib/minitest/find_minimal_combination.rb +127 -0
  16. data/lib/minitest/hell.rb +1 -1
  17. data/lib/minitest/manual_plugins.rb +4 -16
  18. data/lib/minitest/parallel.rb +5 -3
  19. data/lib/minitest/path_expander.rb +425 -0
  20. data/lib/minitest/pride.rb +2 -2
  21. data/lib/minitest/pride_plugin.rb +1 -1
  22. data/lib/minitest/server.rb +45 -0
  23. data/lib/minitest/server_plugin.rb +84 -0
  24. data/lib/minitest/spec.rb +11 -37
  25. data/lib/minitest/sprint.rb +104 -0
  26. data/lib/minitest/sprint_plugin.rb +39 -0
  27. data/lib/minitest/test.rb +8 -13
  28. data/lib/minitest/test_task.rb +44 -20
  29. data/lib/minitest.rb +99 -107
  30. data/test/minitest/metametameta.rb +1 -1
  31. data/test/minitest/test_bisect.rb +235 -0
  32. data/test/minitest/test_find_minimal_combination.rb +138 -0
  33. data/test/minitest/test_minitest_assertions.rb +51 -108
  34. data/test/minitest/test_minitest_benchmark.rb +14 -0
  35. data/test/minitest/test_minitest_reporter.rb +6 -5
  36. data/test/minitest/test_minitest_spec.rb +60 -128
  37. data/test/minitest/test_minitest_test.rb +22 -101
  38. data/test/minitest/test_path_expander.rb +229 -0
  39. data/test/minitest/test_server.rb +149 -0
  40. data.tar.gz.sig +0 -0
  41. metadata +58 -25
  42. metadata.gz.sig +0 -0
  43. data/.autotest +0 -34
  44. data/lib/minitest/mock.rb +0 -347
  45. data/lib/minitest/unit.rb +0 -42
  46. data/test/minitest/test_minitest_mock.rb +0 -1218
@@ -0,0 +1,127 @@
1
+ #!/usr/bin/ruby -w
2
+
3
+ ##
4
+ # Finds the minimal combination of a collection of items that satisfy
5
+ # +test+.
6
+
7
+ class ComboFinder
8
+ ##
9
+ # Find the minimal combination of a collection of items that satisfy
10
+ # +test+.
11
+ #
12
+ # If you think of the collection as a binary tree, this algorithm
13
+ # does a breadth first search of the combinations that satisfy
14
+ # +test+.
15
+ #--
16
+ # level collection
17
+ #
18
+ # 0 A
19
+ # 1 B C
20
+ # 2 D E F G
21
+ # 3 1 2 3 4 5 6 7 8
22
+ #
23
+ # This assumes that A has already been tested and you're now trying
24
+ # to reduce the match. Starting at level 1, test B & C separately.
25
+ # If either test positive, reduce the search space accordingly. If
26
+ # not, step down to level 2 and search w/ finer granularity (ie, DF,
27
+ # DG, EF--DE and FG were already tested as B & C). Repeat until a
28
+ # minimal combination is found.
29
+
30
+ def find_minimal_combination ary
31
+ level, n_combos = 1, 1
32
+ seen = {}
33
+
34
+ d "Total number of culprits: #{ary.size}"
35
+
36
+ loop do
37
+ size = 2 ** (Math.log(ary.size) / Math.log(2)).round
38
+ divs = 2 ** level
39
+ done = divs >= size
40
+ divs = size if done
41
+
42
+ subsections = ary.each_slice(size/divs).to_a.combination(n_combos)
43
+
44
+ d
45
+ d "# new round!"
46
+ d "# of subsections in this round: #{subsections.to_a.size}"
47
+ d
48
+
49
+ found = subsections.find { |a|
50
+ b = a.flatten
51
+
52
+ next if seen[b]
53
+
54
+ d "# trying #{b.size} at level #{level} / combo #{n_combos}"
55
+ cache_result yield(b), b, seen
56
+ }
57
+
58
+ if found then
59
+ ary = found.flatten
60
+ break if done
61
+
62
+ seen.delete ary
63
+
64
+ d "# FOUND!"
65
+ d "# search space size = #{ary.size}"
66
+ d "# resetting level and n_combos to 1"
67
+
68
+ level = n_combos = 1
69
+ else
70
+ if done then
71
+ n_combos += 1
72
+ d "# increasing n_combos to #{n_combos}"
73
+ break if n_combos > size
74
+ else
75
+ level += 1
76
+ n_combos = level
77
+ d "# setting level to #{level} and n_combos to #{n_combos}"
78
+ end
79
+ end
80
+ end
81
+
82
+ ary
83
+ end
84
+
85
+ def d s = "" # :nodoc:
86
+ warn s if ENV["MTB_DEBUG"]
87
+ end
88
+
89
+ def cache_result result, data, cache # :nodoc:
90
+ cache[data] = true
91
+
92
+ return result if result
93
+
94
+ unless result or data.size > 128 then
95
+ max = data.size
96
+ subdiv = 2
97
+ until subdiv >= max do
98
+ data.each_slice(max / subdiv) do |sub_data|
99
+ cache[sub_data] = true
100
+ end
101
+ subdiv *= 2
102
+ end
103
+ end
104
+
105
+ result
106
+ end
107
+ end
108
+
109
+ class Array # :nodoc:
110
+ ##
111
+ # Find the minimal combination of a collection of items that satisfy +test+.
112
+
113
+ def find_minimal_combination &test
114
+ ComboFinder.new.find_minimal_combination(self, &test)
115
+ end
116
+
117
+ def find_minimal_combination_and_count
118
+ count = 0
119
+
120
+ found = self.find_minimal_combination do |ary|
121
+ count += 1
122
+ yield ary
123
+ end
124
+
125
+ return found, count
126
+ end
127
+ end
data/lib/minitest/hell.rb CHANGED
@@ -1,4 +1,4 @@
1
- require "minitest/parallel"
1
+ require_relative "parallel"
2
2
 
3
3
  class Minitest::Test
4
4
  parallelize_me!
@@ -1,16 +1,4 @@
1
- require "minitest"
2
-
3
- ARGV << "--no-plugins"
4
-
5
- module Minitest
6
- ##
7
- # Manually load plugins by name.
8
-
9
- def self.load *names
10
- names.each do |name|
11
- require "minitest/#{name}_plugin"
12
-
13
- self.extensions << name.to_s
14
- end
15
- end
16
- end
1
+ #
2
+ # See the functionality in Minitest#load
3
+ #
4
+ warn "This file is no longer necessary. Called from #{caller.first}"
@@ -1,3 +1,5 @@
1
+ require "thread"
2
+
1
3
  module Minitest
2
4
  module Parallel # :nodoc:
3
5
 
@@ -30,7 +32,7 @@ module Minitest
30
32
  while job = queue.pop do
31
33
  klass, method, reporter = job
32
34
  reporter.synchronize { reporter.prerecord klass, method }
33
- result = Minitest.run_one_method klass, method
35
+ result = klass.new(method).run
34
36
  reporter.synchronize { reporter.record result }
35
37
  end
36
38
  end
@@ -57,11 +59,11 @@ module Minitest
57
59
  def _synchronize; Minitest::Test.io_lock.synchronize { yield }; end # :nodoc:
58
60
 
59
61
  module ClassMethods # :nodoc:
60
- def run_one_method klass, method_name, reporter
62
+ def run klass, method_name, reporter
61
63
  Minitest.parallel_executor << [klass, method_name, reporter]
62
64
  end
63
65
 
64
- def test_order
66
+ def run_order
65
67
  :parallel
66
68
  end
67
69
  end
@@ -0,0 +1,425 @@
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
+ def pre_process = nil
182
+ def post_process = nil
183
+
184
+ ##
185
+ # A file filter mechanism similar to, but not as extensive as,
186
+ # .gitignore files:
187
+ #
188
+ # + If a pattern does not contain a slash, it is treated as a shell glob.
189
+ # + If a pattern ends in a slash, it matches on directories (and contents).
190
+ # + Otherwise, it matches on relative paths.
191
+ #
192
+ # File.fnmatch is used throughout, so glob patterns work for all 3 types.
193
+ #
194
+ # Takes a list of +files+ and either an io or path of +ignore+ data
195
+ # and returns a list of files left after filtering.
196
+
197
+ def filter_files files, ignore
198
+ ignore_paths = if ignore.respond_to? :read then
199
+ ignore.read
200
+ elsif File.exist? ignore then
201
+ File.read ignore
202
+ end
203
+
204
+ if ignore_paths then
205
+ nonglobs, globs = ignore_paths.split("\n").partition { |p| p.include? "/" }
206
+ dirs, ifiles = nonglobs.partition { |p| p.end_with? "/" }
207
+ dirs = dirs.map { |s| s.chomp "/" }
208
+
209
+ dirs.map! { |i| File.expand_path i }
210
+ globs.map! { |i| File.expand_path i }
211
+ ifiles.map! { |i| File.expand_path i }
212
+
213
+ only_paths = File::FNM_PATHNAME
214
+ files = files.reject { |f|
215
+ f = File.expand_path(f)
216
+ dirs.any? { |i| File.fnmatch?(i, File.dirname(f), only_paths) } ||
217
+ globs.any? { |i| File.fnmatch?(i, f) } ||
218
+ ifiles.any? { |i| File.fnmatch?(i, f, only_paths) }
219
+ }
220
+ end
221
+
222
+ files
223
+ end
224
+ end # VendoredPathExpander
225
+
226
+ ##
227
+ # Minitest's PathExpander to find and filter tests.
228
+
229
+ class Minitest::PathExpander < Minitest::VendoredPathExpander
230
+ attr_accessor :by_line # :nodoc:
231
+
232
+ TEST_GLOB = "**/{test_*,*_test,spec_*,*_spec}.rb" # :nodoc:
233
+
234
+ def initialize args = ARGV # :nodoc:
235
+ super args, TEST_GLOB, "test"
236
+ self.by_line = {}
237
+ end
238
+
239
+ def process_args # :nodoc:
240
+ args.reject! { |arg| # this is a good use of overriding
241
+ case arg
242
+ when /^(.*):([\d,-]+)$/ then
243
+ f, ls = $1, $2
244
+ ls = ls
245
+ .split(/,/)
246
+ .map { |l|
247
+ case l
248
+ when /^\d+$/ then
249
+ l.to_i
250
+ when /^(\d+)-(\d+)$/ then
251
+ $1.to_i..$2.to_i
252
+ else
253
+ raise "unhandled argument format: %p" % [l]
254
+ end
255
+ }
256
+ next unless File.exist? f
257
+ f = _normalize f
258
+ args << f # push path on lest it run whole dir
259
+ by_line[f] = ls # implies rejection
260
+ end
261
+ }
262
+
263
+ super
264
+ end
265
+
266
+ ##
267
+ # Overrides PathExpander#process_flags to filter out ruby flags
268
+ # from minitest flags. Only supports -I<paths>, -d, and -w for
269
+ # ruby.
270
+
271
+ def process_flags flags
272
+ flags.reject { |flag| # all hits are truthy, so this works out well
273
+ case flag
274
+ when /^-I(.*)/ then
275
+ $LOAD_PATH.prepend(*$1.split(/:/))
276
+ when /^-d/ then
277
+ $DEBUG = true
278
+ when /^-w/ then
279
+ $VERBOSE = true
280
+ else
281
+ false
282
+ end
283
+ }
284
+ end
285
+
286
+ ##
287
+ # Add additional arguments to args to handle path:line argument filtering
288
+
289
+ def post_process
290
+ return if by_line.empty?
291
+
292
+ tests = tests_by_class
293
+
294
+ exit! 1 if handle_missing_tests? tests
295
+
296
+ test_res = tests_to_regexp tests
297
+ self.args << "-n" << "/#{test_res.join "|"}/"
298
+ end
299
+
300
+ ##
301
+ # Find and return all known tests as a hash of klass => [TM...]
302
+ # pairs.
303
+
304
+ def all_tests
305
+ Minitest.seed = 42 # minor hack to deal with runnable_methods shuffling
306
+ Minitest::Runnable.runnables
307
+ .to_h { |k|
308
+ ms = k.runnable_methods
309
+ .sort
310
+ .map { |m| TM.new k, m.to_sym }
311
+ .sort_by { |t| [t.path, t.line_s] }
312
+ [k, ms]
313
+ }
314
+ .reject { |k, v| v.empty? }
315
+ end
316
+
317
+ ##
318
+ # Returns a hash mapping Minitest runnable classes to TMs
319
+
320
+ def tests_by_class
321
+ all_tests
322
+ .transform_values { |ms|
323
+ ms.select { |m|
324
+ bl = by_line[m.path]
325
+ not bl or bl.any? { |l| m.include? l }
326
+ }
327
+ }
328
+ .reject { |k, v| v.empty? }
329
+ end
330
+
331
+ ##
332
+ # Converts +tests+ to an array of "klass#(methods+)" regexps to be
333
+ # used for test selection.
334
+
335
+ def tests_to_regexp tests
336
+ tests # { k1 => [Test(a), ...}
337
+ .transform_values { |tms| tms.map(&:name) } # { k1 => %w[a, b], ...}
338
+ .map { |k, ns| # [ "k1#(?:a|b)", "k2#c", ...]
339
+ if ns.size > 1 then
340
+ ns.map! { |n| Regexp.escape n }
341
+ "%s#\(?:%s\)" % [Regexp.escape(k.name), ns.join("|")]
342
+ else
343
+ "%s#%s" % [Regexp.escape(k.name), ns.first]
344
+ end
345
+ }
346
+ end
347
+
348
+ ##
349
+ # Handle the case where a line number doesn't match any known tests.
350
+ # Returns true to signal that running should stop.
351
+
352
+ def handle_missing_tests? tests
353
+ _tests = tests.values.flatten
354
+ not_found = by_line
355
+ .flat_map { |f, ls| ls.map { |l| [f, l] } }
356
+ .reject { |f, l|
357
+ _tests.any? { |t| t.path == f and t.include? l }
358
+ }
359
+
360
+ unless not_found.empty? then
361
+ by_path = all_tests.values.flatten.group_by(&:path)
362
+
363
+ puts
364
+ puts "ERROR: test(s) not found at:"
365
+ not_found.each do |f, l|
366
+ puts " %s:%s" % [f, l]
367
+ puts
368
+ puts "Did you mean?"
369
+ puts
370
+ l = l.begin if l.is_a? Range
371
+ by_path[f] and
372
+ by_path[f]
373
+ .sort_by { |m| (m.line_s - l).abs }
374
+ .first(2)
375
+ .each do |m|
376
+ puts " %-30s (dist=%+d) (%s)" % [m, m.line_s - l, m.name]
377
+ end
378
+ puts
379
+ end
380
+ $stdout.flush
381
+ $stderr.flush
382
+ true
383
+ end
384
+ end
385
+
386
+ ##
387
+ # Simple TestMethod (abbr TM) Data object.
388
+
389
+ TM = Data.define :klass, :name, :path, :lines do
390
+ def initialize klass:, name:
391
+ method = klass.instance_method name
392
+ path, line_s = method.source_location
393
+
394
+ path = path.delete_prefix "#{Dir.pwd}/"
395
+
396
+ line_e = line_s + TM.source_for(method).lines.size - 1
397
+
398
+ lines = line_s..line_e
399
+
400
+ super klass:, name:, path:, lines:
401
+ end
402
+
403
+ def self.source_for method
404
+ path, line = method.source_location
405
+ file = cache[path] ||= File.readlines(path)
406
+
407
+ ruby = +""
408
+
409
+ file[line-1..].each do |l|
410
+ ruby << l
411
+ return ruby if Prism.parse_success? ruby
412
+ end
413
+
414
+ nil
415
+ end
416
+
417
+ def self.cache = @cache ||= {}
418
+
419
+ def include?(o) = o.is_a?(Integer) ? lines.include?(o) : lines.overlap?(o)
420
+
421
+ def to_s = "%s:%d-%d" % [path, lines.begin, lines.end]
422
+
423
+ def line_s = lines.begin
424
+ end
425
+ end
@@ -1,4 +1,4 @@
1
- require "minitest"
1
+ require_relative "../minitest"
2
2
 
3
- Minitest.load_plugins
3
+ Minitest.load :pride
4
4
  Minitest::PrideIO.pride!
@@ -1,4 +1,4 @@
1
- require "minitest"
1
+ require_relative "../minitest"
2
2
 
3
3
  module Minitest
4
4
  def self.plugin_pride_options opts, _options # :nodoc:
@@ -0,0 +1,45 @@
1
+ require "drb"
2
+ require "tmpdir"
3
+ require_relative "../minitest"
4
+
5
+ class Minitest::Server
6
+ VERSION = "1.0.9"
7
+
8
+ TOPDIR = Dir.pwd + "/"
9
+
10
+ def self.path pid = $$
11
+ "drbunix:#{Dir.tmpdir}/minitest.#{pid}"
12
+ end
13
+
14
+ def self.run client
15
+ DRb.start_service path, new(client)
16
+ end
17
+
18
+ def self.stop
19
+ DRb.stop_service
20
+ end
21
+
22
+ attr_accessor :client
23
+
24
+ def initialize client
25
+ self.client = client
26
+ end
27
+
28
+ def quit
29
+ self.class.stop
30
+ end
31
+
32
+ def start
33
+ client.minitest_start
34
+ end
35
+
36
+ def result file, klass, method, fails, assertions, time
37
+ file = file.sub(/^#{TOPDIR}/, "")
38
+
39
+ client.minitest_result file, klass, method, fails, assertions, time
40
+ end
41
+
42
+ def report
43
+ # do nothing
44
+ end
45
+ end