minitest 5.27.0 → 6.0.0.a1

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.
@@ -0,0 +1,306 @@
1
+ require_relative "find_minimal_combination"
2
+ require_relative "server"
3
+ require "shellwords"
4
+ require "rbconfig"
5
+ require_relative "path_expander" # this is gonna break some shit?
6
+
7
+ module Minitest; end # :nodoc:
8
+
9
+ ##
10
+ # Minitest::Bisect helps you isolate and debug random test failures.
11
+
12
+ class Minitest::Bisect
13
+ VERSION = "1.8.0" # :nodoc:
14
+
15
+ class PathExpander < Minitest::VendoredPathExpander # :nodoc:
16
+ TEST_GLOB = "**/{test_*,*_test,spec_*,*_spec}.rb" # :nodoc:
17
+
18
+ attr_accessor :rb_flags
19
+
20
+ def initialize args = ARGV # :nodoc:
21
+ super args, TEST_GLOB, "test"
22
+ self.rb_flags = %w[-Itest:lib]
23
+ end
24
+
25
+ ##
26
+ # Overrides PathExpander#process_flags to filter out ruby flags
27
+ # from minitest flags. Only supports -I<paths>, -d, and -w for
28
+ # ruby.
29
+
30
+ def process_flags flags
31
+ flags.reject { |flag| # all hits are truthy, so this works out well
32
+ case flag
33
+ when /^-I(.*)/ then
34
+ rb_flags << flag
35
+ when /^-d/ then
36
+ rb_flags << flag
37
+ when /^-w/ then
38
+ rb_flags << flag
39
+ else
40
+ false
41
+ end
42
+ }
43
+ end
44
+ end
45
+
46
+ mtbv = ENV["MTB_VERBOSE"].to_i
47
+ SHH = case # :nodoc:
48
+ when mtbv == 1 then " > /dev/null"
49
+ when mtbv >= 2 then nil
50
+ else " > /dev/null 2>&1"
51
+ end
52
+
53
+ # Borrowed from rake
54
+ RUBY = ENV['RUBY'] ||
55
+ File.join(RbConfig::CONFIG['bindir'],
56
+ RbConfig::CONFIG['ruby_install_name'] +
57
+ RbConfig::CONFIG['EXEEXT']).sub(/.*\s.*/m, '"\&"')
58
+
59
+ ##
60
+ # True if this run has seen a failure.
61
+
62
+ attr_accessor :tainted
63
+ alias :tainted? :tainted
64
+
65
+ ##
66
+ # Failures seen in this run. Shape:
67
+ #
68
+ # {"file.rb"=>{"Class"=>["test_method1", "test_method2"] ...} ...}
69
+
70
+ attr_accessor :failures
71
+
72
+ ##
73
+ # An array of tests seen so far. NOT cleared by #reset.
74
+
75
+ attr_accessor :culprits
76
+
77
+ attr_accessor :seen_bad # :nodoc:
78
+
79
+ ##
80
+ # Top-level runner. Instantiate and call +run+, handling exceptions.
81
+
82
+ def self.run files
83
+ new.run files
84
+ rescue => e
85
+ warn e.message
86
+ warn "Try running with MTB_VERBOSE=2 to verify."
87
+ exit 1
88
+ end
89
+
90
+ ##
91
+ # Instantiate a new Bisect.
92
+
93
+ def initialize
94
+ self.culprits = []
95
+ self.failures = Hash.new { |h, k| h[k] = Hash.new { |h2, k2| h2[k2] = [] } }
96
+ end
97
+
98
+ ##
99
+ # Reset per-bisect-run variables.
100
+
101
+ def reset
102
+ self.seen_bad = false
103
+ self.tainted = false
104
+ failures.clear
105
+ # not clearing culprits on purpose
106
+ end
107
+
108
+ ##
109
+ # Instance-level runner. Handles Minitest::Server, argument
110
+ # processing, and invoking +bisect_methods+.
111
+
112
+ def run args
113
+ Minitest::Server.run self
114
+
115
+ cmd = nil
116
+
117
+ mt_flags = args.dup
118
+ expander = Minitest::Bisect::PathExpander.new mt_flags
119
+
120
+ files = expander.process.to_a
121
+ rb_flags = expander.rb_flags
122
+ mt_flags += ["--server", $$.to_s]
123
+
124
+ cmd = bisect_methods files, rb_flags, mt_flags
125
+
126
+ puts "Final reproduction:"
127
+ puts
128
+
129
+ system cmd.sub(/--server \d+/, "")
130
+ ensure
131
+ Minitest::Server.stop
132
+ end
133
+
134
+ ##
135
+ # Normal: find "what is the minimal combination of tests to run to
136
+ # make X fail?"
137
+ #
138
+ # Run with: minitest_bisect ... --seed=N
139
+ #
140
+ # 1. Verify the failure running normally with the seed.
141
+ # 2. If no failure, punt.
142
+ # 3. If no passing tests before failure, punt. (No culprits == no debug)
143
+ # 4. Verify the failure doesn't fail in isolation.
144
+ # 5. If it still fails by itself, warn that it might not be an ordering
145
+ # issue.
146
+ # 6. Cull all tests after the failure, they're not involved.
147
+ # 7. Bisect the culprits + bad until you find a minimal combo that fails.
148
+ # 8. Display minimal combo by running one last time.
149
+ #
150
+ # Inverted: find "what is the minimal combination of tests to run to
151
+ # make this test pass?"
152
+ #
153
+ # Run with: minitest_bisect ... --seed=N -n="/failing_test_name_regexp/"
154
+ #
155
+ # 1. Verify the failure by running normally w/ the seed and -n=/.../
156
+ # 2. If no failure, punt.
157
+ # 3. Verify the passing case by running everything.
158
+ # 4. If failure, punt. This is not a false positive.
159
+ # 5. Cull all tests after the bad test from #1, they're not involved.
160
+ # 6. Bisect the culprits + bad until you find a minimal combo that passes.
161
+ # 7. Display minimal combo by running one last time.
162
+
163
+ def bisect_methods files, rb_flags, mt_flags
164
+ bad_names, mt_flags = mt_flags.partition { |s| s =~ /^(?:-n|--name)/ }
165
+ normal = bad_names.empty?
166
+ inverted = !normal
167
+
168
+ if inverted then
169
+ time_it "reproducing w/ scoped failure (inverted run!)...", build_methods_cmd(build_files_cmd(files, rb_flags, mt_flags + bad_names))
170
+ raise "No failures. Probably not a false positive. Aborting." if failures.empty?
171
+ bad = map_failures
172
+ end
173
+
174
+ cmd = build_files_cmd(files, rb_flags, mt_flags)
175
+
176
+ msg = normal ? "reproducing..." : "reproducing false positive..."
177
+ time_it msg, build_methods_cmd(cmd)
178
+
179
+ if normal then
180
+ raise "Reproduction run passed? Aborting." unless tainted?
181
+ raise "Verification failed. No culprits? Aborting." if culprits.empty? && seen_bad
182
+ else
183
+ raise "Reproduction failed? Not false positive. Aborting." if tainted?
184
+ raise "Verification failed. No culprits? Aborting." if culprits.empty? || seen_bad
185
+ end
186
+
187
+ if normal then
188
+ bad = map_failures
189
+
190
+ time_it "verifying...", build_methods_cmd(cmd, [], bad)
191
+
192
+ new_bad = map_failures
193
+
194
+ if bad == new_bad then
195
+ warn "Tests fail by themselves. This may not be an ordering issue."
196
+ end
197
+ end
198
+
199
+ idx = culprits.index bad.first
200
+ self.culprits = culprits.take idx+1 if idx # cull tests after bad
201
+
202
+ # culprits populated by initial reproduction via minitest/server
203
+ found, count = culprits.find_minimal_combination_and_count do |test|
204
+ prompt = "# of culprit methods: #{test.size}"
205
+
206
+ time_it prompt, build_methods_cmd(cmd, test, bad)
207
+
208
+ normal == tainted? # either normal and failed, or inverse and passed
209
+ end
210
+
211
+ puts
212
+ puts "Minimal methods found in #{count} steps:"
213
+ puts
214
+ puts "Culprit methods: %p" % [found + bad]
215
+ puts
216
+ cmd = build_methods_cmd cmd, found, bad
217
+ puts cmd.sub(/--server \d+/, "")
218
+ puts
219
+ cmd
220
+ end
221
+
222
+ def time_it prompt, cmd # :nodoc:
223
+ print prompt
224
+ t0 = Time.now
225
+ system "#{cmd} #{SHH}"
226
+ puts " in %.2f sec" % (Time.now - t0)
227
+ end
228
+
229
+ def map_failures # :nodoc:
230
+ # from: {"file.rb"=>{"Class"=>["test_method1", "test_method2"]}}
231
+ # to: ["Class#test_method1", "Class#test_method2"]
232
+ failures.values.map { |h|
233
+ h.map { |k,vs| vs.map { |v| "#{k}##{v}" } }
234
+ }.flatten.sort
235
+ end
236
+
237
+ def build_files_cmd culprits, rb, mt # :nodoc:
238
+ tests = culprits.flatten.compact.map { |f| %(require "./#{f}") }.join " ; "
239
+
240
+ %(#{RUBY} #{rb.shelljoin} -e '#{tests}' -- #{mt.map(&:to_s).shelljoin})
241
+ end
242
+
243
+ def build_methods_cmd cmd, culprits = [], bad = nil # :nodoc:
244
+ reset
245
+
246
+ if bad then
247
+ re = build_re culprits + bad
248
+
249
+ cmd += " -n \"#{re}\"" if bad
250
+ end
251
+
252
+ if ENV["MTB_VERBOSE"].to_i >= 1 then
253
+ puts
254
+ puts cmd
255
+ puts
256
+ end
257
+
258
+ cmd
259
+ end
260
+
261
+ def build_re bad # :nodoc:
262
+ re = []
263
+
264
+ # bad by class, you perv
265
+ bbc = bad.map { |s| s.split(/#/, 2) }.group_by(&:first)
266
+
267
+ bbc.each do |klass, methods|
268
+ methods = methods.map(&:last).flatten.uniq.map { |method|
269
+ re_escape method
270
+ }
271
+
272
+ methods = methods.join "|"
273
+ re << /#{re_escape klass}#(?:#{methods})/.to_s[7..-2] # (?-mix:...)
274
+ end
275
+
276
+ re = re.join("|").to_s.gsub(/-mix/, "")
277
+
278
+ "/^(?:#{re})$/"
279
+ end
280
+
281
+ def re_escape str # :nodoc:
282
+ str.gsub(/([`'"!?&\[\]\(\)\{\}\|\+])/, '\\\\\1')
283
+ end
284
+
285
+ ############################################################
286
+ # Server Methods:
287
+
288
+ def minitest_start # :nodoc:
289
+ self.failures.clear
290
+ end
291
+
292
+ def minitest_result file, klass, method, fails, assertions, time # :nodoc:
293
+ fails.reject! { |fail| Minitest::Skip === fail }
294
+
295
+ if fails.empty? then
296
+ culprits << "#{klass}##{method}" unless seen_bad # UGH
297
+ else
298
+ self.seen_bad = true
299
+ end
300
+
301
+ return if fails.empty?
302
+
303
+ self.tainted = true
304
+ self.failures[file][klass] << method
305
+ end
306
+ end
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env -S ruby
2
+
3
+ # :stopdoc:
4
+
5
+ require "optparse"
6
+ require "shellwords"
7
+
8
+ # complete -o bashdefault -f -C 'ruby lib/minitest/complete.rb' minitest
9
+ # using eg:
10
+ # COMP_LINE="blah test/test_file.rb -n test_pattern"
11
+ # or test directly with:
12
+ # ./lib/minitest/complete.rb test/test_file.rb -n test_pattern
13
+
14
+ argv = Shellwords.split ENV["COMP_LINE"] || ARGV.join(" ")
15
+ comp_re = nil
16
+
17
+ begin
18
+ OptionParser.new do |opts|
19
+ # part of my unofficial embedded gem "makeoptparseworkwell"
20
+ def opts.topdict(name) = (name.length > 1 ? top.long : top.short)
21
+ def opts.alias(from, to) = (dict = topdict(from) ; dict[to] = dict[from])
22
+
23
+ opts.on "-n", "--name [METHOD]", "minitest option" do |m|
24
+ comp_re = Regexp.new m
25
+ end
26
+
27
+ opts.alias "name", "include"
28
+ opts.alias "name", "exclude"
29
+ opts.alias "n", "i"
30
+ opts.alias "n", "e"
31
+ opts.alias "n", "x"
32
+ end.parse! argv
33
+ rescue
34
+ retry # ignore options passed to Ruby
35
+ end
36
+
37
+ path = argv.find_all { |f| File.file? f }.last
38
+
39
+ exit unless comp_re && path
40
+
41
+ require "prism"
42
+
43
+ names, queue = [], [Prism.parse_file(path).value]
44
+
45
+ while node = queue.shift do
46
+ if node.type == :def_node then
47
+ name = node.name
48
+ names << name if name =~ comp_re
49
+ else
50
+ queue.concat node.compact_child_nodes # no need to process def body
51
+ end
52
+ end
53
+
54
+ puts names.sort
55
+
56
+ # :startdoc:
@@ -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
@@ -1,16 +1,4 @@
1
- require_relative "../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}"
@@ -32,7 +32,7 @@ module Minitest
32
32
  while job = queue.pop do
33
33
  klass, method, reporter = job
34
34
  reporter.synchronize { reporter.prerecord klass, method }
35
- result = Minitest.run_one_method klass, method
35
+ result = klass.new(method).run
36
36
  reporter.synchronize { reporter.record result }
37
37
  end
38
38
  end
@@ -59,11 +59,11 @@ module Minitest
59
59
  def _synchronize; Minitest::Test.io_lock.synchronize { yield }; end # :nodoc:
60
60
 
61
61
  module ClassMethods # :nodoc:
62
- def run_one_method klass, method_name, reporter
62
+ def run klass, method_name, reporter
63
63
  Minitest.parallel_executor << [klass, method_name, reporter]
64
64
  end
65
65
 
66
- def test_order
66
+ def run_order
67
67
  :parallel
68
68
  end
69
69
  end