minitest 5.25.5 → 6.0.4
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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +2 -4
- data/History.rdoc +156 -0
- data/Manifest.txt +13 -4
- data/README.rdoc +21 -100
- data/Rakefile +10 -2
- data/bin/minitest +5 -0
- data/design_rationale.rb +21 -19
- data/lib/hoe/minitest.rb +2 -1
- data/lib/minitest/assertions.rb +39 -73
- data/lib/minitest/autorun.rb +3 -4
- data/lib/minitest/benchmark.rb +3 -3
- data/lib/minitest/bisect.rb +304 -0
- data/lib/minitest/complete.rb +56 -0
- data/lib/minitest/find_minimal_combination.rb +127 -0
- data/lib/minitest/hell.rb +1 -1
- data/lib/minitest/manual_plugins.rb +4 -16
- data/lib/minitest/parallel.rb +5 -3
- data/lib/minitest/path_expander.rb +432 -0
- data/lib/minitest/pride.rb +2 -2
- data/lib/minitest/pride_plugin.rb +1 -1
- data/lib/minitest/server.rb +49 -0
- data/lib/minitest/server_plugin.rb +88 -0
- data/lib/minitest/spec.rb +11 -37
- data/lib/minitest/sprint.rb +105 -0
- data/lib/minitest/sprint_plugin.rb +39 -0
- data/lib/minitest/test.rb +8 -13
- data/lib/minitest/test_task.rb +44 -20
- data/lib/minitest.rb +104 -107
- data/test/minitest/metametameta.rb +1 -1
- data/test/minitest/test_bisect.rb +249 -0
- data/test/minitest/test_find_minimal_combination.rb +138 -0
- data/test/minitest/test_minitest_assertions.rb +54 -105
- data/test/minitest/test_minitest_benchmark.rb +14 -0
- data/test/minitest/test_minitest_reporter.rb +6 -5
- data/test/minitest/test_minitest_spec.rb +60 -128
- data/test/minitest/test_minitest_test.rb +22 -101
- data/test/minitest/test_path_expander.rb +229 -0
- data/test/minitest/test_server.rb +146 -0
- data.tar.gz.sig +0 -0
- metadata +92 -33
- metadata.gz.sig +0 -0
- data/.autotest +0 -34
- data/lib/minitest/mock.rb +0 -347
- data/lib/minitest/unit.rb +0 -42
- data/test/minitest/test_minitest_mock.rb +0 -1218
data/lib/minitest/assertions.rb
CHANGED
|
@@ -62,11 +62,11 @@ module Minitest
|
|
|
62
62
|
return "Expected: #{mu_pp exp}\n Actual: #{mu_pp act}" unless
|
|
63
63
|
expect
|
|
64
64
|
|
|
65
|
-
Tempfile.
|
|
65
|
+
Tempfile.create "expect" do |a|
|
|
66
66
|
a.puts expect
|
|
67
67
|
a.flush
|
|
68
68
|
|
|
69
|
-
Tempfile.
|
|
69
|
+
Tempfile.create "butwas" do |b|
|
|
70
70
|
b.puts butwas
|
|
71
71
|
b.flush
|
|
72
72
|
|
|
@@ -186,9 +186,8 @@ module Minitest
|
|
|
186
186
|
# Fails unless +obj+ is empty.
|
|
187
187
|
|
|
188
188
|
def assert_empty obj, msg = nil
|
|
189
|
-
msg = message(msg) { "Expected #{mu_pp
|
|
190
|
-
|
|
191
|
-
assert obj.empty?, msg
|
|
189
|
+
msg = message(msg) { "Expected #{mu_pp(obj)} to be empty" }
|
|
190
|
+
assert_predicate obj, :empty?, msg
|
|
192
191
|
end
|
|
193
192
|
|
|
194
193
|
def _where # :nodoc:
|
|
@@ -196,8 +195,6 @@ module Minitest
|
|
|
196
195
|
.split(":in ", 2).first # clean up noise
|
|
197
196
|
end
|
|
198
197
|
|
|
199
|
-
E = "" # :nodoc:
|
|
200
|
-
|
|
201
198
|
##
|
|
202
199
|
# Fails unless <tt>exp == act</tt> printing the difference between
|
|
203
200
|
# the two, if possible.
|
|
@@ -212,18 +209,11 @@ module Minitest
|
|
|
212
209
|
# See also: Minitest::Assertions.diff
|
|
213
210
|
|
|
214
211
|
def assert_equal exp, act, msg = nil
|
|
215
|
-
msg = message(msg,
|
|
216
|
-
result = assert exp == act, msg
|
|
212
|
+
msg = message(msg, nil) { diff exp, act }
|
|
217
213
|
|
|
218
|
-
if nil
|
|
219
|
-
if Minitest::VERSION >= "6" then
|
|
220
|
-
refute_nil exp, "Use assert_nil if expecting nil."
|
|
221
|
-
else
|
|
222
|
-
warn "DEPRECATED: Use assert_nil if expecting nil from #{_where}. This will fail in Minitest 6."
|
|
223
|
-
end
|
|
224
|
-
end
|
|
214
|
+
refute_nil exp, message { "Use assert_nil if expecting nil" } if exp.nil? # don't count
|
|
225
215
|
|
|
226
|
-
|
|
216
|
+
assert exp == act, msg
|
|
227
217
|
end
|
|
228
218
|
|
|
229
219
|
##
|
|
@@ -255,8 +245,7 @@ module Minitest
|
|
|
255
245
|
msg = message(msg) {
|
|
256
246
|
"Expected #{mu_pp collection} to include #{mu_pp obj}"
|
|
257
247
|
}
|
|
258
|
-
|
|
259
|
-
assert collection.include?(obj), msg
|
|
248
|
+
assert_operator collection, :include?, obj, msg
|
|
260
249
|
end
|
|
261
250
|
|
|
262
251
|
##
|
|
@@ -308,6 +297,7 @@ module Minitest
|
|
|
308
297
|
|
|
309
298
|
def assert_operator o1, op, o2 = UNDEFINED, msg = nil
|
|
310
299
|
return assert_predicate o1, op, msg if UNDEFINED == o2
|
|
300
|
+
assert_respond_to o1, op
|
|
311
301
|
msg = message(msg) { "Expected #{mu_pp o1} to be #{op} #{mu_pp o2}" }
|
|
312
302
|
assert o1.__send__(op, o2), msg
|
|
313
303
|
end
|
|
@@ -368,15 +358,12 @@ module Minitest
|
|
|
368
358
|
# error.
|
|
369
359
|
|
|
370
360
|
def assert_pattern
|
|
371
|
-
raise NotImplementedError, "only available in Ruby 3.0+" unless RUBY_VERSION >= "3.0"
|
|
372
361
|
flunk "assert_pattern requires a block to capture errors." unless block_given?
|
|
373
362
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
flunk e.message
|
|
379
|
-
end
|
|
363
|
+
yield
|
|
364
|
+
pass
|
|
365
|
+
rescue NoMatchingPatternError => e
|
|
366
|
+
flunk e.message
|
|
380
367
|
end
|
|
381
368
|
|
|
382
369
|
##
|
|
@@ -389,6 +376,7 @@ module Minitest
|
|
|
389
376
|
# str.must_be :empty?
|
|
390
377
|
|
|
391
378
|
def assert_predicate o1, op, msg = nil
|
|
379
|
+
assert_respond_to o1, op, include_all:true
|
|
392
380
|
msg = message(msg) { "Expected #{mu_pp o1} to be #{op}" }
|
|
393
381
|
assert o1.__send__(op), msg
|
|
394
382
|
end
|
|
@@ -448,9 +436,7 @@ module Minitest
|
|
|
448
436
|
# include_all defaults to false to match Object#respond_to?
|
|
449
437
|
|
|
450
438
|
def assert_respond_to obj, meth, msg = nil, include_all: false
|
|
451
|
-
msg = message(msg) {
|
|
452
|
-
"Expected #{mu_pp obj} (#{obj.class}) to respond to ##{meth}"
|
|
453
|
-
}
|
|
439
|
+
msg = message(msg) { "Expected #{mu_pp obj} (#{obj.class}) to respond to ##{meth}" }
|
|
454
440
|
assert obj.respond_to?(meth, include_all), msg
|
|
455
441
|
end
|
|
456
442
|
|
|
@@ -462,22 +448,10 @@ module Minitest
|
|
|
462
448
|
data = [mu_pp(act), act.object_id, mu_pp(exp), exp.object_id]
|
|
463
449
|
"Expected %s (oid=%d) to be the same as %s (oid=%d)" % data
|
|
464
450
|
}
|
|
465
|
-
assert exp.equal?(act), msg
|
|
466
|
-
end
|
|
467
451
|
|
|
468
|
-
|
|
469
|
-
# +send_ary+ is a receiver, message and arguments.
|
|
470
|
-
#
|
|
471
|
-
# Fails unless the call returns a true value
|
|
472
|
-
|
|
473
|
-
def assert_send send_ary, m = nil
|
|
474
|
-
warn "DEPRECATED: assert_send. From #{_where}"
|
|
452
|
+
refute_nil exp, message { "Use assert_nil if expecting nil" } if exp.nil? # don't count
|
|
475
453
|
|
|
476
|
-
|
|
477
|
-
m = message(m) {
|
|
478
|
-
"Expected #{mu_pp recv}.#{msg}(*#{mu_pp args}) to return true"
|
|
479
|
-
}
|
|
480
|
-
assert recv.__send__(msg, *args), m
|
|
454
|
+
assert exp.equal?(act), msg
|
|
481
455
|
end
|
|
482
456
|
|
|
483
457
|
##
|
|
@@ -500,14 +474,9 @@ module Minitest
|
|
|
500
474
|
value = catch sym do
|
|
501
475
|
begin
|
|
502
476
|
yield
|
|
503
|
-
rescue
|
|
504
|
-
default += ", not :#{e.message[/uncaught throw \`(\w+?)\'/, 1]}"
|
|
505
|
-
rescue ArgumentError => e # 1.9 exception
|
|
477
|
+
rescue ArgumentError => e # 1.9+ exception
|
|
506
478
|
raise e unless e.message.include? "uncaught throw"
|
|
507
479
|
default += ", not #{e.message.split(/ /).last}"
|
|
508
|
-
rescue NameError => e # 1.8 exception
|
|
509
|
-
raise e unless e.name == sym
|
|
510
|
-
default += ", not #{e.name.inspect}"
|
|
511
480
|
end
|
|
512
481
|
caught = false
|
|
513
482
|
end
|
|
@@ -629,13 +598,16 @@ module Minitest
|
|
|
629
598
|
end
|
|
630
599
|
|
|
631
600
|
##
|
|
632
|
-
# Returns a proc that
|
|
601
|
+
# Returns a proc that delays generation of an output message. If
|
|
602
|
+
# +msg+ is a proc (eg, from another +message+ call) return +msg+
|
|
603
|
+
# as-is. Otherwise, return a proc that will output +msg+ along
|
|
604
|
+
# with the value of the result of the block passed to +message+.
|
|
633
605
|
|
|
634
|
-
def message msg = nil, ending =
|
|
606
|
+
def message msg = nil, ending = ".", &default
|
|
607
|
+
return msg if Proc === msg
|
|
635
608
|
proc {
|
|
636
|
-
msg = msg.call.chomp(".") if Proc === msg
|
|
637
609
|
custom_message = "#{msg}.\n" unless msg.nil? or msg.to_s.empty?
|
|
638
|
-
"#{custom_message}#{default.call}#{ending
|
|
610
|
+
"#{custom_message}#{default.call}#{ending}"
|
|
639
611
|
}
|
|
640
612
|
end
|
|
641
613
|
|
|
@@ -659,8 +631,7 @@ module Minitest
|
|
|
659
631
|
|
|
660
632
|
def refute_empty obj, msg = nil
|
|
661
633
|
msg = message(msg) { "Expected #{mu_pp obj} to not be empty" }
|
|
662
|
-
|
|
663
|
-
refute obj.empty?, msg
|
|
634
|
+
refute_predicate obj, :empty?, msg
|
|
664
635
|
end
|
|
665
636
|
|
|
666
637
|
##
|
|
@@ -692,19 +663,16 @@ module Minitest
|
|
|
692
663
|
# For comparing Floats. Fails if +exp+ and +act+ have a relative error
|
|
693
664
|
# less than +epsilon+.
|
|
694
665
|
|
|
695
|
-
def refute_in_epsilon
|
|
696
|
-
refute_in_delta
|
|
666
|
+
def refute_in_epsilon exp, act, epsilon = 0.001, msg = nil
|
|
667
|
+
refute_in_delta exp, act, [exp.abs, act.abs].min * epsilon, msg
|
|
697
668
|
end
|
|
698
669
|
|
|
699
670
|
##
|
|
700
|
-
# Fails if +
|
|
671
|
+
# Fails if +obj+ includes +sub+.
|
|
701
672
|
|
|
702
|
-
def refute_includes
|
|
703
|
-
msg = message(msg) {
|
|
704
|
-
|
|
705
|
-
}
|
|
706
|
-
assert_respond_to collection, :include?
|
|
707
|
-
refute collection.include?(obj), msg
|
|
673
|
+
def refute_includes obj, sub, msg = nil
|
|
674
|
+
msg = message(msg) { "Expected #{mu_pp obj} to not include #{mu_pp sub}" }
|
|
675
|
+
refute_operator obj, :include?, sub, msg
|
|
708
676
|
end
|
|
709
677
|
|
|
710
678
|
##
|
|
@@ -730,9 +698,8 @@ module Minitest
|
|
|
730
698
|
|
|
731
699
|
def refute_match matcher, obj, msg = nil
|
|
732
700
|
msg = message(msg) { "Expected #{mu_pp matcher} to not match #{mu_pp obj}" }
|
|
733
|
-
assert_respond_to matcher, :=~
|
|
734
701
|
matcher = Regexp.new Regexp.escape matcher if String === matcher
|
|
735
|
-
|
|
702
|
+
refute_operator matcher, :=~, obj, msg
|
|
736
703
|
end
|
|
737
704
|
|
|
738
705
|
##
|
|
@@ -756,15 +723,12 @@ module Minitest
|
|
|
756
723
|
# other exceptions will be raised as normal and generate a test error.
|
|
757
724
|
|
|
758
725
|
def refute_pattern
|
|
759
|
-
raise NotImplementedError, "only available in Ruby 3.0+" unless RUBY_VERSION >= "3.0"
|
|
760
726
|
flunk "refute_pattern requires a block to capture errors." unless block_given?
|
|
761
727
|
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
pass
|
|
767
|
-
end
|
|
728
|
+
yield
|
|
729
|
+
flunk "NoMatchingPatternError expected, but nothing was raised."
|
|
730
|
+
rescue NoMatchingPatternError
|
|
731
|
+
pass
|
|
768
732
|
end
|
|
769
733
|
|
|
770
734
|
##
|
|
@@ -775,6 +739,7 @@ module Minitest
|
|
|
775
739
|
|
|
776
740
|
def refute_operator o1, op, o2 = UNDEFINED, msg = nil
|
|
777
741
|
return refute_predicate o1, op, msg if UNDEFINED == o2
|
|
742
|
+
assert_respond_to o1, op
|
|
778
743
|
msg = message(msg) { "Expected #{mu_pp o1} to not be #{op} #{mu_pp o2}" }
|
|
779
744
|
refute o1.__send__(op, o2), msg
|
|
780
745
|
end
|
|
@@ -797,6 +762,7 @@ module Minitest
|
|
|
797
762
|
# str.wont_be :empty?
|
|
798
763
|
|
|
799
764
|
def refute_predicate o1, op, msg = nil
|
|
765
|
+
assert_respond_to o1, op, include_all:true
|
|
800
766
|
msg = message(msg) { "Expected #{mu_pp o1} to not be #{op}" }
|
|
801
767
|
refute o1.__send__(op), msg
|
|
802
768
|
end
|
data/lib/minitest/autorun.rb
CHANGED
data/lib/minitest/benchmark.rb
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
require_relative "test"
|
|
2
|
+
require_relative "spec"
|
|
3
3
|
|
|
4
4
|
module Minitest
|
|
5
5
|
##
|
|
@@ -17,7 +17,7 @@ module Minitest
|
|
|
17
17
|
self.class.io
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
-
def self.run
|
|
20
|
+
def self.run klass, method_name, reporter # :nodoc:
|
|
21
21
|
@io = reporter.io
|
|
22
22
|
super
|
|
23
23
|
end
|
|
@@ -0,0 +1,304 @@
|
|
|
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[]
|
|
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({"MINITEST_SERVER" => "1"}, 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({"MINITEST_SERVER" => "1"}, "#{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, cmd:$0 # :nodoc:
|
|
238
|
+
([cmd] + rb + culprits + mt).shelljoin
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def build_methods_cmd cmd, culprits = [], bad = nil # :nodoc:
|
|
242
|
+
reset
|
|
243
|
+
|
|
244
|
+
if bad then
|
|
245
|
+
re = build_re culprits + bad
|
|
246
|
+
|
|
247
|
+
cmd += " -n \"#{re}\"" if bad
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
if ENV["MTB_VERBOSE"].to_i >= 1 then
|
|
251
|
+
puts
|
|
252
|
+
puts cmd
|
|
253
|
+
puts
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
cmd
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def build_re bad # :nodoc:
|
|
260
|
+
re = []
|
|
261
|
+
|
|
262
|
+
# bad by class, you perv
|
|
263
|
+
bbc = bad.map { |s| s.split(/#/, 2) }.group_by(&:first)
|
|
264
|
+
|
|
265
|
+
bbc.each do |klass, methods|
|
|
266
|
+
methods = methods.map(&:last).flatten.uniq.map { |method|
|
|
267
|
+
re_escape method
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
methods = methods.join "|"
|
|
271
|
+
re << /#{re_escape klass}#(?:#{methods})/.to_s[7..-2] # (?-mix:...)
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
re = re.join("|").to_s.gsub(/-mix/, "")
|
|
275
|
+
|
|
276
|
+
"/^(?:#{re})$/"
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def re_escape str # :nodoc:
|
|
280
|
+
str.gsub(/([`'"!?&\[\]\(\)\{\}\|\+])/, '\\\\\1')
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
############################################################
|
|
284
|
+
# Server Methods:
|
|
285
|
+
|
|
286
|
+
def minitest_start # :nodoc:
|
|
287
|
+
self.failures.clear
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def minitest_result file, klass, method, fails, assertions, time # :nodoc:
|
|
291
|
+
fails.reject! { |fail| Minitest::Skip === fail }
|
|
292
|
+
|
|
293
|
+
if fails.empty? then
|
|
294
|
+
culprits << "#{klass}##{method}" unless seen_bad # UGH
|
|
295
|
+
else
|
|
296
|
+
self.seen_bad = true
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
return if fails.empty?
|
|
300
|
+
|
|
301
|
+
self.tainted = true
|
|
302
|
+
self.failures[file][klass] << method
|
|
303
|
+
end
|
|
304
|
+
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:
|