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 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
@@ -0,0 +1,2 @@
1
+ ���
2
+ �ۗgEc�m1��x���wN���H��$��*Y���j���� W����N.���S`{J��\��:���X7���r�u���Z�vѵ��'���`~�ESu铱�j9顅6�2�S������=ȸ��[k�j�vd�u+�n����
- ��n��9;`� n���'=x�e�7<\�K������]��6����4y �cP�]�(?>��|Qq����&jo�;7���L��WU��8\i��@3UMuM
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
@@ -0,0 +1,6 @@
1
+ === 1.0.0 / 2014-07-16
2
+
3
+ * 1 major enhancement
4
+
5
+ * Birthday!
6
+
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
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/ruby -w
2
+
3
+ require "minitest/bisect"
4
+
5
+ Minitest::Bisect.run ARGV
@@ -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
@@ -0,0 +1,4 @@
1
+ require "minitest/autorun"
2
+ require_relative "helper"
3
+
4
+ TestBad1 = create_test 1, 100, 1 => true
@@ -0,0 +1,4 @@
1
+ require "minitest/autorun"
2
+ require_relative "helper"
3
+
4
+ TestBad2 = create_test 2, 100
@@ -0,0 +1,4 @@
1
+ require "minitest/autorun"
2
+ require_relative "helper"
3
+
4
+ TestBad3 = create_test 3, 100, 72 => true
@@ -0,0 +1,4 @@
1
+ require "minitest/autorun"
2
+ require_relative "helper"
3
+
4
+ TestBad4 = create_test 4, 100
@@ -0,0 +1,4 @@
1
+ require "minitest/autorun"
2
+ require_relative "helper"
3
+
4
+ TestBad5 = create_test 5, 100, 17 => true
@@ -0,0 +1,4 @@
1
+ require "minitest/autorun"
2
+ require_relative "helper"
3
+
4
+ TestBad6 = create_test 6, 100
@@ -0,0 +1,4 @@
1
+ require "minitest/autorun"
2
+ require_relative "helper"
3
+
4
+ TestBad7 = create_test 7, 100
@@ -0,0 +1,4 @@
1
+ require "minitest/autorun"
2
+ require_relative "helper"
3
+
4
+ TestBad8 = create_test 8, 100, 43 => 3
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,4 @@
1
+ require "minitest/autorun"
2
+ require_relative "helper"
3
+
4
+ TestBad1 = create_test 1, 100, 1 => false
@@ -0,0 +1,4 @@
1
+ require "minitest/autorun"
2
+ require_relative "helper"
3
+
4
+ TestBad2 = create_test 2, 100
@@ -0,0 +1,4 @@
1
+ require "minitest/autorun"
2
+ require_relative "helper"
3
+
4
+ TestBad3 = create_test 3, 100
@@ -0,0 +1,4 @@
1
+ require "minitest/autorun"
2
+ require_relative "helper"
3
+
4
+ TestBad4 = create_test 4, 100, 4 => true
@@ -0,0 +1,4 @@
1
+ require "minitest/autorun"
2
+ require_relative "helper"
3
+
4
+ TestBad5 = create_test 5, 100
@@ -0,0 +1,4 @@
1
+ require "minitest/autorun"
2
+ require_relative "helper"
3
+
4
+ TestBad6 = create_test 6, 100
@@ -0,0 +1,4 @@
1
+ require "minitest/autorun"
2
+ require_relative "helper"
3
+
4
+ TestBad7 = create_test 7, 100
@@ -0,0 +1,4 @@
1
+ require "minitest/autorun"
2
+ require_relative "helper"
3
+
4
+ TestBad8 = create_test 8, 100
@@ -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,10 @@
1
+ require "minitest/autorun"
2
+ require "minitest/bisect"
3
+
4
+ module TestMinitest; end
5
+
6
+ class TestMinitest::TestBisect < Minitest::Test
7
+ def test_sanity
8
+ flunk "write tests or I will kneecap you"
9
+ end
10
+ 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
@@ -0,0 +1,2 @@
1
+ �O��
2
+ �����26I����>W3�����a1\�kI@�{4�:�,"M�����{�]ò��W9dgG�pKM��`��J���$�4����C��8`��S���i���!�0�Df�uܨ��.�����ً[������26���4'rXV��qL#ݟ��:�]�ӈ���\=g<�*�7�l�@�Zg�#�\oR�Qs�e+KG�w�~0%��<�i��ˬ0{*C��(��1�I�<��OֶF���b@���u���