minitest-bisect 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- checksums.yaml.gz.sig +0 -0
- data.tar.gz.sig +2 -1
- data/.autotest +26 -0
- data/.gemtest +0 -0
- data/History.rdoc +6 -0
- data/Manifest.txt +30 -0
- data/README.rdoc +166 -0
- data/Rakefile +72 -0
- data/bin/minitest_bisect +5 -0
- data/example-many/helper.rb +25 -0
- data/example-many/test_bad1.rb +4 -0
- data/example-many/test_bad2.rb +4 -0
- data/example-many/test_bad3.rb +4 -0
- data/example-many/test_bad4.rb +4 -0
- data/example-many/test_bad5.rb +4 -0
- data/example-many/test_bad6.rb +4 -0
- data/example-many/test_bad7.rb +4 -0
- data/example-many/test_bad8.rb +4 -0
- data/example/helper.rb +25 -0
- data/example/test_bad1.rb +4 -0
- data/example/test_bad2.rb +4 -0
- data/example/test_bad3.rb +4 -0
- data/example/test_bad4.rb +4 -0
- data/example/test_bad5.rb +4 -0
- data/example/test_bad6.rb +4 -0
- data/example/test_bad7.rb +4 -0
- data/example/test_bad8.rb +4 -0
- data/lib/minitest/bisect.rb +150 -0
- data/lib/minitest/find_minimal_combination.rb +106 -0
- data/lib/minitest/server.rb +45 -0
- data/lib/minitest/server_plugin.rb +44 -0
- data/test/minitest/test_bisect.rb +10 -0
- data/test/minitest/test_find_minimal_combination.rb +126 -0
- metadata +158 -0
- metadata.gz.sig +2 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 6a2f0e5308b623a3bd5ba94ef584fec183e7dd51
|
4
|
+
data.tar.gz: 62eb672d93534aee515afcbf939b01d01fff7ba3
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: ded844dfc9c7dffd7a598b6a2a5fc7a3dd2de11d7091856b508e303a3f1f366797df0dcbe9e1aaa1fa8eec0002f6d460a682878463624ca9b80656671bc0fdac
|
7
|
+
data.tar.gz: bffd131f915c60ed1e2185f957ffb6ac523008690b49c2a03786f9ba29c0c20a2720aad6c5090f2a8dd3d99a17ee8acd4b663f79718486dff902360d2f82d22a
|
checksums.yaml.gz.sig
ADDED
Binary file
|
data.tar.gz.sig
ADDED
data/.autotest
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
|
3
|
+
require "autotest/restart"
|
4
|
+
|
5
|
+
Autotest.add_hook :initialize do |at|
|
6
|
+
at.testlib = "minitest/autorun"
|
7
|
+
at.add_exception "tmp"
|
8
|
+
|
9
|
+
# at.extra_files << "../some/external/dependency.rb"
|
10
|
+
#
|
11
|
+
# at.libs << ":../some/external"
|
12
|
+
#
|
13
|
+
# at.add_exception "vendor"
|
14
|
+
#
|
15
|
+
# at.add_mapping(/dependency.rb/) do |f, _|
|
16
|
+
# at.files_matching(/test_.*rb$/)
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# %w(TestA TestB).each do |klass|
|
20
|
+
# at.extra_class_map[klass] = "test/test_misc.rb"
|
21
|
+
# end
|
22
|
+
end
|
23
|
+
|
24
|
+
# Autotest.add_hook :run_command do |at|
|
25
|
+
# system "rake build"
|
26
|
+
# end
|
data/.gemtest
ADDED
File without changes
|
data/History.rdoc
ADDED
data/Manifest.txt
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
.autotest
|
2
|
+
History.rdoc
|
3
|
+
Manifest.txt
|
4
|
+
README.rdoc
|
5
|
+
Rakefile
|
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
|
25
|
+
lib/minitest/bisect.rb
|
26
|
+
lib/minitest/find_minimal_combination.rb
|
27
|
+
lib/minitest/server.rb
|
28
|
+
lib/minitest/server_plugin.rb
|
29
|
+
test/minitest/test_bisect.rb
|
30
|
+
test/minitest/test_find_minimal_combination.rb
|
data/README.rdoc
ADDED
@@ -0,0 +1,166 @@
|
|
1
|
+
= minitest-bisect
|
2
|
+
|
3
|
+
home :: https://github.com/seattlerb/minitest-bisect
|
4
|
+
rdoc :: http://docs.seattlerb.org/minitest-bisect
|
5
|
+
|
6
|
+
== DESCRIPTION:
|
7
|
+
|
8
|
+
Hunting down random test failures can be very very difficult,
|
9
|
+
sometimes impossible, but minitest-bisect makes it easy.
|
10
|
+
|
11
|
+
minitest-bisect helps you isolate and debug random test failures.
|
12
|
+
|
13
|
+
If your tests only fail randomly, you can reproduce the error
|
14
|
+
consistently by using `--seed <num>`, but what then? How do you figure
|
15
|
+
out which combination of tests out of hundreds are responsible for the
|
16
|
+
failure? You know which test is failing, but what others are causing
|
17
|
+
it to fail or were helping it succeed in a different order? That's
|
18
|
+
what minitest-bisect does best.
|
19
|
+
|
20
|
+
== FEATURES/PROBLEMS:
|
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.
|
26
|
+
|
27
|
+
== SYNOPSIS:
|
28
|
+
|
29
|
+
Let's say you have a bunch of test files and they fail sometimes, but
|
30
|
+
not consistently. You get a run that fails, so you record the
|
31
|
+
randomization seed that minitest displays at the top of every run.
|
32
|
+
|
33
|
+
=== Original Failure:
|
34
|
+
|
35
|
+
Here's an example run that fails randomly:
|
36
|
+
|
37
|
+
$ rake
|
38
|
+
ruby -I.: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"'
|
39
|
+
Run options: --seed 3911
|
40
|
+
|
41
|
+
# Running:
|
42
|
+
|
43
|
+
..............................................................................
|
44
|
+
..............................................................................
|
45
|
+
..............................................................................
|
46
|
+
..............................................................................
|
47
|
+
.........................................F....................................
|
48
|
+
..............................................................................
|
49
|
+
..............................................................................
|
50
|
+
..............................................................................
|
51
|
+
..............................................................................
|
52
|
+
..............................................................................
|
53
|
+
....................
|
54
|
+
|
55
|
+
Finished in 200.836561s, 3.9833 runs/s, 3.9784 assertions/s.
|
56
|
+
|
57
|
+
1) Failure:
|
58
|
+
TestBad4#test_bad4_4 [example/helper.rb:16]:
|
59
|
+
muahahaha order dependency bug!
|
60
|
+
|
61
|
+
800 runs, 799 assertions, 1 failures, 0 errors, 0 skips
|
62
|
+
|
63
|
+
=== Minimization and Isolation:
|
64
|
+
|
65
|
+
The problem is about how to efficiently figure out the minimal
|
66
|
+
reproduction of the random failure so you can actually focus on the
|
67
|
+
problem and not all the noise.
|
68
|
+
|
69
|
+
So, you run the tests again, but this time with minitest_bisect.
|
70
|
+
Provide the seed given in the failure so the tests always run in the
|
71
|
+
same order and reproduce every time.
|
72
|
+
|
73
|
+
minitest_bisect with first minimize the number of files, then it will
|
74
|
+
turn around and minimize the number of methods.
|
75
|
+
|
76
|
+
$ minitest_bisect --seed 3911 example/test*.rb
|
77
|
+
reproducing...
|
78
|
+
reproduced
|
79
|
+
# of culprit files: 4
|
80
|
+
# of culprit files: 2
|
81
|
+
# of culprit files: 2
|
82
|
+
# of culprit files: 2
|
83
|
+
# of culprit files: 2
|
84
|
+
|
85
|
+
Minimal files found in 5 steps:
|
86
|
+
|
87
|
+
ruby -Ilib -e 'require "./example/test_bad1.rb" ; require "./example/test_bad4.rb"' -- --seed 3911 -s 48222
|
88
|
+
|
89
|
+
reproducing...
|
90
|
+
reproduced
|
91
|
+
# of culprit methods: 64
|
92
|
+
# of culprit methods: 64
|
93
|
+
# of culprit methods: 32
|
94
|
+
# of culprit methods: 16
|
95
|
+
# of culprit methods: 8
|
96
|
+
# of culprit methods: 8
|
97
|
+
# of culprit methods: 4
|
98
|
+
# of culprit methods: 2
|
99
|
+
# of culprit methods: 2
|
100
|
+
# of culprit methods: 1
|
101
|
+
|
102
|
+
Minimal methods found in 10 steps:
|
103
|
+
|
104
|
+
ruby -Ilib -e 'require "./example/test_bad1.rb" ; require "./example/test_bad4.rb"' -- --seed 3911 -s 48222 -n '/^(?:TestBad1\#test_bad1_1|TestBad4\#test_bad4_4)$/'
|
105
|
+
|
106
|
+
Final reproduction:
|
107
|
+
|
108
|
+
Run options: --seed 3911 -s 48222 -n "/^(?:TestBad1\\#test_bad1_1|TestBad4\\#test_bad4_4)$/"
|
109
|
+
|
110
|
+
# Running:
|
111
|
+
|
112
|
+
.F
|
113
|
+
|
114
|
+
Finished in 0.505776s, 3.9543 runs/s, 1.9772 assertions/s.
|
115
|
+
|
116
|
+
1) Failure:
|
117
|
+
TestBad4#test_bad4_4 [/Users/ryan/Work/p4/zss/src/minitest-bisect/dev/example/helper.rb:16]:
|
118
|
+
muahahaha order dependency bug!
|
119
|
+
|
120
|
+
2 runs, 1 assertions, 1 failures, 0 errors, 0 skips
|
121
|
+
|
122
|
+
Voila! This reduced it from 800 tests across 8 files down to 2 tests
|
123
|
+
across 2 files. Note how we went from a 200 second test run to a 0.5
|
124
|
+
second test run. Debugging that will be much easier.
|
125
|
+
|
126
|
+
It is now up to you to look at the source of both of those tests to
|
127
|
+
determine what side-effects (or lack thereof) are causing your test
|
128
|
+
failure when run in this specific order.
|
129
|
+
|
130
|
+
This happens in a single run. Depending on how many files / tests you
|
131
|
+
have and how long they take, the first phase might take a long time.
|
132
|
+
The second phase might also take a while, but each iteration should
|
133
|
+
reduce the number of tests so it should get quicker with each step.
|
134
|
+
|
135
|
+
== REQUIREMENTS:
|
136
|
+
|
137
|
+
* minitest 5
|
138
|
+
|
139
|
+
== INSTALL:
|
140
|
+
|
141
|
+
* sudo gem install minitest-bisect
|
142
|
+
|
143
|
+
== LICENSE:
|
144
|
+
|
145
|
+
(The MIT License)
|
146
|
+
|
147
|
+
Copyright (c) Ryan Davis, seattle.rb
|
148
|
+
|
149
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
150
|
+
a copy of this software and associated documentation files (the
|
151
|
+
'Software'), to deal in the Software without restriction, including
|
152
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
153
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
154
|
+
permit persons to whom the Software is furnished to do so, subject to
|
155
|
+
the following conditions:
|
156
|
+
|
157
|
+
The above copyright notice and this permission notice shall be
|
158
|
+
included in all copies or substantial portions of the Software.
|
159
|
+
|
160
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
161
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
162
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
163
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
164
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
165
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
166
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
|
3
|
+
require "rubygems"
|
4
|
+
require "hoe"
|
5
|
+
|
6
|
+
Hoe.plugin :isolate
|
7
|
+
Hoe.plugin :seattlerb
|
8
|
+
Hoe.plugin :rdoc
|
9
|
+
|
10
|
+
Hoe.spec "minitest-bisect" do
|
11
|
+
developer "Ryan Davis", "ryand-ruby@zenspider.com"
|
12
|
+
license "MIT"
|
13
|
+
end
|
14
|
+
|
15
|
+
require "rake/testtask"
|
16
|
+
|
17
|
+
Rake::TestTask.new(:badtest) do |t|
|
18
|
+
t.test_files = Dir["badtest/test*.rb"]
|
19
|
+
end
|
20
|
+
|
21
|
+
def banner text
|
22
|
+
puts
|
23
|
+
puts "#" * 70
|
24
|
+
puts "# #{text} ::"
|
25
|
+
puts "#" * 70
|
26
|
+
puts
|
27
|
+
unless ENV["SLEEP"] == "0" then
|
28
|
+
print "Press return to continue "
|
29
|
+
$stdin.gets
|
30
|
+
puts
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def run cmd
|
35
|
+
sh cmd do end
|
36
|
+
end
|
37
|
+
|
38
|
+
def req glob
|
39
|
+
Dir["#{glob}.rb"].map { |s| "require #{s.inspect}" }.join ";"
|
40
|
+
end
|
41
|
+
|
42
|
+
task :repro do
|
43
|
+
unless ENV.has_key? "SLEEP" then
|
44
|
+
warn "NOTE: Defaulting to sleeping 0.01 seconds per test."
|
45
|
+
warn "NOTE: Use SLEEP=0 to disable or any other value to simulate your tests."
|
46
|
+
end
|
47
|
+
|
48
|
+
ruby = "ruby -I.:lib"
|
49
|
+
|
50
|
+
banner "Original run that causes the test order dependency bug"
|
51
|
+
run "#{ruby} -e '#{req "example/test*"}' -- --seed 3911"
|
52
|
+
|
53
|
+
banner "Reduce the problem down to the minimal reproduction"
|
54
|
+
run "#{ruby} bin/minitest_bisect -Ilib --seed 3911 example/test*.rb"
|
55
|
+
end
|
56
|
+
|
57
|
+
task :many do
|
58
|
+
unless ENV.has_key? "SLEEP" then
|
59
|
+
warn "NOTE: Defaulting to sleeping 0.01 seconds per test."
|
60
|
+
warn "NOTE: Use SLEEP=0 to disable or any other value to simulate your tests."
|
61
|
+
end
|
62
|
+
|
63
|
+
ruby = "ruby -I.:lib"
|
64
|
+
|
65
|
+
banner "Original run that causes the test order dependency bug"
|
66
|
+
run "#{ruby} -e '#{req "example-many/test*"}' -- --seed 27083"
|
67
|
+
|
68
|
+
banner "Reduce the problem down to the minimal reproduction"
|
69
|
+
run "#{ruby} bin/minitest_bisect -Ilib --seed 27083 example-many/test*.rb"
|
70
|
+
end
|
71
|
+
|
72
|
+
# vim: syntax=ruby
|
data/bin/minitest_bisect
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
$hosed ||= 0
|
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 = 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
|
+
$hosed += 1
|
17
|
+
when Fixnum then
|
18
|
+
flunk "muahahaha order dependency bug!" if $hosed >= bad_methods[n]
|
19
|
+
else
|
20
|
+
assert true
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
data/example/helper.rb
ADDED
@@ -0,0 +1,25 @@
|
|
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 = 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
|
@@ -0,0 +1,150 @@
|
|
1
|
+
require "minitest/find_minimal_combination"
|
2
|
+
require "minitest/server"
|
3
|
+
require "shellwords"
|
4
|
+
|
5
|
+
class Minitest::Bisect
|
6
|
+
VERSION = "1.0.0"
|
7
|
+
SHH = " &> /dev/null"
|
8
|
+
|
9
|
+
attr_accessor :tainted, :failures, :culprits, :mode, :seen_bad
|
10
|
+
alias :tainted? :tainted
|
11
|
+
|
12
|
+
def self.run files
|
13
|
+
new.run files
|
14
|
+
end
|
15
|
+
|
16
|
+
def initialize
|
17
|
+
self.culprits = []
|
18
|
+
self.failures = Hash.new { |h,k| h[k] = Hash.new { |h2,k2| h2[k2] = [] } }
|
19
|
+
end
|
20
|
+
|
21
|
+
def reset
|
22
|
+
self.seen_bad = false
|
23
|
+
self.tainted = false
|
24
|
+
failures.clear
|
25
|
+
# not clearing culprits on purpose
|
26
|
+
end
|
27
|
+
|
28
|
+
def run files
|
29
|
+
Minitest::Server.run self
|
30
|
+
|
31
|
+
cmd = nil
|
32
|
+
|
33
|
+
if :until_I_have_negative_filtering_in_minitest then
|
34
|
+
files, flags = files.partition { |arg| File.file? arg }
|
35
|
+
rb_flags, mt_flags = flags.partition { |arg| arg =~ /^-I/ }
|
36
|
+
mt_flags += ["-s", $$]
|
37
|
+
|
38
|
+
cmd = bisect_methods build_files_cmd(files, rb_flags, mt_flags)
|
39
|
+
else
|
40
|
+
cmd = bisect_methods bisect_files files
|
41
|
+
end
|
42
|
+
|
43
|
+
puts "Final reproduction:"
|
44
|
+
puts
|
45
|
+
|
46
|
+
system cmd
|
47
|
+
ensure
|
48
|
+
Minitest::Server.stop
|
49
|
+
end
|
50
|
+
|
51
|
+
def bisect_files files
|
52
|
+
self.mode = :files
|
53
|
+
|
54
|
+
files, flags = files.partition { |arg| File.file? arg }
|
55
|
+
rb_flags, mt_flags = flags.partition { |arg| arg =~ /^-I/ }
|
56
|
+
mt_flags += ["-s", $$]
|
57
|
+
|
58
|
+
puts "reproducing..."
|
59
|
+
system "#{build_files_cmd files, rb_flags, mt_flags} #{SHH}"
|
60
|
+
abort "Reproduction run passed? Aborting." unless tainted?
|
61
|
+
puts "reproduced"
|
62
|
+
|
63
|
+
found, count = files.find_minimal_combination_and_count do |test|
|
64
|
+
puts "# of culprit files: #{test.size}"
|
65
|
+
|
66
|
+
system "#{build_files_cmd test, rb_flags, mt_flags} #{SHH}"
|
67
|
+
|
68
|
+
self.tainted?
|
69
|
+
end
|
70
|
+
|
71
|
+
puts
|
72
|
+
puts "Minimal files found in #{count} steps:"
|
73
|
+
puts
|
74
|
+
cmd = build_files_cmd found, rb_flags, mt_flags
|
75
|
+
puts cmd
|
76
|
+
cmd
|
77
|
+
end
|
78
|
+
|
79
|
+
def bisect_methods cmd
|
80
|
+
self.mode = :methods
|
81
|
+
|
82
|
+
puts "reproducing..."
|
83
|
+
system "#{build_methods_cmd cmd} #{SHH}"
|
84
|
+
abort "Reproduction run passed? Aborting." unless tainted?
|
85
|
+
puts "reproduced"
|
86
|
+
|
87
|
+
# from: {"file.rb"=>{"Class"=>["test_method"]}} to: "Class#test_method"
|
88
|
+
bad = failures.values.first.to_a.join "#"
|
89
|
+
|
90
|
+
found, count = culprits.find_minimal_combination_and_count do |test|
|
91
|
+
puts "# of culprit methods: #{test.size}"
|
92
|
+
|
93
|
+
system "#{build_methods_cmd cmd, test, bad} #{SHH}"
|
94
|
+
|
95
|
+
self.tainted?
|
96
|
+
end
|
97
|
+
|
98
|
+
puts
|
99
|
+
puts "Minimal methods found in #{count} steps:"
|
100
|
+
puts
|
101
|
+
cmd = build_methods_cmd cmd, found, bad
|
102
|
+
puts cmd
|
103
|
+
puts
|
104
|
+
cmd
|
105
|
+
end
|
106
|
+
|
107
|
+
def build_files_cmd culprits, rb, mt
|
108
|
+
reset
|
109
|
+
|
110
|
+
tests = culprits.flatten.compact.map {|f| %(require "./#{f}")}.join " ; "
|
111
|
+
|
112
|
+
%(ruby #{rb.shelljoin} -e '#{tests}' -- #{mt.shelljoin})
|
113
|
+
end
|
114
|
+
|
115
|
+
def build_methods_cmd cmd, culprits = [], bad = nil
|
116
|
+
reset
|
117
|
+
|
118
|
+
if bad then
|
119
|
+
re = []
|
120
|
+
|
121
|
+
bbc = (culprits + [bad]).map { |s| s.split(/#/) }.group_by(&:first)
|
122
|
+
bbc.each do |klass, methods|
|
123
|
+
methods = methods.map(&:last).flatten
|
124
|
+
|
125
|
+
re << /#{klass}##{Regexp.union(methods)}/
|
126
|
+
end
|
127
|
+
|
128
|
+
re = Regexp.union(re).to_s.gsub(/-mix/, "")
|
129
|
+
|
130
|
+
cmd += " -n '/^#{re}$/'" if bad
|
131
|
+
end
|
132
|
+
|
133
|
+
cmd
|
134
|
+
end
|
135
|
+
|
136
|
+
def result file, klass, method, fails, assertions, time
|
137
|
+
if mode == :methods then
|
138
|
+
if fails.empty? then
|
139
|
+
culprits << "#{klass}##{method}" unless seen_bad # UGH
|
140
|
+
else
|
141
|
+
self.seen_bad = true
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
unless fails.empty?
|
146
|
+
self.tainted = true
|
147
|
+
self.failures[file][klass] << method
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
#!/usr/bin/ruby -w
|
2
|
+
|
3
|
+
module Minitest; end
|
4
|
+
|
5
|
+
class ComboFinder
|
6
|
+
##
|
7
|
+
# Find the minimal combination of a collection of items that satisfy +test+.
|
8
|
+
#
|
9
|
+
# If you think of the collection as a binary tree, this algorithm
|
10
|
+
# does a breadth first search of the combinations that satisfy
|
11
|
+
# +test+.
|
12
|
+
#--
|
13
|
+
# level collection
|
14
|
+
#
|
15
|
+
# 0 A
|
16
|
+
# 1 B C
|
17
|
+
# 2 D E F G
|
18
|
+
# 3 1 2 3 4 5 6 7 8
|
19
|
+
#
|
20
|
+
# This assumes that A has already been tested and you're now trying
|
21
|
+
# to reduce the match. Starting at level 1, test B & C separately.
|
22
|
+
# If either test positive, reduce the search space accordingly. If
|
23
|
+
# not, step down to level 2 and search w/ finer granularity (ie, DF,
|
24
|
+
# DG, EF--DE and FG were already tested as B & C). Repeat until a
|
25
|
+
# minimal combination is found.
|
26
|
+
|
27
|
+
def find_minimal_combination ary
|
28
|
+
level, n_combos = 1, 1
|
29
|
+
seen = {}
|
30
|
+
|
31
|
+
loop do
|
32
|
+
size = 2 ** (Math.log(ary.size) / Math.log(2)).round
|
33
|
+
divs = 2 ** level
|
34
|
+
done = divs >= size
|
35
|
+
divs = size if done
|
36
|
+
|
37
|
+
subsections = ary.each_slice(size/divs).to_a.combination(n_combos)
|
38
|
+
|
39
|
+
found = subsections.find { |a|
|
40
|
+
b = a.flatten
|
41
|
+
|
42
|
+
next if seen[b]
|
43
|
+
|
44
|
+
cache_result yield(b), b, seen
|
45
|
+
}
|
46
|
+
|
47
|
+
if found then
|
48
|
+
ary = found.flatten
|
49
|
+
break if done
|
50
|
+
|
51
|
+
seen.delete ary
|
52
|
+
|
53
|
+
level = n_combos = 1
|
54
|
+
else
|
55
|
+
if done then
|
56
|
+
n_combos += 1
|
57
|
+
break if n_combos > size
|
58
|
+
else
|
59
|
+
level += 1
|
60
|
+
n_combos = level
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
ary
|
66
|
+
end
|
67
|
+
|
68
|
+
def cache_result result, data, cache
|
69
|
+
cache[data] = true
|
70
|
+
|
71
|
+
return result if result
|
72
|
+
|
73
|
+
unless result or data.size > 128 then
|
74
|
+
max = data.size
|
75
|
+
subdiv = 2
|
76
|
+
until subdiv >= max do
|
77
|
+
data.each_slice(max / subdiv) do |sub_data|
|
78
|
+
cache[sub_data] = true
|
79
|
+
end
|
80
|
+
subdiv *= 2
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
result
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
class Array
|
89
|
+
##
|
90
|
+
# Find the minimal combination of a collection of items that satisfy +test+.
|
91
|
+
|
92
|
+
def find_minimal_combination &test
|
93
|
+
ComboFinder.new.find_minimal_combination(self, &test)
|
94
|
+
end
|
95
|
+
|
96
|
+
def find_minimal_combination_and_count
|
97
|
+
count = 0
|
98
|
+
|
99
|
+
found = self.find_minimal_combination do |ary|
|
100
|
+
count += 1
|
101
|
+
yield ary
|
102
|
+
end
|
103
|
+
|
104
|
+
return found, count
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require "drb"
|
2
|
+
require "tmpdir"
|
3
|
+
require "minitest"
|
4
|
+
|
5
|
+
module Minitest
|
6
|
+
class Server
|
7
|
+
TOPDIR = Dir.pwd + "/"
|
8
|
+
|
9
|
+
def self.path pid = $$
|
10
|
+
"drbunix:#{Dir.tmpdir}/minitest.#{pid}"
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.run client
|
14
|
+
DRb.start_service path, new(client)
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.stop
|
18
|
+
DRb.stop_service
|
19
|
+
end
|
20
|
+
|
21
|
+
attr_accessor :client
|
22
|
+
|
23
|
+
def initialize client
|
24
|
+
self.client = client
|
25
|
+
end
|
26
|
+
|
27
|
+
def quit
|
28
|
+
self.class.stop
|
29
|
+
end
|
30
|
+
|
31
|
+
def start
|
32
|
+
client.failures.clear # TODO: push down to subclass
|
33
|
+
end
|
34
|
+
|
35
|
+
def result file, klass, method, fails, assertions, time
|
36
|
+
file = file.sub(/^#{TOPDIR}/, "")
|
37
|
+
|
38
|
+
client.result file, klass, method, fails, assertions, time
|
39
|
+
end
|
40
|
+
|
41
|
+
def report
|
42
|
+
# do nothing
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require "minitest"
|
2
|
+
|
3
|
+
module Minitest
|
4
|
+
@server = false
|
5
|
+
|
6
|
+
def self.plugin_server_options opts, options # :nodoc:
|
7
|
+
opts.on "-s", "--server=pid", Integer, "Connect to minitest server w/ pid." do |s|
|
8
|
+
@server = s
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.plugin_server_init options
|
13
|
+
if @server then
|
14
|
+
require "minitest/server"
|
15
|
+
self.reporter << Minitest::ServerReporter.new(@server)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
module Minitest
|
21
|
+
class ServerReporter < Minitest::AbstractReporter
|
22
|
+
def initialize pid
|
23
|
+
DRb.start_service
|
24
|
+
uri = Minitest::Server.path(pid)
|
25
|
+
@mt_server = DRbObject.new_with_uri uri
|
26
|
+
super()
|
27
|
+
end
|
28
|
+
|
29
|
+
def start
|
30
|
+
@mt_server.start
|
31
|
+
end
|
32
|
+
|
33
|
+
def record result
|
34
|
+
r = result
|
35
|
+
c = r.class
|
36
|
+
file, = c.instance_method(r.name).source_location
|
37
|
+
@mt_server.result file, c.name, r.name, r.failures, r.assertions, r.time
|
38
|
+
end
|
39
|
+
|
40
|
+
def report
|
41
|
+
@mt_server.report
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,126 @@
|
|
1
|
+
#!/usr/bin/ruby -w
|
2
|
+
|
3
|
+
$: << "." << "lib"
|
4
|
+
|
5
|
+
gem "minitest"
|
6
|
+
require "minitest/autorun"
|
7
|
+
require "minitest/find_minimal_combination"
|
8
|
+
|
9
|
+
describe Array, :find_minimal_combination do
|
10
|
+
def check(*bad)
|
11
|
+
lambda { |sample| bad & sample == bad }
|
12
|
+
end
|
13
|
+
|
14
|
+
def assert_steps input, bad, exp
|
15
|
+
tests = []
|
16
|
+
|
17
|
+
found = input.find_minimal_combination do |test|
|
18
|
+
tests << test
|
19
|
+
bad & test == bad
|
20
|
+
end
|
21
|
+
|
22
|
+
assert_equal bad, found, "algorithm is bad"
|
23
|
+
|
24
|
+
assert_equal exp, tests
|
25
|
+
end
|
26
|
+
|
27
|
+
def test_ordering_best_case
|
28
|
+
a = (0..15).to_a
|
29
|
+
bad = [0, 1]
|
30
|
+
exp = [[0, 1, 2, 3, 4, 5, 6, 7],
|
31
|
+
[0, 1, 2, 3],
|
32
|
+
[0, 1],
|
33
|
+
[0],
|
34
|
+
[1],
|
35
|
+
[0, 1]]
|
36
|
+
|
37
|
+
assert_steps a, bad, exp
|
38
|
+
end
|
39
|
+
|
40
|
+
def test_ordering
|
41
|
+
a = (0..15).to_a
|
42
|
+
bad = [0, 15]
|
43
|
+
|
44
|
+
# lvl collection
|
45
|
+
#
|
46
|
+
# 0 | A
|
47
|
+
# 1 | B C
|
48
|
+
# 2 | D E F G
|
49
|
+
# 3 | H I J K L M N O
|
50
|
+
# 4 | 0 1 2 3 4 5 6 7 8 9 A B C D E F
|
51
|
+
#
|
52
|
+
# 0 +++++++++++++++++++++++++++++++
|
53
|
+
# 1 ---------------
|
54
|
+
# 1 ---------------
|
55
|
+
# 2 ------- -------
|
56
|
+
# 2 +++++++ +++++++
|
57
|
+
# 3 xxxxxxx
|
58
|
+
# 3 xxxxxxx
|
59
|
+
# 3 --- ---
|
60
|
+
# 3 +++ +++
|
61
|
+
# 4 xxx
|
62
|
+
# 4 xxx
|
63
|
+
# 4 - -
|
64
|
+
# 4 + +
|
65
|
+
#
|
66
|
+
# - = miss
|
67
|
+
# + = hit
|
68
|
+
# x = unwanted test
|
69
|
+
|
70
|
+
exp = [
|
71
|
+
# level 1 = B, C
|
72
|
+
[0, 1, 2, 3, 4, 5, 6, 7],
|
73
|
+
[8, 9, 10, 11, 12, 13, 14, 15],
|
74
|
+
|
75
|
+
# level 2 = DF, DG, EF, EG
|
76
|
+
[0, 1, 2, 3, 8, 9, 10, 11],
|
77
|
+
[0, 1, 2, 3, 12, 13, 14, 15],
|
78
|
+
|
79
|
+
# level 3
|
80
|
+
# [0, 1, 2, 3], # I think this is bad, we've tested B
|
81
|
+
# [12, 13, 14, 15], # again, bad, we've tested C
|
82
|
+
[0, 1, 12, 13],
|
83
|
+
[0, 1, 14, 15],
|
84
|
+
|
85
|
+
# level 4
|
86
|
+
# [0, 1],
|
87
|
+
# [14, 15],
|
88
|
+
[0, 14],
|
89
|
+
[0, 15],
|
90
|
+
]
|
91
|
+
|
92
|
+
assert_steps a, bad, exp
|
93
|
+
end
|
94
|
+
|
95
|
+
make_my_diffs_pretty!
|
96
|
+
|
97
|
+
def self.test_find_minimal_combination max, *bad
|
98
|
+
define_method "test_find_minimal_combination_#{max}_#{bad.join "_"}" do
|
99
|
+
a = (1..max).to_a
|
100
|
+
a.find_minimal_combination(&check(*bad)).must_equal bad
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
test_find_minimal_combination 8, 5
|
105
|
+
test_find_minimal_combination 8, 2, 7
|
106
|
+
test_find_minimal_combination 8, 1, 2, 7
|
107
|
+
test_find_minimal_combination 8, 1, 4, 7
|
108
|
+
test_find_minimal_combination 8, 1, 3, 5, 7
|
109
|
+
|
110
|
+
test_find_minimal_combination 9, 5
|
111
|
+
test_find_minimal_combination 9, 9
|
112
|
+
test_find_minimal_combination 9, 2, 7
|
113
|
+
test_find_minimal_combination 9, 1, 2, 7
|
114
|
+
test_find_minimal_combination 9, 1, 4, 7
|
115
|
+
test_find_minimal_combination 9, 1, 3, 5, 7
|
116
|
+
|
117
|
+
test_find_minimal_combination 1023, 5
|
118
|
+
test_find_minimal_combination 1023, 1005
|
119
|
+
test_find_minimal_combination 1023, 802, 907
|
120
|
+
test_find_minimal_combination 1023, 7, 15, 166, 1001
|
121
|
+
test_find_minimal_combination 1023, 1000, 1001, 1002
|
122
|
+
test_find_minimal_combination 1023, 1001, 1003, 1005, 1007
|
123
|
+
|
124
|
+
# test_find_minimal_combination 1024, 1001, 1003, 1005, 1007
|
125
|
+
# test_find_minimal_combination 1023, 1001, 1003, 1005, 1007
|
126
|
+
end
|
metadata
ADDED
@@ -0,0 +1,158 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: minitest-bisect
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Ryan Davis
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain:
|
11
|
+
- |
|
12
|
+
-----BEGIN CERTIFICATE-----
|
13
|
+
MIIDPjCCAiagAwIBAgIBATANBgkqhkiG9w0BAQUFADBFMRMwEQYDVQQDDApyeWFu
|
14
|
+
ZC1ydWJ5MRkwFwYKCZImiZPyLGQBGRYJemVuc3BpZGVyMRMwEQYKCZImiZPyLGQB
|
15
|
+
GRYDY29tMB4XDTEzMDkxNjIzMDQxMloXDTE0MDkxNjIzMDQxMlowRTETMBEGA1UE
|
16
|
+
AwwKcnlhbmQtcnVieTEZMBcGCgmSJomT8ixkARkWCXplbnNwaWRlcjETMBEGCgmS
|
17
|
+
JomT8ixkARkWA2NvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALda
|
18
|
+
b9DCgK+627gPJkB6XfjZ1itoOQvpqH1EXScSaba9/S2VF22VYQbXU1xQXL/WzCkx
|
19
|
+
taCPaLmfYIaFcHHCSY4hYDJijRQkLxPeB3xbOfzfLoBDbjvx5JxgJxUjmGa7xhcT
|
20
|
+
oOvjtt5P8+GSK9zLzxQP0gVLS/D0FmoE44XuDr3iQkVS2ujU5zZL84mMNqNB1znh
|
21
|
+
GiadM9GHRaDiaxuX0cIUBj19T01mVE2iymf9I6bEsiayK/n6QujtyCbTWsAS9Rqt
|
22
|
+
qhtV7HJxNKuPj/JFH0D2cswvzznE/a5FOYO68g+YCuFi5L8wZuuM8zzdwjrWHqSV
|
23
|
+
gBEfoTEGr7Zii72cx+sCAwEAAaM5MDcwCQYDVR0TBAIwADALBgNVHQ8EBAMCBLAw
|
24
|
+
HQYDVR0OBBYEFEfFe9md/r/tj/Wmwpy+MI8d9k/hMA0GCSqGSIb3DQEBBQUAA4IB
|
25
|
+
AQCFZ7JTzoy1gcG4d8A6dmOJy7ygtO5MFpRIz8HuKCF5566nOvpy7aHhDDzFmQuu
|
26
|
+
FX3zDU6ghx5cQIueDhf2SGOncyBmmJRRYawm3wI0o1MeN6LZJ/3cRaOTjSFy6+S6
|
27
|
+
zqDmHBp8fVA2TGJtO0BLNkbGVrBJjh0UPmSoGzWlRhEVnYC33TpDAbNA+u39UrQI
|
28
|
+
ynwhNN7YbnmSR7+JU2cUjBFv2iPBO+TGuWC+9L2zn3NHjuc6tnmSYipA9y8Hv+As
|
29
|
+
Y4evBVezr3SjXz08vPqRO5YRdO3zfeMT8gBjRqZjWJGMZ2lD4XNfrs7eky74CyZw
|
30
|
+
xx3n58i0lQkBE1EpKE0lFu/y
|
31
|
+
-----END CERTIFICATE-----
|
32
|
+
date: 2014-08-30 00:00:00.000000000 Z
|
33
|
+
dependencies:
|
34
|
+
- !ruby/object:Gem::Dependency
|
35
|
+
name: minitest
|
36
|
+
requirement: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ~>
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '5.4'
|
41
|
+
type: :development
|
42
|
+
prerelease: false
|
43
|
+
version_requirements: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ~>
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '5.4'
|
48
|
+
- !ruby/object:Gem::Dependency
|
49
|
+
name: rdoc
|
50
|
+
requirement: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ~>
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '4.0'
|
55
|
+
type: :development
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '4.0'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: hoe
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ~>
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '3.12'
|
69
|
+
type: :development
|
70
|
+
prerelease: false
|
71
|
+
version_requirements: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ~>
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '3.12'
|
76
|
+
description: |-
|
77
|
+
Hunting down random test failures can be very very difficult,
|
78
|
+
sometimes impossible, but minitest-bisect makes it easy.
|
79
|
+
|
80
|
+
minitest-bisect helps you isolate and debug random test failures.
|
81
|
+
|
82
|
+
If your tests only fail randomly, you can reproduce the error
|
83
|
+
consistently by using `--seed <num>`, but what then? How do you figure
|
84
|
+
out which combination of tests out of hundreds are responsible for the
|
85
|
+
failure? You know which test is failing, but what others are causing
|
86
|
+
it to fail or were helping it succeed in a different order? That's
|
87
|
+
what minitest-bisect does best.
|
88
|
+
email:
|
89
|
+
- ryand-ruby@zenspider.com
|
90
|
+
executables:
|
91
|
+
- minitest_bisect
|
92
|
+
extensions: []
|
93
|
+
extra_rdoc_files:
|
94
|
+
- History.rdoc
|
95
|
+
- Manifest.txt
|
96
|
+
- README.rdoc
|
97
|
+
files:
|
98
|
+
- .autotest
|
99
|
+
- .gemtest
|
100
|
+
- History.rdoc
|
101
|
+
- Manifest.txt
|
102
|
+
- README.rdoc
|
103
|
+
- Rakefile
|
104
|
+
- bin/minitest_bisect
|
105
|
+
- example-many/helper.rb
|
106
|
+
- example-many/test_bad1.rb
|
107
|
+
- example-many/test_bad2.rb
|
108
|
+
- example-many/test_bad3.rb
|
109
|
+
- example-many/test_bad4.rb
|
110
|
+
- example-many/test_bad5.rb
|
111
|
+
- example-many/test_bad6.rb
|
112
|
+
- example-many/test_bad7.rb
|
113
|
+
- example-many/test_bad8.rb
|
114
|
+
- example/helper.rb
|
115
|
+
- example/test_bad1.rb
|
116
|
+
- example/test_bad2.rb
|
117
|
+
- example/test_bad3.rb
|
118
|
+
- example/test_bad4.rb
|
119
|
+
- example/test_bad5.rb
|
120
|
+
- example/test_bad6.rb
|
121
|
+
- example/test_bad7.rb
|
122
|
+
- example/test_bad8.rb
|
123
|
+
- lib/minitest/bisect.rb
|
124
|
+
- lib/minitest/find_minimal_combination.rb
|
125
|
+
- lib/minitest/server.rb
|
126
|
+
- lib/minitest/server_plugin.rb
|
127
|
+
- test/minitest/test_bisect.rb
|
128
|
+
- test/minitest/test_find_minimal_combination.rb
|
129
|
+
homepage: https://github.com/seattlerb/minitest-bisect
|
130
|
+
licenses:
|
131
|
+
- MIT
|
132
|
+
metadata: {}
|
133
|
+
post_install_message:
|
134
|
+
rdoc_options:
|
135
|
+
- --main
|
136
|
+
- README.rdoc
|
137
|
+
require_paths:
|
138
|
+
- lib
|
139
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
140
|
+
requirements:
|
141
|
+
- - '>='
|
142
|
+
- !ruby/object:Gem::Version
|
143
|
+
version: '0'
|
144
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
145
|
+
requirements:
|
146
|
+
- - '>='
|
147
|
+
- !ruby/object:Gem::Version
|
148
|
+
version: '0'
|
149
|
+
requirements: []
|
150
|
+
rubyforge_project:
|
151
|
+
rubygems_version: 2.2.1
|
152
|
+
signing_key:
|
153
|
+
specification_version: 4
|
154
|
+
summary: Hunting down random test failures can be very very difficult, sometimes impossible,
|
155
|
+
but minitest-bisect makes it easy
|
156
|
+
test_files:
|
157
|
+
- test/minitest/test_bisect.rb
|
158
|
+
- test/minitest/test_find_minimal_combination.rb
|
metadata.gz.sig
ADDED