Class: Minitest::Bisect

Inherits:
Object
  • Object
show all
Defined in:
lib/minitest/bisect.rb

Defined Under Namespace

Classes: PathExpander

Constant Summary collapse

VERSION =
"1.5.0"
SHH =
case
when mtbv == 1 then " > /dev/null"
when mtbv >= 2 then nil
else " > /dev/null 2>&1"
end
RUBY =

Borrowed from rake

ENV['RUBY'] ||
    File.join(RbConfig::CONFIG['bindir'],
              RbConfig::CONFIG['ruby_install_name'] +
RbConfig::CONFIG['EXEEXT']).sub(/.*\s.*/m, '"\&"')

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeBisect

Returns a new instance of Bisect.



64
65
66
67
# File 'lib/minitest/bisect.rb', line 64

def initialize
  self.culprits = []
  self.failures = Hash.new { |h, k| h[k] = Hash.new { |h2, k2| h2[k2] = [] } }
end

Instance Attribute Details

#culpritsObject

Returns the value of attribute culprits.



54
55
56
# File 'lib/minitest/bisect.rb', line 54

def culprits
  @culprits
end

#failuresObject

Returns the value of attribute failures.



54
55
56
# File 'lib/minitest/bisect.rb', line 54

def failures
  @failures
end

#modeObject

Returns the value of attribute mode.



54
55
56
# File 'lib/minitest/bisect.rb', line 54

def mode
  @mode
end

#seen_badObject

Returns the value of attribute seen_bad.



54
55
56
# File 'lib/minitest/bisect.rb', line 54

def seen_bad
  @seen_bad
end

#taintedObject Also known as: tainted?

Returns the value of attribute tainted.



54
55
56
# File 'lib/minitest/bisect.rb', line 54

def tainted
  @tainted
end

Class Method Details

.run(files) ⇒ Object



57
58
59
60
61
62
# File 'lib/minitest/bisect.rb', line 57

def self.run files
  new.run files
rescue => e
  warn e.message
  exit 1
end

Instance Method Details

#bisect_files(files) ⇒ Object



102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
# File 'lib/minitest/bisect.rb', line 102

def bisect_files files
  self.mode = :files

  files, flags = files.partition { |arg| File.file? arg }
  rb_flags, mt_flags = flags.partition { |arg| arg =~ /^-I/ }
  mt_flags += ["--server", $$]

  puts "reproducing..."
  system "#{build_files_cmd files, rb_flags, mt_flags} #{SHH}"
  abort "Reproduction run passed? Aborting. Try running with MTB_VERBOSE=2 to verify." unless tainted?
  puts "reproduced"

  found, count = files.find_minimal_combination_and_count do |test|
    puts "# of culprit files: #{test.size}"

    system "#{build_files_cmd test, rb_flags, mt_flags} #{SHH}"

    self.tainted?
  end

  puts
  puts "Minimal files found in #{count} steps:"
  puts
  cmd = build_files_cmd found, rb_flags, mt_flags
  puts cmd
  cmd
end

#bisect_methods(cmd) ⇒ Object



130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
# File 'lib/minitest/bisect.rb', line 130

def bisect_methods cmd
  self.mode = :methods

  time_it "reproducing...", build_methods_cmd(cmd)

  unless tainted? then
    $stderr.puts "Reproduction run passed? Aborting."
    abort "Try running with MTB_VERBOSE=2 to verify."
  end

  bad = map_failures

  raise "Nothing to verify against because every test failed. Aborting." if
    culprits.empty? && seen_bad

  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

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

    self.tainted?
  end

  puts
  puts "Minimal methods found in #{count} steps:"
  puts
  puts "Culprit methods: %p" % [found]
  puts
  cmd = build_methods_cmd cmd, found, bad
  puts cmd.sub(/--server \d+/, "")
  puts
  cmd
end

#build_files_cmd(culprits, rb, mt) ⇒ Object



188
189
190
191
192
193
194
# File 'lib/minitest/bisect.rb', line 188

def build_files_cmd culprits, rb, mt
  reset

  tests = culprits.flatten.compact.map { |f| %(require "./#{f}") }.join " ; "

  %(#{RUBY} #{rb.shelljoin} -e '#{tests}' -- #{mt.map(&:to_s).shelljoin})
end

#build_methods_cmd(cmd, culprits = [], bad = nil) ⇒ Object



220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
# File 'lib/minitest/bisect.rb', line 220

def build_methods_cmd cmd, culprits = [], bad = nil
  reset

  if bad then
    re = build_re culprits + bad

    cmd += " -n \"#{re}\"" if bad
  end

  if ENV["MTB_VERBOSE"].to_i >= 1 then
    puts
    puts cmd
    puts
  end

  cmd
end

#build_re(bad) ⇒ Object



196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
# File 'lib/minitest/bisect.rb', line 196

def build_re bad
  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_failuresObject



180
181
182
183
184
185
186
# File 'lib/minitest/bisect.rb', line 180

def map_failures
  # 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) ⇒ Object



245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
# File 'lib/minitest/bisect.rb', line 245

def minitest_result file, klass, method, fails, assertions, time
  fails.reject! { |fail| Minitest::Skip === fail }

  if mode == :methods then
    if fails.empty? then
      culprits << "#{klass}##{method}" unless seen_bad # UGH
    else
      self.seen_bad = true
    end
  end

  return if fails.empty?

  self.tainted = true
  self.failures[file][klass] << method
end

#minitest_startObject

Server Methods:



241
242
243
# File 'lib/minitest/bisect.rb', line 241

def minitest_start
  self.failures.clear
end

#re_escape(str) ⇒ Object



216
217
218
# File 'lib/minitest/bisect.rb', line 216

def re_escape str
  str.gsub(/([`'"!?&\[\]\(\)\{\}\|\+])/, '\\\\\1')
end

#resetObject



69
70
71
72
73
74
# File 'lib/minitest/bisect.rb', line 69

def reset
  self.seen_bad = false
  self.tainted  = false
  failures.clear
  # not clearing culprits on purpose
end

#run(args) ⇒ Object



76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
# File 'lib/minitest/bisect.rb', line 76

def run args
  Minitest::Server.run self

  cmd = nil

  if :until_I_have_negative_filtering_in_minitest != 0 then
    mt_flags = args.dup
    expander = Minitest::Bisect::PathExpander.new mt_flags

    files = expander.process
    rb_flags = expander.rb_flags
    mt_flags += ["--server", $$]

    cmd = bisect_methods build_files_cmd(files, rb_flags, mt_flags)
  else
    cmd = bisect_methods bisect_files args
  end

  puts "Final reproduction:"
  puts

  system cmd.sub(/--server \d+/, "")
ensure
  Minitest::Server.stop
end

#time_it(prompt, cmd) ⇒ Object



173
174
175
176
177
178
# File 'lib/minitest/bisect.rb', line 173

def time_it prompt, cmd
  print prompt
  t0 = Time.now
  system "#{cmd} #{SHH}"
  puts " in %.2f sec" % (Time.now - t0)
end