minitest-bisect 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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