Class: Minitest::Bisect
| Relationships & Source Files | |
| Namespace Children | |
|
Classes:
| |
| Inherits: | Object |
| Defined in: | lib/minitest/bisect.rb |
Overview
Bisect helps you isolate and debug random test failures.
Constant Summary
-
RUBY =
# File 'lib/minitest/bisect.rb', line 54
Borrowed from rake
ENV['RUBY'] || File.join(RbConfig::CONFIG['bindir'], RbConfig::CONFIG['ruby_install_name'] + RbConfig::CONFIG['EXEEXT']).sub(/.*\s.*/m, '"\&"')
-
SHH =
Internal use only
# File 'lib/minitest/bisect.rb', line 47case # :nodoc: when mtbv == 1 then " > /dev/null" when mtbv >= 2 then nil else " > /dev/null 2>&1" end
-
VERSION =
Internal use only
# File 'lib/minitest/bisect.rb', line 13"1.8.0"
Class Method Summary
-
.new ⇒ Bisect
constructor
Instantiate a new
Bisect. -
.run(files)
Top-level runner.
Instance Attribute Summary
Instance Method Summary
-
#bisect_methods(files, rb_flags, mt_flags)
Normal: find “what is the minimal combination of tests to run to.
-
#reset
Reset per-bisect-run variables.
-
#run(args)
Instance-level runner.
-
#tainted
(also: #tainted?)
rw
True if this run has seen a failure.
- #build_files_cmd(culprits, rb, mt) Internal use only
- #build_methods_cmd(cmd, culprits = [], bad = nil) Internal use only
- #build_re(bad) Internal use only
- #map_failures Internal use only
- #minitest_result(file, klass, method, fails, assertions, time) Internal use only
-
#minitest_start
Internal use only
ServerMethods: - #re_escape(str) Internal use only
- #time_it(prompt, cmd) Internal use only
Constructor Details
.new ⇒ Bisect
Instantiate a new Bisect.
Class Method Details
.run(files)
Top-level runner. Instantiate and call run, handling exceptions.
# File 'lib/minitest/bisect.rb', line 82
def self.run files new.run files rescue => e warn e. warn "Try running with MTB_VERBOSE=2 to verify." exit 1 end
Instance Attribute Details
#culprits (rw)
An array of tests seen so far. NOT cleared by #reset.
# File 'lib/minitest/bisect.rb', line 75
attr_accessor :culprits
#failures (rw)
Failures seen in this run. Shape:
{"file.rb"=>{"Class"=>["test_method1", "test_method2"] ...} ...}
# File 'lib/minitest/bisect.rb', line 70
attr_accessor :failures
#seen_bad (rw)
# File 'lib/minitest/bisect.rb', line 77
attr_accessor :seen_bad # :nodoc:
#tainted? (rw)
Alias for #tainted.
# File 'lib/minitest/bisect.rb', line 63
alias :tainted? :tainted
Instance Method Details
#bisect_methods(files, rb_flags, mt_flags)
Normal: find “what is the minimal combination of tests to run to
make X fail?"
Run with: minitest_bisect … –seed=N
-
Verify the failure running normally with the seed.
-
If no failure, punt.
-
If no passing tests before failure, punt. (No culprits == no debug)
-
-
Verify the failure doesn’t fail in isolation.
-
If it still fails by itself, warn that it might not be an ordering issue.
-
-
Cull all tests after the failure, they’re not involved.
-
Bisectthe culprits + bad until you find a minimal combo that fails. -
Display minimal combo by running one last time.
Inverted: find “what is the minimal combination of tests to run to
make this test pass?"
Run with: minitest_bisect … –seed=N -n=“/failing_test_name_regexp/”
-
Verify the failure by running normally w/ the seed and -n=/…/
-
If no failure, punt.
-
-
Verify the passing case by running everything.
-
If failure, punt. This is not a false positive.
-
-
Cull all tests after the bad test from
#1, they’re not involved. -
Bisectthe culprits + bad until you find a minimal combo that passes. -
Display minimal combo by running one last time.
# File 'lib/minitest/bisect.rb', line 163
def bisect_methods files, rb_flags, mt_flags bad_names, mt_flags = mt_flags.partition { |s| s =~ /^(?:-n|--name)/ } normal = bad_names.empty? inverted = !normal if inverted then time_it "reproducing w/ scoped failure (inverted run!)...", build_methods_cmd(build_files_cmd(files, rb_flags, mt_flags + bad_names)) raise "No failures. Probably not a false positive. Aborting." if failures.empty? bad = map_failures end cmd = build_files_cmd(files, rb_flags, mt_flags) msg = normal ? "reproducing..." : "reproducing false positive..." time_it msg, build_methods_cmd(cmd) if normal then raise "Reproduction run passed? Aborting." unless tainted? raise "Verification failed. No culprits? Aborting." if culprits.empty? && seen_bad else raise "Reproduction failed? Not false positive. Aborting." if tainted? raise "Verification failed. No culprits? Aborting." if culprits.empty? || seen_bad end if normal then bad = map_failures time_it "verifying...", build_methods_cmd(cmd, [], bad) new_bad = map_failures if bad == new_bad then warn "Tests fail by themselves. This may not be an ordering issue." end end idx = culprits.index bad.first self.culprits = culprits.take idx+1 if idx # cull tests after bad # culprits populated by initial reproduction via minitest/server found, count = culprits.find_minimal_combination_and_count do |test| prompt = "# of culprit methods: #{test.size}" time_it prompt, build_methods_cmd(cmd, test, bad) normal == tainted? # either normal and failed, or inverse and passed end puts puts "Minimal methods found in #{count} steps:" puts puts "Culprit methods: %p" % [found + bad] puts cmd = build_methods_cmd cmd, found, bad puts cmd.sub(/--server \d+/, "") puts cmd end
#build_files_cmd(culprits, rb, mt)
#build_methods_cmd(cmd, culprits = [], bad = nil)
#build_re(bad)
# File 'lib/minitest/bisect.rb', line 261
def build_re bad # :nodoc: re = [] # bad by class, you perv bbc = bad.map { |s| s.split(/#/, 2) }.group_by(&:first) bbc.each do |klass, methods| methods = methods.map(&:last).flatten.uniq.map { |method| re_escape method } methods = methods.join "|" re << /#{re_escape klass}#(?:#{methods})/.to_s[7..-2] # (?-mix:...) end re = re.join("|").to_s.gsub(/-mix/, "") "/^(?:#{re})$/" end
#map_failures
# File 'lib/minitest/bisect.rb', line 229
def map_failures # :nodoc: # from: {"file.rb"=>{"Class"=>["test_method1", "test_method2"]}} # to: ["Class#test_method1", "Class#test_method2"] failures.values.map { |h| h.map { |k,vs| vs.map { |v| "#{k}##{v}" } } }.flatten.sort end
#minitest_result(file, klass, method, fails, assertions, time)
# File 'lib/minitest/bisect.rb', line 292
def minitest_result file, klass, method, fails, assertions, time # :nodoc: fails.reject! { |fail| Minitest::Skip === fail } if fails.empty? then culprits << "#{klass}##{method}" unless seen_bad # UGH else self.seen_bad = true end return if fails.empty? self.tainted = true self.failures[file][klass] << method end
#minitest_start
Server Methods:
# File 'lib/minitest/bisect.rb', line 288
def minitest_start # :nodoc: self.failures.clear end
#re_escape(str)
# File 'lib/minitest/bisect.rb', line 281
def re_escape str # :nodoc: str.gsub(/([`'"!?&\[\]\(\)\{\}\|\+])/, '\\\\\1') end
#reset
Reset per-bisect-run variables.
#run(args)
Instance-level runner. Handles Server, argument processing, and invoking #bisect_methods.
# File 'lib/minitest/bisect.rb', line 112
def run args Minitest::Server.run self cmd = nil mt_flags = args.dup = Minitest::Bisect::PathExpander.new mt_flags files = .process.to_a rb_flags = .rb_flags mt_flags += ["--server", $$.to_s] cmd = bisect_methods files, rb_flags, mt_flags puts "Final reproduction:" puts system cmd.sub(/--server \d+/, "") ensure Minitest::Server.stop end
#tainted (rw) Also known as: #tainted?
True if this run has seen a failure.
# File 'lib/minitest/bisect.rb', line 62
attr_accessor :tainted
#time_it(prompt, cmd)
# File 'lib/minitest/bisect.rb', line 222
def time_it prompt, cmd # :nodoc: print prompt t0 = Time.now system "#{cmd} #{SHH}" puts " in %.2f sec" % (Time.now - t0) end