minitest 5.25.5 → 6.0.0
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 +116 -0
- data/Manifest.txt +13 -4
- data/README.rdoc +18 -98
- data/Rakefile +7 -2
- data/bin/minitest +5 -0
- data/design_rationale.rb +21 -19
- data/lib/hoe/minitest.rb +2 -1
- data/lib/minitest/assertions.rb +37 -74
- data/lib/minitest/autorun.rb +3 -4
- data/lib/minitest/benchmark.rb +2 -2
- data/lib/minitest/bisect.rb +306 -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 +418 -0
- data/lib/minitest/pride.rb +2 -2
- data/lib/minitest/pride_plugin.rb +1 -1
- data/lib/minitest/server.rb +45 -0
- data/lib/minitest/server_plugin.rb +84 -0
- data/lib/minitest/spec.rb +11 -37
- data/lib/minitest/sprint.rb +104 -0
- data/lib/minitest/sprint_plugin.rb +39 -0
- data/lib/minitest/test.rb +8 -13
- data/lib/minitest/test_task.rb +32 -17
- data/lib/minitest.rb +94 -107
- data/test/minitest/metametameta.rb +1 -1
- data/test/minitest/test_bisect.rb +235 -0
- data/test/minitest/test_find_minimal_combination.rb +138 -0
- data/test/minitest/test_minitest_assertions.rb +51 -108
- 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 +149 -0
- data.tar.gz.sig +0 -0
- metadata +54 -21
- 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
|
|
|
@@ -465,21 +451,6 @@ module Minitest
|
|
|
465
451
|
assert exp.equal?(act), msg
|
|
466
452
|
end
|
|
467
453
|
|
|
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}"
|
|
475
|
-
|
|
476
|
-
recv, msg, *args = send_ary
|
|
477
|
-
m = message(m) {
|
|
478
|
-
"Expected #{mu_pp recv}.#{msg}(*#{mu_pp args}) to return true"
|
|
479
|
-
}
|
|
480
|
-
assert recv.__send__(msg, *args), m
|
|
481
|
-
end
|
|
482
|
-
|
|
483
454
|
##
|
|
484
455
|
# Fails if the block outputs anything to stderr or stdout.
|
|
485
456
|
#
|
|
@@ -500,14 +471,9 @@ module Minitest
|
|
|
500
471
|
value = catch sym do
|
|
501
472
|
begin
|
|
502
473
|
yield
|
|
503
|
-
rescue
|
|
504
|
-
default += ", not :#{e.message[/uncaught throw \`(\w+?)\'/, 1]}"
|
|
505
|
-
rescue ArgumentError => e # 1.9 exception
|
|
474
|
+
rescue ArgumentError => e # 1.9+ exception
|
|
506
475
|
raise e unless e.message.include? "uncaught throw"
|
|
507
476
|
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
477
|
end
|
|
512
478
|
caught = false
|
|
513
479
|
end
|
|
@@ -629,13 +595,16 @@ module Minitest
|
|
|
629
595
|
end
|
|
630
596
|
|
|
631
597
|
##
|
|
632
|
-
# Returns a proc that
|
|
598
|
+
# Returns a proc that delays generation of an output message. If
|
|
599
|
+
# +msg+ is a proc (eg, from another +message+ call) return +msg+
|
|
600
|
+
# as-is. Otherwise, return a proc that will output +msg+ along
|
|
601
|
+
# with the value of the result of the block passed to +message+.
|
|
633
602
|
|
|
634
|
-
def message msg = nil, ending =
|
|
603
|
+
def message msg = nil, ending = ".", &default
|
|
604
|
+
return msg if Proc === msg
|
|
635
605
|
proc {
|
|
636
|
-
msg = msg.call.chomp(".") if Proc === msg
|
|
637
606
|
custom_message = "#{msg}.\n" unless msg.nil? or msg.to_s.empty?
|
|
638
|
-
"#{custom_message}#{default.call}#{ending
|
|
607
|
+
"#{custom_message}#{default.call}#{ending}"
|
|
639
608
|
}
|
|
640
609
|
end
|
|
641
610
|
|
|
@@ -659,8 +628,7 @@ module Minitest
|
|
|
659
628
|
|
|
660
629
|
def refute_empty obj, msg = nil
|
|
661
630
|
msg = message(msg) { "Expected #{mu_pp obj} to not be empty" }
|
|
662
|
-
|
|
663
|
-
refute obj.empty?, msg
|
|
631
|
+
refute_predicate obj, :empty?, msg
|
|
664
632
|
end
|
|
665
633
|
|
|
666
634
|
##
|
|
@@ -692,19 +660,16 @@ module Minitest
|
|
|
692
660
|
# For comparing Floats. Fails if +exp+ and +act+ have a relative error
|
|
693
661
|
# less than +epsilon+.
|
|
694
662
|
|
|
695
|
-
def refute_in_epsilon
|
|
696
|
-
refute_in_delta
|
|
663
|
+
def refute_in_epsilon exp, act, epsilon = 0.001, msg = nil
|
|
664
|
+
refute_in_delta exp, act, [exp.abs, act.abs].min * epsilon, msg
|
|
697
665
|
end
|
|
698
666
|
|
|
699
667
|
##
|
|
700
|
-
# Fails if +
|
|
668
|
+
# Fails if +obj+ includes +sub+.
|
|
701
669
|
|
|
702
|
-
def refute_includes
|
|
703
|
-
msg = message(msg) {
|
|
704
|
-
|
|
705
|
-
}
|
|
706
|
-
assert_respond_to collection, :include?
|
|
707
|
-
refute collection.include?(obj), msg
|
|
670
|
+
def refute_includes obj, sub, msg = nil
|
|
671
|
+
msg = message(msg) { "Expected #{mu_pp obj} to not include #{mu_pp sub}" }
|
|
672
|
+
refute_operator obj, :include?, sub, msg
|
|
708
673
|
end
|
|
709
674
|
|
|
710
675
|
##
|
|
@@ -730,9 +695,8 @@ module Minitest
|
|
|
730
695
|
|
|
731
696
|
def refute_match matcher, obj, msg = nil
|
|
732
697
|
msg = message(msg) { "Expected #{mu_pp matcher} to not match #{mu_pp obj}" }
|
|
733
|
-
assert_respond_to matcher, :=~
|
|
734
698
|
matcher = Regexp.new Regexp.escape matcher if String === matcher
|
|
735
|
-
|
|
699
|
+
refute_operator matcher, :=~, obj, msg
|
|
736
700
|
end
|
|
737
701
|
|
|
738
702
|
##
|
|
@@ -756,15 +720,12 @@ module Minitest
|
|
|
756
720
|
# other exceptions will be raised as normal and generate a test error.
|
|
757
721
|
|
|
758
722
|
def refute_pattern
|
|
759
|
-
raise NotImplementedError, "only available in Ruby 3.0+" unless RUBY_VERSION >= "3.0"
|
|
760
723
|
flunk "refute_pattern requires a block to capture errors." unless block_given?
|
|
761
724
|
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
pass
|
|
767
|
-
end
|
|
725
|
+
yield
|
|
726
|
+
flunk "NoMatchingPatternError expected, but nothing was raised."
|
|
727
|
+
rescue NoMatchingPatternError
|
|
728
|
+
pass
|
|
768
729
|
end
|
|
769
730
|
|
|
770
731
|
##
|
|
@@ -775,6 +736,7 @@ module Minitest
|
|
|
775
736
|
|
|
776
737
|
def refute_operator o1, op, o2 = UNDEFINED, msg = nil
|
|
777
738
|
return refute_predicate o1, op, msg if UNDEFINED == o2
|
|
739
|
+
assert_respond_to o1, op
|
|
778
740
|
msg = message(msg) { "Expected #{mu_pp o1} to not be #{op} #{mu_pp o2}" }
|
|
779
741
|
refute o1.__send__(op, o2), msg
|
|
780
742
|
end
|
|
@@ -797,6 +759,7 @@ module Minitest
|
|
|
797
759
|
# str.wont_be :empty?
|
|
798
760
|
|
|
799
761
|
def refute_predicate o1, op, msg = nil
|
|
762
|
+
assert_respond_to o1, op
|
|
800
763
|
msg = message(msg) { "Expected #{mu_pp o1} to not be #{op}" }
|
|
801
764
|
refute o1.__send__(op), msg
|
|
802
765
|
end
|
data/lib/minitest/autorun.rb
CHANGED
data/lib/minitest/benchmark.rb
CHANGED
|
@@ -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:
|