minitest 5.25.5 → 6.0.6
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 +4 -4
- checksums.yaml.gz.sig +2 -4
- data/History.rdoc +170 -0
- data/Manifest.txt +13 -4
- data/README.rdoc +21 -100
- data/Rakefile +10 -2
- data/bin/minitest +5 -0
- data/design_rationale.rb +21 -19
- data/lib/hoe/minitest.rb +2 -1
- data/lib/minitest/assertions.rb +47 -81
- data/lib/minitest/autorun.rb +3 -4
- data/lib/minitest/benchmark.rb +3 -3
- data/lib/minitest/bisect.rb +304 -0
- data/lib/minitest/complete.rb +56 -0
- data/lib/minitest/find_minimal_combination.rb +127 -0
- data/lib/minitest/hell.rb +1 -1
- data/lib/minitest/manual_plugins.rb +4 -16
- data/lib/minitest/parallel.rb +5 -3
- data/lib/minitest/path_expander.rb +432 -0
- data/lib/minitest/pride.rb +2 -2
- data/lib/minitest/pride_plugin.rb +1 -1
- data/lib/minitest/server.rb +49 -0
- data/lib/minitest/server_plugin.rb +88 -0
- data/lib/minitest/spec.rb +11 -37
- data/lib/minitest/sprint.rb +105 -0
- data/lib/minitest/sprint_plugin.rb +39 -0
- data/lib/minitest/test.rb +8 -13
- data/lib/minitest/test_task.rb +44 -20
- data/lib/minitest.rb +104 -107
- data/test/minitest/metametameta.rb +1 -1
- data/test/minitest/test_bisect.rb +249 -0
- data/test/minitest/test_find_minimal_combination.rb +138 -0
- data/test/minitest/test_minitest_assertions.rb +114 -105
- data/test/minitest/test_minitest_benchmark.rb +14 -0
- data/test/minitest/test_minitest_reporter.rb +6 -5
- data/test/minitest/test_minitest_spec.rb +60 -128
- data/test/minitest/test_minitest_test.rb +22 -101
- data/test/minitest/test_path_expander.rb +229 -0
- data/test/minitest/test_server.rb +146 -0
- data.tar.gz.sig +0 -0
- metadata +92 -33
- metadata.gz.sig +0 -0
- data/.autotest +0 -34
- data/lib/minitest/mock.rb +0 -347
- data/lib/minitest/unit.rb +0 -42
- data/test/minitest/test_minitest_mock.rb +0 -1218
data/lib/minitest/assertions.rb
CHANGED
|
@@ -62,11 +62,11 @@ module Minitest
|
|
|
62
62
|
return "Expected: #{mu_pp exp}\n Actual: #{mu_pp act}" unless
|
|
63
63
|
expect
|
|
64
64
|
|
|
65
|
-
Tempfile.
|
|
65
|
+
Tempfile.create "expect" do |a|
|
|
66
66
|
a.puts expect
|
|
67
67
|
a.flush
|
|
68
68
|
|
|
69
|
-
Tempfile.
|
|
69
|
+
Tempfile.create "butwas" do |b|
|
|
70
70
|
b.puts butwas
|
|
71
71
|
b.flush
|
|
72
72
|
|
|
@@ -186,18 +186,10 @@ module Minitest
|
|
|
186
186
|
# Fails unless +obj+ is empty.
|
|
187
187
|
|
|
188
188
|
def assert_empty obj, msg = nil
|
|
189
|
-
msg = message(msg) { "Expected #{mu_pp
|
|
190
|
-
|
|
191
|
-
assert obj.empty?, msg
|
|
189
|
+
msg = message(msg) { "Expected #{mu_pp(obj)} to be empty" }
|
|
190
|
+
assert_predicate obj, :empty?, msg
|
|
192
191
|
end
|
|
193
192
|
|
|
194
|
-
def _where # :nodoc:
|
|
195
|
-
Minitest.filter_backtrace(caller).first
|
|
196
|
-
.split(":in ", 2).first # clean up noise
|
|
197
|
-
end
|
|
198
|
-
|
|
199
|
-
E = "" # :nodoc:
|
|
200
|
-
|
|
201
193
|
##
|
|
202
194
|
# Fails unless <tt>exp == act</tt> printing the difference between
|
|
203
195
|
# the two, if possible.
|
|
@@ -212,18 +204,11 @@ module Minitest
|
|
|
212
204
|
# See also: Minitest::Assertions.diff
|
|
213
205
|
|
|
214
206
|
def assert_equal exp, act, msg = nil
|
|
215
|
-
msg = message(msg,
|
|
216
|
-
result = assert exp == act, msg
|
|
207
|
+
msg = message(msg, nil) { diff exp, act }
|
|
217
208
|
|
|
218
|
-
if nil == exp
|
|
219
|
-
if Minitest::VERSION >= "6" then
|
|
220
|
-
refute_nil exp, "Use assert_nil if expecting nil."
|
|
221
|
-
else
|
|
222
|
-
warn "DEPRECATED: Use assert_nil if expecting nil from #{_where}. This will fail in Minitest 6."
|
|
223
|
-
end
|
|
224
|
-
end
|
|
209
|
+
refute_nil exp, message { "Use assert_nil if expecting nil" } if nil == exp # don't count
|
|
225
210
|
|
|
226
|
-
|
|
211
|
+
assert exp == act, msg
|
|
227
212
|
end
|
|
228
213
|
|
|
229
214
|
##
|
|
@@ -255,8 +240,7 @@ module Minitest
|
|
|
255
240
|
msg = message(msg) {
|
|
256
241
|
"Expected #{mu_pp collection} to include #{mu_pp obj}"
|
|
257
242
|
}
|
|
258
|
-
|
|
259
|
-
assert collection.include?(obj), msg
|
|
243
|
+
assert_operator collection, :include?, obj, msg
|
|
260
244
|
end
|
|
261
245
|
|
|
262
246
|
##
|
|
@@ -298,7 +282,7 @@ module Minitest
|
|
|
298
282
|
|
|
299
283
|
def assert_nil obj, msg = nil
|
|
300
284
|
msg = message(msg) { "Expected #{mu_pp obj} to be nil" }
|
|
301
|
-
assert
|
|
285
|
+
assert nil == obj, msg
|
|
302
286
|
end
|
|
303
287
|
|
|
304
288
|
##
|
|
@@ -308,6 +292,7 @@ module Minitest
|
|
|
308
292
|
|
|
309
293
|
def assert_operator o1, op, o2 = UNDEFINED, msg = nil
|
|
310
294
|
return assert_predicate o1, op, msg if UNDEFINED == o2
|
|
295
|
+
assert_respond_to o1, op
|
|
311
296
|
msg = message(msg) { "Expected #{mu_pp o1} to be #{op} #{mu_pp o2}" }
|
|
312
297
|
assert o1.__send__(op, o2), msg
|
|
313
298
|
end
|
|
@@ -368,15 +353,12 @@ module Minitest
|
|
|
368
353
|
# error.
|
|
369
354
|
|
|
370
355
|
def assert_pattern
|
|
371
|
-
raise NotImplementedError, "only available in Ruby 3.0+" unless RUBY_VERSION >= "3.0"
|
|
372
356
|
flunk "assert_pattern requires a block to capture errors." unless block_given?
|
|
373
357
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
flunk e.message
|
|
379
|
-
end
|
|
358
|
+
yield
|
|
359
|
+
pass
|
|
360
|
+
rescue NoMatchingPatternError => e
|
|
361
|
+
flunk e.message
|
|
380
362
|
end
|
|
381
363
|
|
|
382
364
|
##
|
|
@@ -389,10 +371,13 @@ module Minitest
|
|
|
389
371
|
# str.must_be :empty?
|
|
390
372
|
|
|
391
373
|
def assert_predicate o1, op, msg = nil
|
|
374
|
+
assert_respond_to o1, op, include_all:true
|
|
392
375
|
msg = message(msg) { "Expected #{mu_pp o1} to be #{op}" }
|
|
393
376
|
assert o1.__send__(op), msg
|
|
394
377
|
end
|
|
395
378
|
|
|
379
|
+
NO_RE_MSG = "class or module required for rescue clause. Got %p"
|
|
380
|
+
|
|
396
381
|
##
|
|
397
382
|
# Fails unless the block raises one of +exp+. Returns the
|
|
398
383
|
# exception matched so you can check the message, attributes, etc.
|
|
@@ -422,6 +407,9 @@ module Minitest
|
|
|
422
407
|
msg = "#{exp.pop}.\n" if String === exp.last
|
|
423
408
|
exp << StandardError if exp.empty?
|
|
424
409
|
|
|
410
|
+
# TODO: remove this if https://bugs.ruby-lang.org/issues/22007 gets fixed
|
|
411
|
+
raise TypeError, NO_RE_MSG % [exp] unless exp.all? Module
|
|
412
|
+
|
|
425
413
|
begin
|
|
426
414
|
yield
|
|
427
415
|
rescue *exp => e
|
|
@@ -448,9 +436,7 @@ module Minitest
|
|
|
448
436
|
# include_all defaults to false to match Object#respond_to?
|
|
449
437
|
|
|
450
438
|
def assert_respond_to obj, meth, msg = nil, include_all: false
|
|
451
|
-
msg = message(msg) {
|
|
452
|
-
"Expected #{mu_pp obj} (#{obj.class}) to respond to ##{meth}"
|
|
453
|
-
}
|
|
439
|
+
msg = message(msg) { "Expected #{mu_pp obj} (#{obj.class}) to respond to ##{meth}" }
|
|
454
440
|
assert obj.respond_to?(meth, include_all), msg
|
|
455
441
|
end
|
|
456
442
|
|
|
@@ -462,22 +448,10 @@ module Minitest
|
|
|
462
448
|
data = [mu_pp(act), act.object_id, mu_pp(exp), exp.object_id]
|
|
463
449
|
"Expected %s (oid=%d) to be the same as %s (oid=%d)" % data
|
|
464
450
|
}
|
|
465
|
-
assert exp.equal?(act), msg
|
|
466
|
-
end
|
|
467
451
|
|
|
468
|
-
|
|
469
|
-
# +send_ary+ is a receiver, message and arguments.
|
|
470
|
-
#
|
|
471
|
-
# Fails unless the call returns a true value
|
|
452
|
+
refute_nil exp, message { "Use assert_nil if expecting nil" } if nil == exp # don't count
|
|
472
453
|
|
|
473
|
-
|
|
474
|
-
warn "DEPRECATED: assert_send. From #{_where}"
|
|
475
|
-
|
|
476
|
-
recv, msg, *args = send_ary
|
|
477
|
-
m = message(m) {
|
|
478
|
-
"Expected #{mu_pp recv}.#{msg}(*#{mu_pp args}) to return true"
|
|
479
|
-
}
|
|
480
|
-
assert recv.__send__(msg, *args), m
|
|
454
|
+
assert exp.equal?(act), msg
|
|
481
455
|
end
|
|
482
456
|
|
|
483
457
|
##
|
|
@@ -500,14 +474,9 @@ module Minitest
|
|
|
500
474
|
value = catch sym do
|
|
501
475
|
begin
|
|
502
476
|
yield
|
|
503
|
-
rescue
|
|
504
|
-
default += ", not :#{e.message[/uncaught throw \`(\w+?)\'/, 1]}"
|
|
505
|
-
rescue ArgumentError => e # 1.9 exception
|
|
477
|
+
rescue ArgumentError => e # 1.9+ exception
|
|
506
478
|
raise e unless e.message.include? "uncaught throw"
|
|
507
479
|
default += ", not #{e.message.split(/ /).last}"
|
|
508
|
-
rescue NameError => e # 1.8 exception
|
|
509
|
-
raise e unless e.name == sym
|
|
510
|
-
default += ", not #{e.name.inspect}"
|
|
511
480
|
end
|
|
512
481
|
caught = false
|
|
513
482
|
end
|
|
@@ -629,13 +598,16 @@ module Minitest
|
|
|
629
598
|
end
|
|
630
599
|
|
|
631
600
|
##
|
|
632
|
-
# Returns a proc that
|
|
601
|
+
# Returns a proc that delays generation of an output message. If
|
|
602
|
+
# +msg+ is a proc (eg, from another +message+ call) return +msg+
|
|
603
|
+
# as-is. Otherwise, return a proc that will output +msg+ along
|
|
604
|
+
# with the value of the result of the block passed to +message+.
|
|
633
605
|
|
|
634
|
-
def message msg = nil, ending =
|
|
606
|
+
def message msg = nil, ending = ".", &default
|
|
607
|
+
return msg if Proc === msg
|
|
635
608
|
proc {
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
"#{custom_message}#{default.call}#{ending || "."}"
|
|
609
|
+
custom_message = "#{msg}.\n" unless nil == msg or msg.to_s.empty?
|
|
610
|
+
"#{custom_message}#{default.call}#{ending}"
|
|
639
611
|
}
|
|
640
612
|
end
|
|
641
613
|
|
|
@@ -659,8 +631,7 @@ module Minitest
|
|
|
659
631
|
|
|
660
632
|
def refute_empty obj, msg = nil
|
|
661
633
|
msg = message(msg) { "Expected #{mu_pp obj} to not be empty" }
|
|
662
|
-
|
|
663
|
-
refute obj.empty?, msg
|
|
634
|
+
refute_predicate obj, :empty?, msg
|
|
664
635
|
end
|
|
665
636
|
|
|
666
637
|
##
|
|
@@ -692,19 +663,16 @@ module Minitest
|
|
|
692
663
|
# For comparing Floats. Fails if +exp+ and +act+ have a relative error
|
|
693
664
|
# less than +epsilon+.
|
|
694
665
|
|
|
695
|
-
def refute_in_epsilon
|
|
696
|
-
refute_in_delta
|
|
666
|
+
def refute_in_epsilon exp, act, epsilon = 0.001, msg = nil
|
|
667
|
+
refute_in_delta exp, act, [exp.abs, act.abs].min * epsilon, msg
|
|
697
668
|
end
|
|
698
669
|
|
|
699
670
|
##
|
|
700
|
-
# Fails if +
|
|
671
|
+
# Fails if +obj+ includes +sub+.
|
|
701
672
|
|
|
702
|
-
def refute_includes
|
|
703
|
-
msg = message(msg) {
|
|
704
|
-
|
|
705
|
-
}
|
|
706
|
-
assert_respond_to collection, :include?
|
|
707
|
-
refute collection.include?(obj), msg
|
|
673
|
+
def refute_includes obj, sub, msg = nil
|
|
674
|
+
msg = message(msg) { "Expected #{mu_pp obj} to not include #{mu_pp sub}" }
|
|
675
|
+
refute_operator obj, :include?, sub, msg
|
|
708
676
|
end
|
|
709
677
|
|
|
710
678
|
##
|
|
@@ -730,9 +698,8 @@ module Minitest
|
|
|
730
698
|
|
|
731
699
|
def refute_match matcher, obj, msg = nil
|
|
732
700
|
msg = message(msg) { "Expected #{mu_pp matcher} to not match #{mu_pp obj}" }
|
|
733
|
-
assert_respond_to matcher, :=~
|
|
734
701
|
matcher = Regexp.new Regexp.escape matcher if String === matcher
|
|
735
|
-
|
|
702
|
+
refute_operator matcher, :=~, obj, msg
|
|
736
703
|
end
|
|
737
704
|
|
|
738
705
|
##
|
|
@@ -740,7 +707,7 @@ module Minitest
|
|
|
740
707
|
|
|
741
708
|
def refute_nil obj, msg = nil
|
|
742
709
|
msg = message(msg) { "Expected #{mu_pp obj} to not be nil" }
|
|
743
|
-
refute
|
|
710
|
+
refute nil == obj, msg
|
|
744
711
|
end
|
|
745
712
|
|
|
746
713
|
##
|
|
@@ -756,15 +723,12 @@ module Minitest
|
|
|
756
723
|
# other exceptions will be raised as normal and generate a test error.
|
|
757
724
|
|
|
758
725
|
def refute_pattern
|
|
759
|
-
raise NotImplementedError, "only available in Ruby 3.0+" unless RUBY_VERSION >= "3.0"
|
|
760
726
|
flunk "refute_pattern requires a block to capture errors." unless block_given?
|
|
761
727
|
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
pass
|
|
767
|
-
end
|
|
728
|
+
yield
|
|
729
|
+
flunk "NoMatchingPatternError expected, but nothing was raised."
|
|
730
|
+
rescue NoMatchingPatternError
|
|
731
|
+
pass
|
|
768
732
|
end
|
|
769
733
|
|
|
770
734
|
##
|
|
@@ -775,6 +739,7 @@ module Minitest
|
|
|
775
739
|
|
|
776
740
|
def refute_operator o1, op, o2 = UNDEFINED, msg = nil
|
|
777
741
|
return refute_predicate o1, op, msg if UNDEFINED == o2
|
|
742
|
+
assert_respond_to o1, op
|
|
778
743
|
msg = message(msg) { "Expected #{mu_pp o1} to not be #{op} #{mu_pp o2}" }
|
|
779
744
|
refute o1.__send__(op, o2), msg
|
|
780
745
|
end
|
|
@@ -797,6 +762,7 @@ module Minitest
|
|
|
797
762
|
# str.wont_be :empty?
|
|
798
763
|
|
|
799
764
|
def refute_predicate o1, op, msg = nil
|
|
765
|
+
assert_respond_to o1, op, include_all:true
|
|
800
766
|
msg = message(msg) { "Expected #{mu_pp o1} to not be #{op}" }
|
|
801
767
|
refute o1.__send__(op), msg
|
|
802
768
|
end
|
data/lib/minitest/autorun.rb
CHANGED
data/lib/minitest/benchmark.rb
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
require_relative "test"
|
|
2
|
+
require_relative "spec"
|
|
3
3
|
|
|
4
4
|
module Minitest
|
|
5
5
|
##
|
|
@@ -17,7 +17,7 @@ module Minitest
|
|
|
17
17
|
self.class.io
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
-
def self.run
|
|
20
|
+
def self.run klass, method_name, reporter # :nodoc:
|
|
21
21
|
@io = reporter.io
|
|
22
22
|
super
|
|
23
23
|
end
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
require_relative "find_minimal_combination"
|
|
2
|
+
require_relative "server"
|
|
3
|
+
require "shellwords"
|
|
4
|
+
require "rbconfig"
|
|
5
|
+
require_relative "path_expander" # this is gonna break some shit?
|
|
6
|
+
|
|
7
|
+
module Minitest; end # :nodoc:
|
|
8
|
+
|
|
9
|
+
##
|
|
10
|
+
# Minitest::Bisect helps you isolate and debug random test failures.
|
|
11
|
+
|
|
12
|
+
class Minitest::Bisect
|
|
13
|
+
VERSION = "1.8.0" # :nodoc:
|
|
14
|
+
|
|
15
|
+
class PathExpander < Minitest::VendoredPathExpander # :nodoc:
|
|
16
|
+
TEST_GLOB = "**/{test_*,*_test,spec_*,*_spec}.rb" # :nodoc:
|
|
17
|
+
|
|
18
|
+
attr_accessor :rb_flags
|
|
19
|
+
|
|
20
|
+
def initialize args = ARGV # :nodoc:
|
|
21
|
+
super args, TEST_GLOB, "test"
|
|
22
|
+
self.rb_flags = %w[]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
##
|
|
26
|
+
# Overrides PathExpander#process_flags to filter out ruby flags
|
|
27
|
+
# from minitest flags. Only supports -I<paths>, -d, and -w for
|
|
28
|
+
# ruby.
|
|
29
|
+
|
|
30
|
+
def process_flags flags
|
|
31
|
+
flags.reject { |flag| # all hits are truthy, so this works out well
|
|
32
|
+
case flag
|
|
33
|
+
when /^-I(.*)/ then
|
|
34
|
+
rb_flags << flag
|
|
35
|
+
when /^-d/ then
|
|
36
|
+
rb_flags << flag
|
|
37
|
+
when /^-w/ then
|
|
38
|
+
rb_flags << flag
|
|
39
|
+
else
|
|
40
|
+
false
|
|
41
|
+
end
|
|
42
|
+
}
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
mtbv = ENV["MTB_VERBOSE"].to_i
|
|
47
|
+
SHH = case # :nodoc:
|
|
48
|
+
when mtbv == 1 then " > /dev/null"
|
|
49
|
+
when mtbv >= 2 then nil
|
|
50
|
+
else " > /dev/null 2>&1"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Borrowed from rake
|
|
54
|
+
RUBY = ENV['RUBY'] ||
|
|
55
|
+
File.join(RbConfig::CONFIG['bindir'],
|
|
56
|
+
RbConfig::CONFIG['ruby_install_name'] +
|
|
57
|
+
RbConfig::CONFIG['EXEEXT']).sub(/.*\s.*/m, '"\&"')
|
|
58
|
+
|
|
59
|
+
##
|
|
60
|
+
# True if this run has seen a failure.
|
|
61
|
+
|
|
62
|
+
attr_accessor :tainted
|
|
63
|
+
alias :tainted? :tainted
|
|
64
|
+
|
|
65
|
+
##
|
|
66
|
+
# Failures seen in this run. Shape:
|
|
67
|
+
#
|
|
68
|
+
# {"file.rb"=>{"Class"=>["test_method1", "test_method2"] ...} ...}
|
|
69
|
+
|
|
70
|
+
attr_accessor :failures
|
|
71
|
+
|
|
72
|
+
##
|
|
73
|
+
# An array of tests seen so far. NOT cleared by #reset.
|
|
74
|
+
|
|
75
|
+
attr_accessor :culprits
|
|
76
|
+
|
|
77
|
+
attr_accessor :seen_bad # :nodoc:
|
|
78
|
+
|
|
79
|
+
##
|
|
80
|
+
# Top-level runner. Instantiate and call +run+, handling exceptions.
|
|
81
|
+
|
|
82
|
+
def self.run files
|
|
83
|
+
new.run files
|
|
84
|
+
rescue => e
|
|
85
|
+
warn e.message
|
|
86
|
+
warn "Try running with MTB_VERBOSE=2 to verify."
|
|
87
|
+
exit 1
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
##
|
|
91
|
+
# Instantiate a new Bisect.
|
|
92
|
+
|
|
93
|
+
def initialize
|
|
94
|
+
self.culprits = []
|
|
95
|
+
self.failures = Hash.new { |h, k| h[k] = Hash.new { |h2, k2| h2[k2] = [] } }
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
##
|
|
99
|
+
# Reset per-bisect-run variables.
|
|
100
|
+
|
|
101
|
+
def reset
|
|
102
|
+
self.seen_bad = false
|
|
103
|
+
self.tainted = false
|
|
104
|
+
failures.clear
|
|
105
|
+
# not clearing culprits on purpose
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
##
|
|
109
|
+
# Instance-level runner. Handles Minitest::Server, argument
|
|
110
|
+
# processing, and invoking +bisect_methods+.
|
|
111
|
+
|
|
112
|
+
def run args
|
|
113
|
+
Minitest::Server.run self
|
|
114
|
+
|
|
115
|
+
cmd = nil
|
|
116
|
+
|
|
117
|
+
mt_flags = args.dup
|
|
118
|
+
expander = Minitest::Bisect::PathExpander.new mt_flags
|
|
119
|
+
|
|
120
|
+
files = expander.process.to_a
|
|
121
|
+
rb_flags = expander.rb_flags
|
|
122
|
+
mt_flags += ["--server", $$.to_s]
|
|
123
|
+
|
|
124
|
+
cmd = bisect_methods files, rb_flags, mt_flags
|
|
125
|
+
|
|
126
|
+
puts "Final reproduction:"
|
|
127
|
+
puts
|
|
128
|
+
|
|
129
|
+
system({"MINITEST_SERVER" => "1"}, cmd.sub(/--server \d+/, "", ))
|
|
130
|
+
ensure
|
|
131
|
+
Minitest::Server.stop
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
##
|
|
135
|
+
# Normal: find "what is the minimal combination of tests to run to
|
|
136
|
+
# make X fail?"
|
|
137
|
+
#
|
|
138
|
+
# Run with: minitest_bisect ... --seed=N
|
|
139
|
+
#
|
|
140
|
+
# 1. Verify the failure running normally with the seed.
|
|
141
|
+
# 2. If no failure, punt.
|
|
142
|
+
# 3. If no passing tests before failure, punt. (No culprits == no debug)
|
|
143
|
+
# 4. Verify the failure doesn't fail in isolation.
|
|
144
|
+
# 5. If it still fails by itself, warn that it might not be an ordering
|
|
145
|
+
# issue.
|
|
146
|
+
# 6. Cull all tests after the failure, they're not involved.
|
|
147
|
+
# 7. Bisect the culprits + bad until you find a minimal combo that fails.
|
|
148
|
+
# 8. Display minimal combo by running one last time.
|
|
149
|
+
#
|
|
150
|
+
# Inverted: find "what is the minimal combination of tests to run to
|
|
151
|
+
# make this test pass?"
|
|
152
|
+
#
|
|
153
|
+
# Run with: minitest_bisect ... --seed=N -n="/failing_test_name_regexp/"
|
|
154
|
+
#
|
|
155
|
+
# 1. Verify the failure by running normally w/ the seed and -n=/.../
|
|
156
|
+
# 2. If no failure, punt.
|
|
157
|
+
# 3. Verify the passing case by running everything.
|
|
158
|
+
# 4. If failure, punt. This is not a false positive.
|
|
159
|
+
# 5. Cull all tests after the bad test from #1, they're not involved.
|
|
160
|
+
# 6. Bisect the culprits + bad until you find a minimal combo that passes.
|
|
161
|
+
# 7. Display minimal combo by running one last time.
|
|
162
|
+
|
|
163
|
+
def bisect_methods files, rb_flags, mt_flags
|
|
164
|
+
bad_names, mt_flags = mt_flags.partition { |s| s =~ /^(?:-n|--name)/ }
|
|
165
|
+
normal = bad_names.empty?
|
|
166
|
+
inverted = !normal
|
|
167
|
+
|
|
168
|
+
if inverted then
|
|
169
|
+
time_it "reproducing w/ scoped failure (inverted run!)...", build_methods_cmd(build_files_cmd(files, rb_flags, mt_flags + bad_names))
|
|
170
|
+
raise "No failures. Probably not a false positive. Aborting." if failures.empty?
|
|
171
|
+
bad = map_failures
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
cmd = build_files_cmd(files, rb_flags, mt_flags)
|
|
175
|
+
|
|
176
|
+
msg = normal ? "reproducing..." : "reproducing false positive..."
|
|
177
|
+
time_it msg, build_methods_cmd(cmd)
|
|
178
|
+
|
|
179
|
+
if normal then
|
|
180
|
+
raise "Reproduction run passed? Aborting." unless tainted?
|
|
181
|
+
raise "Verification failed. No culprits? Aborting." if culprits.empty? && seen_bad
|
|
182
|
+
else
|
|
183
|
+
raise "Reproduction failed? Not false positive. Aborting." if tainted?
|
|
184
|
+
raise "Verification failed. No culprits? Aborting." if culprits.empty? || seen_bad
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
if normal then
|
|
188
|
+
bad = map_failures
|
|
189
|
+
|
|
190
|
+
time_it "verifying...", build_methods_cmd(cmd, [], bad)
|
|
191
|
+
|
|
192
|
+
new_bad = map_failures
|
|
193
|
+
|
|
194
|
+
if bad == new_bad then
|
|
195
|
+
warn "Tests fail by themselves. This may not be an ordering issue."
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
idx = culprits.index bad.first
|
|
200
|
+
self.culprits = culprits.take idx+1 if idx # cull tests after bad
|
|
201
|
+
|
|
202
|
+
# culprits populated by initial reproduction via minitest/server
|
|
203
|
+
found, count = culprits.find_minimal_combination_and_count do |test|
|
|
204
|
+
prompt = "# of culprit methods: #{test.size}"
|
|
205
|
+
|
|
206
|
+
time_it prompt, build_methods_cmd(cmd, test, bad)
|
|
207
|
+
|
|
208
|
+
normal == tainted? # either normal and failed, or inverse and passed
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
puts
|
|
212
|
+
puts "Minimal methods found in #{count} steps:"
|
|
213
|
+
puts
|
|
214
|
+
puts "Culprit methods: %p" % [found + bad]
|
|
215
|
+
puts
|
|
216
|
+
cmd = build_methods_cmd cmd, found, bad
|
|
217
|
+
puts cmd.sub(/--server \d+/, "")
|
|
218
|
+
puts
|
|
219
|
+
cmd
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def time_it prompt, cmd # :nodoc:
|
|
223
|
+
print prompt
|
|
224
|
+
t0 = Time.now
|
|
225
|
+
system({"MINITEST_SERVER" => "1"}, "#{cmd} #{SHH}")
|
|
226
|
+
puts " in %.2f sec" % (Time.now - t0)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def map_failures # :nodoc:
|
|
230
|
+
# from: {"file.rb"=>{"Class"=>["test_method1", "test_method2"]}}
|
|
231
|
+
# to: ["Class#test_method1", "Class#test_method2"]
|
|
232
|
+
failures.values.map { |h|
|
|
233
|
+
h.map { |k,vs| vs.map { |v| "#{k}##{v}" } }
|
|
234
|
+
}.flatten.sort
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def build_files_cmd culprits, rb, mt, cmd:$0 # :nodoc:
|
|
238
|
+
([cmd] + rb + culprits + mt).shelljoin
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def build_methods_cmd cmd, culprits = [], bad = nil # :nodoc:
|
|
242
|
+
reset
|
|
243
|
+
|
|
244
|
+
if bad then
|
|
245
|
+
re = build_re culprits + bad
|
|
246
|
+
|
|
247
|
+
cmd += " -n \"#{re}\"" if bad
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
if ENV["MTB_VERBOSE"].to_i >= 1 then
|
|
251
|
+
puts
|
|
252
|
+
puts cmd
|
|
253
|
+
puts
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
cmd
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def build_re bad # :nodoc:
|
|
260
|
+
re = []
|
|
261
|
+
|
|
262
|
+
# bad by class, you perv
|
|
263
|
+
bbc = bad.map { |s| s.split(/#/, 2) }.group_by(&:first)
|
|
264
|
+
|
|
265
|
+
bbc.each do |klass, methods|
|
|
266
|
+
methods = methods.map(&:last).flatten.uniq.map { |method|
|
|
267
|
+
re_escape method
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
methods = methods.join "|"
|
|
271
|
+
re << /#{re_escape klass}#(?:#{methods})/.to_s[7..-2] # (?-mix:...)
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
re = re.join("|").to_s.gsub(/-mix/, "")
|
|
275
|
+
|
|
276
|
+
"/^(?:#{re})$/"
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def re_escape str # :nodoc:
|
|
280
|
+
str.gsub(/([`'"!?&\[\]\(\)\{\}\|\+])/, '\\\\\1')
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
############################################################
|
|
284
|
+
# Server Methods:
|
|
285
|
+
|
|
286
|
+
def minitest_start # :nodoc:
|
|
287
|
+
self.failures.clear
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def minitest_result file, klass, method, fails, assertions, time # :nodoc:
|
|
291
|
+
fails.reject! { |fail| Minitest::Skip === fail }
|
|
292
|
+
|
|
293
|
+
if fails.empty? then
|
|
294
|
+
culprits << "#{klass}##{method}" unless seen_bad # UGH
|
|
295
|
+
else
|
|
296
|
+
self.seen_bad = true
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
return if fails.empty?
|
|
300
|
+
|
|
301
|
+
self.tainted = true
|
|
302
|
+
self.failures[file][klass] << method
|
|
303
|
+
end
|
|
304
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#!/usr/bin/env -S ruby
|
|
2
|
+
|
|
3
|
+
# :stopdoc:
|
|
4
|
+
|
|
5
|
+
require "optparse"
|
|
6
|
+
require "shellwords"
|
|
7
|
+
|
|
8
|
+
# complete -o bashdefault -f -C 'ruby lib/minitest/complete.rb' minitest
|
|
9
|
+
# using eg:
|
|
10
|
+
# COMP_LINE="blah test/test_file.rb -n test_pattern"
|
|
11
|
+
# or test directly with:
|
|
12
|
+
# ./lib/minitest/complete.rb test/test_file.rb -n test_pattern
|
|
13
|
+
|
|
14
|
+
argv = Shellwords.split ENV["COMP_LINE"] || ARGV.join(" ")
|
|
15
|
+
comp_re = nil
|
|
16
|
+
|
|
17
|
+
begin
|
|
18
|
+
OptionParser.new do |opts|
|
|
19
|
+
# part of my unofficial embedded gem "makeoptparseworkwell"
|
|
20
|
+
def opts.topdict(name) = (name.length > 1 ? top.long : top.short)
|
|
21
|
+
def opts.alias(from, to) = (dict = topdict(from) ; dict[to] = dict[from])
|
|
22
|
+
|
|
23
|
+
opts.on "-n", "--name [METHOD]", "minitest option" do |m|
|
|
24
|
+
comp_re = Regexp.new m
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
opts.alias "name", "include"
|
|
28
|
+
opts.alias "name", "exclude"
|
|
29
|
+
opts.alias "n", "i"
|
|
30
|
+
opts.alias "n", "e"
|
|
31
|
+
opts.alias "n", "x"
|
|
32
|
+
end.parse! argv
|
|
33
|
+
rescue
|
|
34
|
+
retry # ignore options passed to Ruby
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
path = argv.find_all { |f| File.file? f }.last
|
|
38
|
+
|
|
39
|
+
exit unless comp_re && path
|
|
40
|
+
|
|
41
|
+
require "prism"
|
|
42
|
+
|
|
43
|
+
names, queue = [], [Prism.parse_file(path).value]
|
|
44
|
+
|
|
45
|
+
while node = queue.shift do
|
|
46
|
+
if node.type == :def_node then
|
|
47
|
+
name = node.name
|
|
48
|
+
names << name if name =~ comp_re
|
|
49
|
+
else
|
|
50
|
+
queue.concat node.compact_child_nodes # no need to process def body
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
puts names.sort
|
|
55
|
+
|
|
56
|
+
# :startdoc:
|