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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0adf4dd9767c961c0030a23e4e200245d74505bb0c5bd79dc0ff4201adeadc63
4
- data.tar.gz: 501678cbbf5567c3408f1e44721dcbb11bc917b8e2d77c7ca063652e5ffc133f
3
+ metadata.gz: 52bae214fa2f36196af3ec54139c2bec839de200e8dee1015f916b56a666a69b
4
+ data.tar.gz: 15a204ca83b0da8a8773e565f3bc40c439713edacef4a1d66435f2f9938edf6e
5
5
  SHA512:
6
- metadata.gz: 3c9f20afc3dcd8cb7e782caaa456e8f9dded619624226c9a7123290cc8cd47abe1ec32afdc1acd61dba41b1d1d1797389296e45f7abc234c27eca3206a8f5473
7
- data.tar.gz: 3db4728af1051d31167814e277af68ee39fc0451a3c8df5c45f417a64b7d5fac8e72a9f2b5a590fa816566f1397f91429c1f16d608cba811d361faad246e7f6b
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-many/helper.rb
8
- example-many/test_bad1.rb
9
- example-many/test_bad2.rb
10
- example-many/test_bad3.rb
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
- * minitest_bisect first runs your tests on a per-file basis to
23
- minimize the number of tests you need to sift through.
24
- * minitest_bisect next runs the minimized files and figures out your
25
- exact failure reproduction.
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 first minimize the number of files, then it 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} -e '#{req "example/test*"}' -- --seed 3911"
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 3911 example/test*.rb"
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} -e '#{req "example-many/test*"}' -- --seed 27083"
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 27083 example-many/test*.rb"
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
@@ -1,4 +1,4 @@
1
- #!/usr/bin/ruby -w
1
+ #!/usr/bin/env ruby -w
2
2
 
3
3
  require "minitest/bisect"
4
4
 
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
- $hosed ||= 0
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 true then
16
- $hosed += 1
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 $hosed >= bad_methods[n]
27
+ flunk "muahahaha order dependency bug!" if $bomb >= bad_methods[n]
19
28
  else
20
29
  assert true
21
30
  end
@@ -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.
@@ -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.6.0"
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
- attr_accessor :tainted, :failures, :culprits, :seen_bad
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 build_files_cmd(files, rb_flags, mt_flags)
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
- def bisect_methods cmd
99
- time_it "reproducing...", build_methods_cmd(cmd)
100
-
101
- unless tainted? then
102
- $stderr.puts "Reproduction run passed? Aborting."
103
- abort "Try running with MTB_VERBOSE=2 to verify."
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
- bad = map_failures
174
+ cmd = build_files_cmd(files, rb_flags, mt_flags)
107
175
 
108
- raise "Nothing to verify against because every test failed. Aborting." if
109
- culprits.empty? && seen_bad
176
+ msg = normal ? "reproducing..." : "reproducing false positive..."
177
+ time_it msg, build_methods_cmd(cmd)
110
178
 
111
- time_it "verifying...", build_methods_cmd(cmd, [], bad)
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
- new_bad = map_failures
190
+ time_it "verifying...", build_methods_cmd(cmd, [], bad)
114
191
 
115
- if bad == new_bad then
116
- warn "Tests fail by themselves. This may not be an ordering issue."
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
- self.tainted?
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 build_re bad
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
- module Minitest; end
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 +test+.
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.6.0
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
- MIIDPjCCAiagAwIBAgIBBjANBgkqhkiG9w0BAQsFADBFMRMwEQYDVQQDDApyeWFu
13
+ MIIDPjCCAiagAwIBAgIBBzANBgkqhkiG9w0BAQsFADBFMRMwEQYDVQQDDApyeWFu
14
14
  ZC1ydWJ5MRkwFwYKCZImiZPyLGQBGRYJemVuc3BpZGVyMRMwEQYKCZImiZPyLGQB
15
- GRYDY29tMB4XDTIxMTIyMzIzMTkwNFoXDTIyMTIyMzIzMTkwNFowRTETMBEGA1UE
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
- AQCKB5jfsuSnKb+t/Wrh3UpdkmX7TrEsjVmERC0pPqzQ5GQJgmEXDD7oMgaKXaAq
26
- x2m+KSZDrqk7c8uho5OX6YMqg4KdxehfSLqqTZGoeV78qwf/jpPQZKTf+W9gUSJh
27
- zsWpo4K50MP+QtdSbKXZwjAafpQ8hK0MnnZ/aeCsW9ov5vdXpYbf3dpg6ADXRGE7
28
- lQY2y1tJ5/chqu6h7dQmnm2ABUqx9O+JcN9hbCYoA5i/EeubUEtFIh2w3SpO6YfB
29
- JFmxn4h9YO/pVdB962BdBNNDia0kgIjI3ENnkLq0dKpYU3+F3KhEuTksLO0L6X/V
30
- YsuyUzsMz6GQA4khyaMgKNSD
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: 2022-05-23 00:00:00.000000000 Z
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: '3.23'
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: '3.23'
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-many/helper.rb
139
- - example-many/test_bad1.rb
140
- - example-many/test_bad2.rb
141
- - example-many/test_bad3.rb
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.3.12
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
- 8=o���PLv80p�
2
- ��dI�{%5���!ggr=@�<� \�Fe��M�_�: vs�Zk\,�
3
- L�V0��~���gщ�,��a/N�yR�,����>U]NL"VT������"c�?;�D�����],��H��™����rL쇊�9��`��_nI�:�v��'y4
4
- �ݸ :1��
1
+ �-���J�y��R�sTG���t�Ql�%s���ꢅ�� ���9�����L)T������E���.����1��+�]�Ήۙ�L\
2
+ oG1
3
+ �^3�P�ˢ|�LԒ�΍ Z-�|&u�ݽ�=�aHKe1,�z}�\Y�;�y � 5�~X�<˳� <sAX{S�|�IȪ X���S*��&\���zZ�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
@@ -1,4 +0,0 @@
1
- require "minitest/autorun"
2
- require_relative "helper"
3
-
4
- TestBad1 = create_test 1, 100, 1 => false
data/example/test_bad2.rb DELETED
@@ -1,4 +0,0 @@
1
- require "minitest/autorun"
2
- require_relative "helper"
3
-
4
- TestBad2 = create_test 2, 100
data/example/test_bad3.rb DELETED
@@ -1,4 +0,0 @@
1
- require "minitest/autorun"
2
- require_relative "helper"
3
-
4
- TestBad3 = create_test 3, 100
data/example/test_bad4.rb DELETED
@@ -1,4 +0,0 @@
1
- require "minitest/autorun"
2
- require_relative "helper"
3
-
4
- TestBad4 = create_test 4, 100, 4 => true
data/example/test_bad5.rb DELETED
@@ -1,4 +0,0 @@
1
- require "minitest/autorun"
2
- require_relative "helper"
3
-
4
- TestBad5 = create_test 5, 100
data/example/test_bad6.rb DELETED
@@ -1,4 +0,0 @@
1
- require "minitest/autorun"
2
- require_relative "helper"
3
-
4
- TestBad6 = create_test 6, 100
data/example/test_bad7.rb DELETED
@@ -1,4 +0,0 @@
1
- require "minitest/autorun"
2
- require_relative "helper"
3
-
4
- TestBad7 = create_test 7, 100
data/example/test_bad8.rb DELETED
@@ -1,4 +0,0 @@
1
- require "minitest/autorun"
2
- require_relative "helper"
3
-
4
- TestBad8 = create_test 8, 100
@@ -1,4 +0,0 @@
1
- require "minitest/autorun"
2
- require_relative "helper"
3
-
4
- TestBad1 = create_test 1, 100, 1 => true
@@ -1,4 +0,0 @@
1
- require "minitest/autorun"
2
- require_relative "helper"
3
-
4
- TestBad2 = create_test 2, 100
@@ -1,4 +0,0 @@
1
- require "minitest/autorun"
2
- require_relative "helper"
3
-
4
- TestBad3 = create_test 3, 100, 72 => true
@@ -1,4 +0,0 @@
1
- require "minitest/autorun"
2
- require_relative "helper"
3
-
4
- TestBad4 = create_test 4, 100
@@ -1,4 +0,0 @@
1
- require "minitest/autorun"
2
- require_relative "helper"
3
-
4
- TestBad5 = create_test 5, 100, 17 => true
@@ -1,4 +0,0 @@
1
- require "minitest/autorun"
2
- require_relative "helper"
3
-
4
- TestBad6 = create_test 6, 100
@@ -1,4 +0,0 @@
1
- require "minitest/autorun"
2
- require_relative "helper"
3
-
4
- TestBad7 = create_test 7, 100
@@ -1,4 +0,0 @@
1
- require "minitest/autorun"
2
- require_relative "helper"
3
-
4
- TestBad8 = create_test 8, 100, 43 => 3