minitest-bisect 1.6.0 → 1.7.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/History.rdoc +20 -0
- data/Manifest.txt +4 -18
- data/README.rdoc +48 -8
- data/Rakefile +26 -9
- data/bin/minitest_bisect +1 -1
- data/example.rb +59 -0
- data/{example-many/helper.rb → example_helper.rb} +13 -4
- data/example_inverse.rb +53 -0
- data/example_many.rb +53 -0
- data/lib/minitest/bisect.rb +129 -48
- data/lib/minitest/find_minimal_combination.rb +8 -5
- data.tar.gz.sig +0 -0
- metadata +17 -31
- metadata.gz.sig +3 -4
- data/example/helper.rb +0 -25
- data/example/test_bad1.rb +0 -4
- data/example/test_bad2.rb +0 -4
- data/example/test_bad3.rb +0 -4
- data/example/test_bad4.rb +0 -4
- data/example/test_bad5.rb +0 -4
- data/example/test_bad6.rb +0 -4
- data/example/test_bad7.rb +0 -4
- data/example/test_bad8.rb +0 -4
- data/example-many/test_bad1.rb +0 -4
- data/example-many/test_bad2.rb +0 -4
- data/example-many/test_bad3.rb +0 -4
- data/example-many/test_bad4.rb +0 -4
- data/example-many/test_bad5.rb +0 -4
- data/example-many/test_bad6.rb +0 -4
- data/example-many/test_bad7.rb +0 -4
- data/example-many/test_bad8.rb +0 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 52bae214fa2f36196af3ec54139c2bec839de200e8dee1015f916b56a666a69b
|
4
|
+
data.tar.gz: 15a204ca83b0da8a8773e565f3bc40c439713edacef4a1d66435f2f9938edf6e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 12c4227163ccd825b06f3c745bafa5a59725327e8b313c68a99cc732cea488c1cdda723c1e229d6ac50683e669594820a643c6d2bd0c5ac17867e75397969d10
|
7
|
+
data.tar.gz: '09994a51e53f704d642d27da154c835ba99b39bfbc2ddefa98e77a4357abfe062084628b6fb8c5efd9b596a641e562e595b3b0f80e196997d8bba5002c1448e5'
|
checksums.yaml.gz.sig
CHANGED
Binary file
|
data/History.rdoc
CHANGED
@@ -1,3 +1,23 @@
|
|
1
|
+
=== 1.7.0 / 2023-07-06
|
2
|
+
|
3
|
+
* 1 major enhancement:
|
4
|
+
|
5
|
+
* Extend bisect_methods to do "inverse" run (eg false positives) via -n=/RE/ argument.
|
6
|
+
|
7
|
+
* 4 minor enhancements:
|
8
|
+
|
9
|
+
* Added example_inverse.rb
|
10
|
+
* Collapsed examples from directories to individual files.
|
11
|
+
* Refactor: push command generation from #run down to #bisect_methods.
|
12
|
+
* build_files_cmd no longer calls reset (zero effect change).
|
13
|
+
|
14
|
+
* 4 bug fixes:
|
15
|
+
|
16
|
+
* 100% documentation coverage.
|
17
|
+
* Fix server process ID to be a string to match rest of args.
|
18
|
+
* Fix shebang cmd on bin/minitest_bisect.
|
19
|
+
* Print known bad methods with found culprit methods in final run.
|
20
|
+
|
1
21
|
=== 1.6.0 / 2022-05-23
|
2
22
|
|
3
23
|
* 1 minor enhancement:
|
data/Manifest.txt
CHANGED
@@ -4,24 +4,10 @@ Manifest.txt
|
|
4
4
|
README.rdoc
|
5
5
|
Rakefile
|
6
6
|
bin/minitest_bisect
|
7
|
-
example
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
example-many/test_bad4.rb
|
12
|
-
example-many/test_bad5.rb
|
13
|
-
example-many/test_bad6.rb
|
14
|
-
example-many/test_bad7.rb
|
15
|
-
example-many/test_bad8.rb
|
16
|
-
example/helper.rb
|
17
|
-
example/test_bad1.rb
|
18
|
-
example/test_bad2.rb
|
19
|
-
example/test_bad3.rb
|
20
|
-
example/test_bad4.rb
|
21
|
-
example/test_bad5.rb
|
22
|
-
example/test_bad6.rb
|
23
|
-
example/test_bad7.rb
|
24
|
-
example/test_bad8.rb
|
7
|
+
example.rb
|
8
|
+
example_helper.rb
|
9
|
+
example_inverse.rb
|
10
|
+
example_many.rb
|
25
11
|
lib/minitest/bisect.rb
|
26
12
|
lib/minitest/find_minimal_combination.rb
|
27
13
|
test/minitest/test_bisect.rb
|
data/README.rdoc
CHANGED
@@ -19,10 +19,13 @@ what minitest-bisect does best.
|
|
19
19
|
|
20
20
|
== FEATURES/PROBLEMS:
|
21
21
|
|
22
|
-
*
|
23
|
-
|
24
|
-
*
|
25
|
-
|
22
|
+
* normal mode: runs your tests with a fixed seed and minimizes the
|
23
|
+
tests to find a minimal set to reproduce the failure.
|
24
|
+
* inverted mode: runs your tests with a fixed seed and a starting
|
25
|
+
failure and minimizes the tests to find a minimal set to reproduce
|
26
|
+
the false positive.
|
27
|
+
* Includes Enumerable#find_minimal_combination and
|
28
|
+
#find_minimal_combination_and_count.
|
26
29
|
|
27
30
|
== SYNOPSIS:
|
28
31
|
|
@@ -94,8 +97,7 @@ So, you run the tests again, but this time with minitest_bisect.
|
|
94
97
|
Provide the seed given in the failure so the tests always run in the
|
95
98
|
same order and reproduce every time.
|
96
99
|
|
97
|
-
minitest_bisect will
|
98
|
-
turn around and minimize the number of methods.
|
100
|
+
minitest_bisect will minimize the number of methods.
|
99
101
|
|
100
102
|
% minitest_bisect example --seed 314
|
101
103
|
reproducing... in 203.83 sec
|
@@ -114,7 +116,7 @@ turn around and minimize the number of methods.
|
|
114
116
|
|
115
117
|
Minimal methods found in 11 steps:
|
116
118
|
|
117
|
-
Culprit methods: ["TestBad1#test_bad1_1"]
|
119
|
+
Culprit methods: ["TestBad1#test_bad1_1", "TestBad4#test_bad4_4"]
|
118
120
|
|
119
121
|
ruby -Itest:lib -e 'require "./example/test_bad1.rb" ; require "./example/test_bad2.rb" ; require "./example/test_bad3.rb" ; require "./example/test_bad4.rb" ; require "./example/test_bad5.rb" ; require "./example/test_bad6.rb" ; require "./example/test_bad7.rb" ; require "./example/test_bad8.rb"' -- --seed 314 -n "/^(?:TestBad1#(?:test_bad1_1)|TestBad4#(?:test_bad4_4))$/"
|
120
122
|
|
@@ -148,9 +150,47 @@ have and how long they take, the minimization might take a long time,
|
|
148
150
|
but each iteration can reduce the number of tests so it should get
|
149
151
|
quicker with time.
|
150
152
|
|
153
|
+
=== But sometimes, it fails:
|
154
|
+
|
155
|
+
Sometimes a test fails by itself consistently but passes most of the
|
156
|
+
time in CI. This is a false-positive in that the test SHOULD be
|
157
|
+
failing all the time but some other test is causing a side-effect that
|
158
|
+
it making it pass. This is sometimes harder to track down and reason
|
159
|
+
about. minitest-bisect now has an "inverted" mode, where you run as
|
160
|
+
normal but point it at a failing test by name. It will then validate
|
161
|
+
that the test fails by itself but passes when run in full with the
|
162
|
+
given seed. Then it tries to isolate and answer "what is the minimal
|
163
|
+
combination of tests to run to # make this test pass?". For example:
|
164
|
+
|
165
|
+
% minitest_bisect example --seed=3 -n="/failing_test_name_regexp/"
|
166
|
+
|
167
|
+
reproducing w/ scoped failure (inverted run!)... in 0.06 sec
|
168
|
+
reproducing false positive... in 0.06 sec
|
169
|
+
# of culprit methods: 2 in 0.06 sec
|
170
|
+
# of culprit methods: 1 in 0.06 sec
|
171
|
+
# of culprit methods: 1 in 0.06 sec
|
172
|
+
|
173
|
+
Minimal methods found in 3 steps:
|
174
|
+
|
175
|
+
Culprit methods: ["TestBad1#test_make_it_go", "TestBad1#test_fail_without"]
|
176
|
+
|
177
|
+
/Users/ryan/.rubies/ruby-3.2.2/bin/ruby -Itest:lib -Ilib -e 'require "./example"' -- --seed 3 -n "/^(?:TestBad1#(?:test_make_it_go|test_fail_without))$/"
|
178
|
+
|
179
|
+
Final reproduction:
|
180
|
+
|
181
|
+
Run options: --seed 3 -n "/^(?:TestBad1#(?:test_make_it_go|test_fail_without))$/"
|
182
|
+
|
183
|
+
# Running:
|
184
|
+
|
185
|
+
..
|
186
|
+
|
187
|
+
Finished in 0.000369s, 5420.0544 runs/s, 0.0000 assertions/s.
|
188
|
+
|
189
|
+
2 runs, 0 assertions, 0 failures, 0 errors, 0 skips
|
190
|
+
|
151
191
|
== REQUIREMENTS:
|
152
192
|
|
153
|
-
* minitest 5
|
193
|
+
* minitest 5+
|
154
194
|
|
155
195
|
== INSTALL:
|
156
196
|
|
data/Rakefile
CHANGED
@@ -40,11 +40,7 @@ def banner text
|
|
40
40
|
end
|
41
41
|
|
42
42
|
def run cmd
|
43
|
-
sh cmd do end
|
44
|
-
end
|
45
|
-
|
46
|
-
def req glob
|
47
|
-
Dir["#{glob}.rb"].map { |s| "require #{s.inspect}" }.join ";"
|
43
|
+
sh cmd do end # block form lets it fail w/o halting rake
|
48
44
|
end
|
49
45
|
|
50
46
|
task :repro => :isolate do
|
@@ -56,10 +52,10 @@ task :repro => :isolate do
|
|
56
52
|
ruby = "ruby -I.:lib"
|
57
53
|
|
58
54
|
banner "Original run that causes the test order dependency bug"
|
59
|
-
run "#{ruby}
|
55
|
+
run "#{ruby} example.rb --seed 1"
|
60
56
|
|
61
57
|
banner "Reduce the problem down to the minimal reproduction"
|
62
|
-
run "#{ruby} bin/minitest_bisect -Ilib --seed
|
58
|
+
run "#{ruby} bin/minitest_bisect -Ilib --seed 1 example.rb"
|
63
59
|
end
|
64
60
|
|
65
61
|
task :many => :isolate do
|
@@ -71,10 +67,31 @@ task :many => :isolate do
|
|
71
67
|
ruby = "ruby -I.:lib"
|
72
68
|
|
73
69
|
banner "Original run that causes the test order dependency bug"
|
74
|
-
run "#{ruby}
|
70
|
+
run "#{ruby} ./example_many.rb --seed 2"
|
75
71
|
|
76
72
|
banner "Reduce the problem down to the minimal reproduction"
|
77
|
-
run "#{ruby} bin/minitest_bisect -Ilib --seed
|
73
|
+
run "#{ruby} bin/minitest_bisect -Ilib --seed 2 example_many.rb"
|
74
|
+
end
|
75
|
+
|
76
|
+
task :inverse => :isolate do
|
77
|
+
unless ENV.key? "SLEEP" then
|
78
|
+
warn "NOTE: Defaulting to sleeping 0.01 seconds per test."
|
79
|
+
warn "NOTE: Use SLEEP=0 to disable or any other value to simulate your tests."
|
80
|
+
end
|
81
|
+
|
82
|
+
ruby = "ruby -I.:lib"
|
83
|
+
|
84
|
+
banner "Original run that passes (seed 1)"
|
85
|
+
run "#{ruby} example_inverse.rb --seed 1"
|
86
|
+
|
87
|
+
banner "Original run that *looks like* a test order dependency bug (seed 3)"
|
88
|
+
run "#{ruby} example_inverse.rb --seed 3"
|
89
|
+
|
90
|
+
banner "BAD bisection (tests fail by themselves) (seed 3)"
|
91
|
+
run "#{ruby} bin/minitest_bisect -Ilib --seed 3 example_inverse.rb"
|
92
|
+
|
93
|
+
banner "Reduce the passing run down to the minimal reproduction (seed 1)"
|
94
|
+
run "#{ruby} bin/minitest_bisect -Ilib --seed 1 example_inverse.rb -n=/TestBad4#test_bad4_4$/"
|
78
95
|
end
|
79
96
|
|
80
97
|
# vim: syntax=ruby
|
data/bin/minitest_bisect
CHANGED
data/example.rb
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
#!/usr/bin/env ruby -w
|
2
|
+
|
3
|
+
require_relative "example_helper"
|
4
|
+
|
5
|
+
# 800 tests, test test_bad4_4 fails if test_bad1_1 runs before it
|
6
|
+
TestBad1 = create_test 1, 100, 1 => :infect
|
7
|
+
TestBad2 = create_test 2, 100
|
8
|
+
TestBad3 = create_test 3, 100
|
9
|
+
TestBad4 = create_test 4, 100, 4 => :flunk
|
10
|
+
TestBad5 = create_test 5, 100
|
11
|
+
TestBad6 = create_test 6, 100
|
12
|
+
TestBad7 = create_test 7, 100
|
13
|
+
TestBad8 = create_test 8, 100
|
14
|
+
|
15
|
+
# seed 1 == one fail
|
16
|
+
# seed 3 == all pass
|
17
|
+
|
18
|
+
# % SLEEP=0 ruby ./example.rb --seed 1
|
19
|
+
#
|
20
|
+
# and it fails, as expected. So we run it through minitest_bisect and see:
|
21
|
+
#
|
22
|
+
# reproducing... in 0.14 sec
|
23
|
+
# verifying... in 0.06 sec
|
24
|
+
# # of culprit methods: 128 in 0.08 sec
|
25
|
+
# # of culprit methods: 64 in 0.07 sec
|
26
|
+
# # of culprit methods: 64 in 0.07 sec
|
27
|
+
# # of culprit methods: 32 in 0.06 sec
|
28
|
+
# # of culprit methods: 16 in 0.06 sec
|
29
|
+
# # of culprit methods: 8 in 0.06 sec
|
30
|
+
# # of culprit methods: 4 in 0.06 sec
|
31
|
+
# # of culprit methods: 4 in 0.06 sec
|
32
|
+
# # of culprit methods: 2 in 0.06 sec
|
33
|
+
# # of culprit methods: 1 in 0.06 sec
|
34
|
+
#
|
35
|
+
# Minimal methods found in 10 steps:
|
36
|
+
#
|
37
|
+
# Culprit methods: ["TestBad1#test_bad1_1", "TestBad4#test_bad4_4"]
|
38
|
+
#
|
39
|
+
# /Users/ryan/.rubies/ruby-3.2.2/bin/ruby -Itest:lib -e 'require "././example.rb"' -- --seed 1 -n "/^(?:TestBad1#(?:test_bad1_1)|TestBad4#(?:test_bad4_4))$/"
|
40
|
+
#
|
41
|
+
# Final reproduction:
|
42
|
+
#
|
43
|
+
# Run options: --seed 1 -n "/^(?:TestBad1#(?:test_bad1_1)|TestBad4#(?:test_bad4_4))$/"
|
44
|
+
#
|
45
|
+
# # Running:
|
46
|
+
#
|
47
|
+
# .F
|
48
|
+
#
|
49
|
+
# Finished in 0.001349s, 1482.5797 runs/s, 741.2898 assertions/s.
|
50
|
+
#
|
51
|
+
# 1) Failure:
|
52
|
+
# TestBad4#test_bad4_4 [/Users/ryan/Work/p4/zss/src/minitest-bisect/dev/example.rb:20]:
|
53
|
+
# muahahaha order dependency bug!
|
54
|
+
#
|
55
|
+
# 2 runs, 1 assertions, 1 failures, 0 errors, 0 skips
|
56
|
+
#
|
57
|
+
# and that's all there is to it! You now know that test_bad4_4 fails
|
58
|
+
# only when paired with test_bad1_1 before it. You can now debug the
|
59
|
+
# two methods and see what they're both modifying/dependent on.
|
@@ -1,4 +1,7 @@
|
|
1
|
-
|
1
|
+
require "minitest/autorun"
|
2
|
+
|
3
|
+
$good = true
|
4
|
+
$bomb = 0
|
2
5
|
|
3
6
|
def create_test suffix, n_methods, bad_methods = {}
|
4
7
|
raise ArgumentError, "Bad args" if Hash === n_methods
|
@@ -12,10 +15,16 @@ def create_test suffix, n_methods, bad_methods = {}
|
|
12
15
|
sleep delay if delay > 0
|
13
16
|
|
14
17
|
case bad_methods[n]
|
15
|
-
when
|
16
|
-
|
18
|
+
when :flunk then
|
19
|
+
flunk "muahahaha order dependency bug!" unless $good
|
20
|
+
when :infect then
|
21
|
+
$good = false
|
22
|
+
when :fix then
|
23
|
+
$good = true
|
24
|
+
when :tick then
|
25
|
+
$bomb += 1
|
17
26
|
when Integer then
|
18
|
-
flunk "muahahaha order dependency bug!" if $
|
27
|
+
flunk "muahahaha order dependency bug!" if $bomb >= bad_methods[n]
|
19
28
|
else
|
20
29
|
assert true
|
21
30
|
end
|
data/example_inverse.rb
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
#!/usr/bin/env ruby -w
|
2
|
+
|
3
|
+
require_relative "example_helper"
|
4
|
+
|
5
|
+
$good = false
|
6
|
+
|
7
|
+
# 800 tests, test test_bad4_4 passes if test_bad1_1 runs before it
|
8
|
+
TestBad1 = create_test 1, 100, 1 => :fix
|
9
|
+
TestBad2 = create_test 2, 100
|
10
|
+
TestBad3 = create_test 3, 100
|
11
|
+
TestBad4 = create_test 4, 100, 4 => :flunk
|
12
|
+
TestBad5 = create_test 5, 100
|
13
|
+
TestBad6 = create_test 6, 100
|
14
|
+
TestBad7 = create_test 7, 100
|
15
|
+
TestBad8 = create_test 8, 100
|
16
|
+
|
17
|
+
# seed 1 == all pass
|
18
|
+
# seed 3 == one fail
|
19
|
+
|
20
|
+
# UNLIKE the scenario spelled out in example.rb...
|
21
|
+
#
|
22
|
+
# % SLEEP=0 ruby ./example_inverse.rb --seed 1
|
23
|
+
#
|
24
|
+
# passes, but
|
25
|
+
#
|
26
|
+
# % SLEEP=0 ruby ./example_inverse.rb --seed 3
|
27
|
+
#
|
28
|
+
# has 1 failure! So we run that through minitest_bisect:
|
29
|
+
#
|
30
|
+
# % SLEEP=0 ruby -Ilib bin/minitest_bisect ./example_inverse.rb --seed 3
|
31
|
+
#
|
32
|
+
# and see:
|
33
|
+
#
|
34
|
+
# Tests fail by themselves. This may not be an ordering issue.
|
35
|
+
#
|
36
|
+
# followed by a result that doesn't actually lead to the failure.
|
37
|
+
#
|
38
|
+
# Doing a normal run of minitest_bisect in this scenario doesn't
|
39
|
+
# detect anything actually relevant. This is because we're not dealing
|
40
|
+
# with a false *negative*, we're dealing with a false *positive*. The
|
41
|
+
# question is "what makes this test pass?". So, we run it again with
|
42
|
+
# the passing seed but this time point it at the failing test by name:
|
43
|
+
#
|
44
|
+
# % SLEEP=0 ruby -Ilib bin/minitest_bisect ./example_inverse.rb --seed 1 -n=/TestBad4#test_bad4_4$/
|
45
|
+
#
|
46
|
+
# and it outputs:
|
47
|
+
#
|
48
|
+
# Culprit methods: ["TestBad1#test_bad1_1", "TestBad4#test_bad4_4"]
|
49
|
+
|
50
|
+
# and shows a minimized run with the 2 passing tests. You now know
|
51
|
+
# that test_bad4_4 passes only when paired with test_bad1_1 before it.
|
52
|
+
# You can now debug the two methods and see what they're both
|
53
|
+
# modifying/dependent on.
|
data/example_many.rb
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
#!/usr/bin/env ruby -w
|
2
|
+
|
3
|
+
require_relative "example_helper"
|
4
|
+
|
5
|
+
TestBad1 = create_test 1, 100, 1 => :tick
|
6
|
+
TestBad2 = create_test 2, 100
|
7
|
+
TestBad3 = create_test 3, 100, 72 => :tick
|
8
|
+
TestBad4 = create_test 4, 100
|
9
|
+
TestBad5 = create_test 5, 100, 17 => :tick
|
10
|
+
TestBad6 = create_test 6, 100
|
11
|
+
TestBad7 = create_test 7, 100
|
12
|
+
TestBad8 = create_test 8, 100, 43 => 3
|
13
|
+
|
14
|
+
# seed 1 == all pass
|
15
|
+
# seed 2 == one fail
|
16
|
+
|
17
|
+
# % ruby example_many.rb --seed 2
|
18
|
+
|
19
|
+
# and it fails, as expected. So we run it through minitest_bisect and see:
|
20
|
+
|
21
|
+
# % ruby -Ilib bin/minitest_bisect example_many.rb --seed 2
|
22
|
+
|
23
|
+
# reproducing... in 0.15 sec
|
24
|
+
# verifying... in 0.06 sec
|
25
|
+
# # of culprit methods: 256 in 0.09 sec
|
26
|
+
# ... 82 more bisections ...
|
27
|
+
# # of culprit methods: 3 in 0.06 sec
|
28
|
+
#
|
29
|
+
# Minimal methods found in 84 steps:
|
30
|
+
#
|
31
|
+
# Culprit methods: ["TestBad3#test_bad3_72", "TestBad5#test_bad5_17", "TestBad1#test_bad1_1", "TestBad8#test_bad8_43"]
|
32
|
+
#
|
33
|
+
# /Users/ryan/.rubies/ruby-3.2.2/bin/ruby -Itest:lib -e 'require "./example_many.rb"' -- --seed 2 -n "/^(?:TestBad3#(?:test_bad3_72)|TestBad5#(?:test_bad5_17)|TestBad1#(?:test_bad1_1)|TestBad8#(?:test_bad8_43))$/"
|
34
|
+
#
|
35
|
+
# Final reproduction:
|
36
|
+
#
|
37
|
+
# Run options: --seed 2 -n "/^(?:TestBad3#(?:test_bad3_72)|TestBad5#(?:test_bad5_17)|TestBad1#(?:test_bad1_1)|TestBad8#(?:test_bad8_43))$/"
|
38
|
+
#
|
39
|
+
# # Running:
|
40
|
+
#
|
41
|
+
# ...F
|
42
|
+
#
|
43
|
+
# Finished in 0.001404s, 2849.0028 runs/s, 712.2507 assertions/s.
|
44
|
+
#
|
45
|
+
# 1) Failure:
|
46
|
+
# TestBad8#test_bad8_43 [/Users/ryan/Work/p4/zss/src/minitest-bisect/dev/example_many.rb:20]:
|
47
|
+
# muahahaha order dependency bug!
|
48
|
+
#
|
49
|
+
# 4 runs, 1 assertions, 1 failures, 0 errors, 0 skips
|
50
|
+
#
|
51
|
+
# and that's all there is to it! You now know that the failing test
|
52
|
+
# fails only when paired with the other 3 tests before it. You can now
|
53
|
+
# debug the 4 methods and see what they're all modifying/dependent on.
|
data/lib/minitest/bisect.rb
CHANGED
@@ -4,10 +4,15 @@ require "shellwords"
|
|
4
4
|
require "rbconfig"
|
5
5
|
require "path_expander"
|
6
6
|
|
7
|
+
module Minitest; end # :nodoc:
|
8
|
+
|
9
|
+
##
|
10
|
+
# Minitest::Bisect helps you isolate and debug random test failures.
|
11
|
+
|
7
12
|
class Minitest::Bisect
|
8
|
-
VERSION = "1.
|
13
|
+
VERSION = "1.7.0" # :nodoc:
|
9
14
|
|
10
|
-
class PathExpander < ::PathExpander
|
15
|
+
class PathExpander < ::PathExpander # :nodoc:
|
11
16
|
TEST_GLOB = "**/{test_*,*_test,spec_*,*_spec}.rb" # :nodoc:
|
12
17
|
|
13
18
|
attr_accessor :rb_flags
|
@@ -39,7 +44,7 @@ class Minitest::Bisect
|
|
39
44
|
end
|
40
45
|
|
41
46
|
mtbv = ENV["MTB_VERBOSE"].to_i
|
42
|
-
SHH = case
|
47
|
+
SHH = case # :nodoc:
|
43
48
|
when mtbv == 1 then " > /dev/null"
|
44
49
|
when mtbv >= 2 then nil
|
45
50
|
else " > /dev/null 2>&1"
|
@@ -51,21 +56,48 @@ class Minitest::Bisect
|
|
51
56
|
RbConfig::CONFIG['ruby_install_name'] +
|
52
57
|
RbConfig::CONFIG['EXEEXT']).sub(/.*\s.*/m, '"\&"')
|
53
58
|
|
54
|
-
|
59
|
+
##
|
60
|
+
# True if this run has seen a failure.
|
61
|
+
|
62
|
+
attr_accessor :tainted
|
55
63
|
alias :tainted? :tainted
|
56
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
|
+
|
57
82
|
def self.run files
|
58
83
|
new.run files
|
59
84
|
rescue => e
|
60
85
|
warn e.message
|
86
|
+
warn "Try running with MTB_VERBOSE=2 to verify."
|
61
87
|
exit 1
|
62
88
|
end
|
63
89
|
|
90
|
+
##
|
91
|
+
# Instantiate a new Bisect.
|
92
|
+
|
64
93
|
def initialize
|
65
94
|
self.culprits = []
|
66
95
|
self.failures = Hash.new { |h, k| h[k] = Hash.new { |h2, k2| h2[k2] = [] } }
|
67
96
|
end
|
68
97
|
|
98
|
+
##
|
99
|
+
# Reset per-bisect-run variables.
|
100
|
+
|
69
101
|
def reset
|
70
102
|
self.seen_bad = false
|
71
103
|
self.tainted = false
|
@@ -73,6 +105,10 @@ class Minitest::Bisect
|
|
73
105
|
# not clearing culprits on purpose
|
74
106
|
end
|
75
107
|
|
108
|
+
##
|
109
|
+
# Instance-level runner. Handles Minitest::Server, argument
|
110
|
+
# processing, and invoking +bisect_methods+.
|
111
|
+
|
76
112
|
def run args
|
77
113
|
Minitest::Server.run self
|
78
114
|
|
@@ -83,9 +119,9 @@ class Minitest::Bisect
|
|
83
119
|
|
84
120
|
files = expander.process
|
85
121
|
rb_flags = expander.rb_flags
|
86
|
-
mt_flags += ["--server",
|
122
|
+
mt_flags += ["--server", $$.to_s]
|
87
123
|
|
88
|
-
cmd = bisect_methods
|
124
|
+
cmd = bisect_methods files, rb_flags, mt_flags
|
89
125
|
|
90
126
|
puts "Final reproduction:"
|
91
127
|
puts
|
@@ -95,40 +131,87 @@ class Minitest::Bisect
|
|
95
131
|
Minitest::Server.stop
|
96
132
|
end
|
97
133
|
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
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
|
104
172
|
end
|
105
173
|
|
106
|
-
|
174
|
+
cmd = build_files_cmd(files, rb_flags, mt_flags)
|
107
175
|
|
108
|
-
|
109
|
-
|
176
|
+
msg = normal ? "reproducing..." : "reproducing false positive..."
|
177
|
+
time_it msg, build_methods_cmd(cmd)
|
110
178
|
|
111
|
-
|
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
|
112
189
|
|
113
|
-
|
190
|
+
time_it "verifying...", build_methods_cmd(cmd, [], bad)
|
114
191
|
|
115
|
-
|
116
|
-
|
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
|
117
197
|
end
|
118
198
|
|
199
|
+
idx = culprits.index bad.first
|
200
|
+
self.culprits = culprits.take idx+1 if idx # cull tests after bad
|
201
|
+
|
119
202
|
# culprits populated by initial reproduction via minitest/server
|
120
203
|
found, count = culprits.find_minimal_combination_and_count do |test|
|
121
204
|
prompt = "# of culprit methods: #{test.size}"
|
122
205
|
|
123
206
|
time_it prompt, build_methods_cmd(cmd, test, bad)
|
124
207
|
|
125
|
-
|
208
|
+
normal == tainted? # either normal and failed, or inverse and passed
|
126
209
|
end
|
127
210
|
|
128
211
|
puts
|
129
212
|
puts "Minimal methods found in #{count} steps:"
|
130
213
|
puts
|
131
|
-
puts "Culprit methods: %p" % [found]
|
214
|
+
puts "Culprit methods: %p" % [found + bad]
|
132
215
|
puts
|
133
216
|
cmd = build_methods_cmd cmd, found, bad
|
134
217
|
puts cmd.sub(/--server \d+/, "")
|
@@ -136,14 +219,14 @@ class Minitest::Bisect
|
|
136
219
|
cmd
|
137
220
|
end
|
138
221
|
|
139
|
-
def time_it prompt, cmd
|
222
|
+
def time_it prompt, cmd # :nodoc:
|
140
223
|
print prompt
|
141
224
|
t0 = Time.now
|
142
225
|
system "#{cmd} #{SHH}"
|
143
226
|
puts " in %.2f sec" % (Time.now - t0)
|
144
227
|
end
|
145
228
|
|
146
|
-
def map_failures
|
229
|
+
def map_failures # :nodoc:
|
147
230
|
# from: {"file.rb"=>{"Class"=>["test_method1", "test_method2"]}}
|
148
231
|
# to: ["Class#test_method1", "Class#test_method2"]
|
149
232
|
failures.values.map { |h|
|
@@ -151,15 +234,31 @@ class Minitest::Bisect
|
|
151
234
|
}.flatten.sort
|
152
235
|
end
|
153
236
|
|
154
|
-
def build_files_cmd culprits, rb, mt
|
155
|
-
reset
|
156
|
-
|
237
|
+
def build_files_cmd culprits, rb, mt # :nodoc:
|
157
238
|
tests = culprits.flatten.compact.map { |f| %(require "./#{f}") }.join " ; "
|
158
239
|
|
159
240
|
%(#{RUBY} #{rb.shelljoin} -e '#{tests}' -- #{mt.map(&:to_s).shelljoin})
|
160
241
|
end
|
161
242
|
|
162
|
-
def
|
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:
|
163
262
|
re = []
|
164
263
|
|
165
264
|
# bad by class, you perv
|
@@ -179,36 +278,18 @@ class Minitest::Bisect
|
|
179
278
|
"/^(?:#{re})$/"
|
180
279
|
end
|
181
280
|
|
182
|
-
def re_escape str
|
281
|
+
def re_escape str # :nodoc:
|
183
282
|
str.gsub(/([`'"!?&\[\]\(\)\{\}\|\+])/, '\\\\\1')
|
184
283
|
end
|
185
284
|
|
186
|
-
def build_methods_cmd cmd, culprits = [], bad = nil
|
187
|
-
reset
|
188
|
-
|
189
|
-
if bad then
|
190
|
-
re = build_re culprits + bad
|
191
|
-
|
192
|
-
cmd += " -n \"#{re}\"" if bad
|
193
|
-
end
|
194
|
-
|
195
|
-
if ENV["MTB_VERBOSE"].to_i >= 1 then
|
196
|
-
puts
|
197
|
-
puts cmd
|
198
|
-
puts
|
199
|
-
end
|
200
|
-
|
201
|
-
cmd
|
202
|
-
end
|
203
|
-
|
204
285
|
############################################################
|
205
286
|
# Server Methods:
|
206
287
|
|
207
|
-
def minitest_start
|
288
|
+
def minitest_start # :nodoc:
|
208
289
|
self.failures.clear
|
209
290
|
end
|
210
291
|
|
211
|
-
def minitest_result file, klass, method, fails, assertions, time
|
292
|
+
def minitest_result file, klass, method, fails, assertions, time # :nodoc:
|
212
293
|
fails.reject! { |fail| Minitest::Skip === fail }
|
213
294
|
|
214
295
|
if fails.empty? then
|
@@ -1,10 +1,13 @@
|
|
1
1
|
#!/usr/bin/ruby -w
|
2
2
|
|
3
|
-
|
3
|
+
##
|
4
|
+
# Finds the minimal combination of a collection of items that satisfy
|
5
|
+
# +test+.
|
4
6
|
|
5
7
|
class ComboFinder
|
6
8
|
##
|
7
|
-
# Find the minimal combination of a collection of items that satisfy
|
9
|
+
# Find the minimal combination of a collection of items that satisfy
|
10
|
+
# +test+.
|
8
11
|
#
|
9
12
|
# If you think of the collection as a binary tree, this algorithm
|
10
13
|
# does a breadth first search of the combinations that satisfy
|
@@ -79,11 +82,11 @@ class ComboFinder
|
|
79
82
|
ary
|
80
83
|
end
|
81
84
|
|
82
|
-
def d s = ""
|
85
|
+
def d s = "" # :nodoc:
|
83
86
|
warn s if ENV["MTB_DEBUG"]
|
84
87
|
end
|
85
88
|
|
86
|
-
def cache_result result, data, cache
|
89
|
+
def cache_result result, data, cache # :nodoc:
|
87
90
|
cache[data] = true
|
88
91
|
|
89
92
|
return result if result
|
@@ -103,7 +106,7 @@ class ComboFinder
|
|
103
106
|
end
|
104
107
|
end
|
105
108
|
|
106
|
-
class Array
|
109
|
+
class Array # :nodoc:
|
107
110
|
##
|
108
111
|
# Find the minimal combination of a collection of items that satisfy +test+.
|
109
112
|
|
data.tar.gz.sig
CHANGED
Binary file
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: minitest-bisect
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.7.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ryan Davis
|
@@ -10,9 +10,9 @@ bindir: bin
|
|
10
10
|
cert_chain:
|
11
11
|
- |
|
12
12
|
-----BEGIN CERTIFICATE-----
|
13
|
-
|
13
|
+
MIIDPjCCAiagAwIBAgIBBzANBgkqhkiG9w0BAQsFADBFMRMwEQYDVQQDDApyeWFu
|
14
14
|
ZC1ydWJ5MRkwFwYKCZImiZPyLGQBGRYJemVuc3BpZGVyMRMwEQYKCZImiZPyLGQB
|
15
|
-
|
15
|
+
GRYDY29tMB4XDTIzMDEwMTA3NTExN1oXDTI0MDEwMTA3NTExN1owRTETMBEGA1UE
|
16
16
|
AwwKcnlhbmQtcnVieTEZMBcGCgmSJomT8ixkARkWCXplbnNwaWRlcjETMBEGCgmS
|
17
17
|
JomT8ixkARkWA2NvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALda
|
18
18
|
b9DCgK+627gPJkB6XfjZ1itoOQvpqH1EXScSaba9/S2VF22VYQbXU1xQXL/WzCkx
|
@@ -22,14 +22,14 @@ cert_chain:
|
|
22
22
|
qhtV7HJxNKuPj/JFH0D2cswvzznE/a5FOYO68g+YCuFi5L8wZuuM8zzdwjrWHqSV
|
23
23
|
gBEfoTEGr7Zii72cx+sCAwEAAaM5MDcwCQYDVR0TBAIwADALBgNVHQ8EBAMCBLAw
|
24
24
|
HQYDVR0OBBYEFEfFe9md/r/tj/Wmwpy+MI8d9k/hMA0GCSqGSIb3DQEBCwUAA4IB
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
25
|
+
AQAkg3y+PBnBAPWdxxITm5sPHqdWQgSyCpRA20o4LTuWr8BWhSXBkfQNa7cY6fOn
|
26
|
+
xyM34VPzBFbExv6XOGDfOMFBVaYTHuN9peC/5/umL7kLl+nflXzL2QA7K6LYj5Bg
|
27
|
+
sM574Onr0dZDM6Vn69bzQ7rBIFDfK/OhlPzqKZad4nsdcsVH8ODCiT+ATMIZyz5K
|
28
|
+
WCnNtqlyiWXI8tdTpahDgcUwfcN/oN7v4K8iU5IbLJX6HQ5DKgmKjfb6XyMth16k
|
29
|
+
ROfWo9Uyp8ba/j9eVG14KkYRaLydAY1MNQk2yd3R5CGfeOpD1kttxjoypoUJ2dOG
|
30
|
+
nsNBRuQJ1UfiCG97a6DNm+Fr
|
31
31
|
-----END CERTIFICATE-----
|
32
|
-
date:
|
32
|
+
date: 2023-07-06 00:00:00.000000000 Z
|
33
33
|
dependencies:
|
34
34
|
- !ruby/object:Gem::Dependency
|
35
35
|
name: minitest-server
|
@@ -99,14 +99,14 @@ dependencies:
|
|
99
99
|
requirements:
|
100
100
|
- - "~>"
|
101
101
|
- !ruby/object:Gem::Version
|
102
|
-
version: '
|
102
|
+
version: '4.0'
|
103
103
|
type: :development
|
104
104
|
prerelease: false
|
105
105
|
version_requirements: !ruby/object:Gem::Requirement
|
106
106
|
requirements:
|
107
107
|
- - "~>"
|
108
108
|
- !ruby/object:Gem::Version
|
109
|
-
version: '
|
109
|
+
version: '4.0'
|
110
110
|
description: |-
|
111
111
|
Hunting down random test failures can be very very difficult,
|
112
112
|
sometimes impossible, but minitest-bisect makes it easy.
|
@@ -135,24 +135,10 @@ files:
|
|
135
135
|
- README.rdoc
|
136
136
|
- Rakefile
|
137
137
|
- bin/minitest_bisect
|
138
|
-
- example
|
139
|
-
-
|
140
|
-
-
|
141
|
-
-
|
142
|
-
- example-many/test_bad4.rb
|
143
|
-
- example-many/test_bad5.rb
|
144
|
-
- example-many/test_bad6.rb
|
145
|
-
- example-many/test_bad7.rb
|
146
|
-
- example-many/test_bad8.rb
|
147
|
-
- example/helper.rb
|
148
|
-
- example/test_bad1.rb
|
149
|
-
- example/test_bad2.rb
|
150
|
-
- example/test_bad3.rb
|
151
|
-
- example/test_bad4.rb
|
152
|
-
- example/test_bad5.rb
|
153
|
-
- example/test_bad6.rb
|
154
|
-
- example/test_bad7.rb
|
155
|
-
- example/test_bad8.rb
|
138
|
+
- example.rb
|
139
|
+
- example_helper.rb
|
140
|
+
- example_inverse.rb
|
141
|
+
- example_many.rb
|
156
142
|
- lib/minitest/bisect.rb
|
157
143
|
- lib/minitest/find_minimal_combination.rb
|
158
144
|
- test/minitest/test_bisect.rb
|
@@ -179,7 +165,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
179
165
|
- !ruby/object:Gem::Version
|
180
166
|
version: '0'
|
181
167
|
requirements: []
|
182
|
-
rubygems_version: 3.
|
168
|
+
rubygems_version: 3.4.10
|
183
169
|
signing_key:
|
184
170
|
specification_version: 4
|
185
171
|
summary: Hunting down random test failures can be very very difficult, sometimes impossible,
|
metadata.gz.sig
CHANGED
@@ -1,4 +1,3 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
L�
|
4
|
-
�ݸ:1��
|
1
|
+
�-���J�y��R�sT�G���t�Q�l�%s���ꢅ�� ���9�����L)T������E���.����1��+�]�Ήۙ�L\
|
2
|
+
oG1
|
3
|
+
�^3�P�ˢ|�LԒ�Z-�|&u�ݽ�=�aHK�e�1,�z}�\Y�;�y �5�~X�<˳� <sAX{�S�|�IȪ X���S*��&\���z�Z�lJ^<>߬4��%�.�F{dI(3���P��u��S2��S;�{���L!4O
|
data/example/helper.rb
DELETED
@@ -1,25 +0,0 @@
|
|
1
|
-
$hosed ||= false
|
2
|
-
|
3
|
-
def create_test suffix, n_methods, bad_methods = {}
|
4
|
-
raise ArgumentError, "Bad args" if Hash === n_methods
|
5
|
-
|
6
|
-
delay = (ENV["SLEEP"] || 0.01).to_f
|
7
|
-
|
8
|
-
Class.new(Minitest::Test) do
|
9
|
-
n_methods.times do |n|
|
10
|
-
n += 1
|
11
|
-
define_method "test_bad#{suffix}_#{n}" do
|
12
|
-
sleep delay if delay > 0
|
13
|
-
|
14
|
-
case bad_methods[n]
|
15
|
-
when true then
|
16
|
-
flunk "muahahaha order dependency bug!" if $hosed
|
17
|
-
when false then
|
18
|
-
$hosed = true
|
19
|
-
else
|
20
|
-
assert true
|
21
|
-
end
|
22
|
-
end
|
23
|
-
end
|
24
|
-
end
|
25
|
-
end
|
data/example/test_bad1.rb
DELETED
data/example/test_bad2.rb
DELETED
data/example/test_bad3.rb
DELETED
data/example/test_bad4.rb
DELETED
data/example/test_bad5.rb
DELETED
data/example/test_bad6.rb
DELETED
data/example/test_bad7.rb
DELETED
data/example/test_bad8.rb
DELETED
data/example-many/test_bad1.rb
DELETED
data/example-many/test_bad2.rb
DELETED
data/example-many/test_bad3.rb
DELETED
data/example-many/test_bad4.rb
DELETED
data/example-many/test_bad5.rb
DELETED
data/example-many/test_bad6.rb
DELETED
data/example-many/test_bad7.rb
DELETED
data/example-many/test_bad8.rb
DELETED