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 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