minitest-bisect 1.6.0 → 1.7.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 +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