minitest 5.12.0 → 5.22.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/History.rdoc +262 -5
- data/Manifest.txt +3 -0
- data/README.rdoc +90 -17
- data/Rakefile +5 -16
- data/lib/hoe/minitest.rb +0 -4
- data/lib/minitest/assertions.rb +171 -37
- data/lib/minitest/benchmark.rb +7 -7
- data/lib/minitest/compress.rb +94 -0
- data/lib/minitest/expectations.rb +72 -35
- data/lib/minitest/mock.rb +123 -34
- data/lib/minitest/pride_plugin.rb +1 -1
- data/lib/minitest/spec.rb +24 -10
- data/lib/minitest/test.rb +44 -16
- data/lib/minitest/test_task.rb +301 -0
- data/lib/minitest/unit.rb +5 -8
- data/lib/minitest.rb +245 -70
- data/test/minitest/metametameta.rb +45 -11
- data/test/minitest/test_minitest_assertions.rb +357 -29
- data/test/minitest/test_minitest_benchmark.rb +2 -2
- data/test/minitest/test_minitest_mock.rb +287 -15
- data/test/minitest/test_minitest_reporter.rb +158 -17
- data/test/minitest/test_minitest_spec.rb +184 -59
- data/test/minitest/test_minitest_test.rb +362 -42
- data/test/minitest/test_minitest_test_task.rb +46 -0
- data.tar.gz.sig +2 -2
- metadata +28 -19
- metadata.gz.sig +0 -0
data/lib/minitest/assertions.rb
CHANGED
@@ -32,8 +32,6 @@ module Minitest
|
|
32
32
|
@diff = if (RbConfig::CONFIG["host_os"] =~ /mswin|mingw/ &&
|
33
33
|
system("diff.exe", __FILE__, __FILE__)) then
|
34
34
|
"diff.exe -u"
|
35
|
-
elsif Minitest::Test.maglev? then
|
36
|
-
"diff -u"
|
37
35
|
elsif system("gdiff", __FILE__, __FILE__)
|
38
36
|
"gdiff -u" # solaris and kin suck
|
39
37
|
elsif system("diff", __FILE__, __FILE__)
|
@@ -55,25 +53,16 @@ module Minitest
|
|
55
53
|
# diff command or if it doesn't make sense to diff the output
|
56
54
|
# (single line, short output), then it simply returns a basic
|
57
55
|
# comparison between the two.
|
56
|
+
#
|
57
|
+
# See +things_to_diff+ for more info.
|
58
58
|
|
59
59
|
def diff exp, act
|
60
|
-
expect = mu_pp_for_diff exp
|
61
|
-
butwas = mu_pp_for_diff act
|
62
60
|
result = nil
|
63
61
|
|
64
|
-
|
65
|
-
b1, b2 = butwas.include?("\n"), butwas.include?("\\n")
|
66
|
-
|
67
|
-
need_to_diff =
|
68
|
-
(e1 ^ e2 ||
|
69
|
-
b1 ^ b2 ||
|
70
|
-
expect.size > 30 ||
|
71
|
-
butwas.size > 30 ||
|
72
|
-
expect == butwas) &&
|
73
|
-
Minitest::Assertions.diff
|
62
|
+
expect, butwas = things_to_diff(exp, act)
|
74
63
|
|
75
64
|
return "Expected: #{mu_pp exp}\n Actual: #{mu_pp act}" unless
|
76
|
-
|
65
|
+
expect
|
77
66
|
|
78
67
|
Tempfile.open("expect") do |a|
|
79
68
|
a.puts expect
|
@@ -102,6 +91,34 @@ module Minitest
|
|
102
91
|
result
|
103
92
|
end
|
104
93
|
|
94
|
+
##
|
95
|
+
# Returns things to diff [expect, butwas], or [nil, nil] if nothing to diff.
|
96
|
+
#
|
97
|
+
# Criterion:
|
98
|
+
#
|
99
|
+
# 1. Strings include newlines or escaped newlines, but not both.
|
100
|
+
# 2. or: String lengths are > 30 characters.
|
101
|
+
# 3. or: Strings are equal to each other (but maybe different encodings?).
|
102
|
+
# 4. and: we found a diff executable.
|
103
|
+
|
104
|
+
def things_to_diff exp, act
|
105
|
+
expect = mu_pp_for_diff exp
|
106
|
+
butwas = mu_pp_for_diff act
|
107
|
+
|
108
|
+
e1, e2 = expect.include?("\n"), expect.include?("\\n")
|
109
|
+
b1, b2 = butwas.include?("\n"), butwas.include?("\\n")
|
110
|
+
|
111
|
+
need_to_diff =
|
112
|
+
(e1 ^ e2 ||
|
113
|
+
b1 ^ b2 ||
|
114
|
+
expect.size > 30 ||
|
115
|
+
butwas.size > 30 ||
|
116
|
+
expect == butwas) &&
|
117
|
+
Minitest::Assertions.diff
|
118
|
+
|
119
|
+
need_to_diff && [expect, butwas]
|
120
|
+
end
|
121
|
+
|
105
122
|
##
|
106
123
|
# This returns a human-readable version of +obj+. By default
|
107
124
|
# #inspect is called. You can override this to use #pretty_inspect
|
@@ -136,8 +153,8 @@ module Minitest
|
|
136
153
|
str = mu_pp obj
|
137
154
|
|
138
155
|
# both '\n' & '\\n' (_after_ mu_pp (aka inspect))
|
139
|
-
single = str.match
|
140
|
-
double = str.match
|
156
|
+
single = !!str.match(/(?<!\\|^)\\n/)
|
157
|
+
double = !!str.match(/(?<=\\|^)\\n/)
|
141
158
|
|
142
159
|
process =
|
143
160
|
if single ^ double then
|
@@ -181,6 +198,11 @@ module Minitest
|
|
181
198
|
assert obj.empty?, msg
|
182
199
|
end
|
183
200
|
|
201
|
+
def _where # :nodoc:
|
202
|
+
where = Minitest.filter_backtrace(caller).first
|
203
|
+
where = where.split(/:in /, 2).first # clean up noise
|
204
|
+
end
|
205
|
+
|
184
206
|
E = "" # :nodoc:
|
185
207
|
|
186
208
|
##
|
@@ -204,10 +226,7 @@ module Minitest
|
|
204
226
|
if Minitest::VERSION =~ /^6/ then
|
205
227
|
refute_nil exp, "Use assert_nil if expecting nil."
|
206
228
|
else
|
207
|
-
|
208
|
-
where = where.split(/:in /, 2).first # clean up noise
|
209
|
-
|
210
|
-
warn "DEPRECATED: Use assert_nil if expecting nil from #{where}. This will fail in Minitest 6."
|
229
|
+
warn "DEPRECATED: Use assert_nil if expecting nil from #{_where}. This will fail in Minitest 6."
|
211
230
|
end
|
212
231
|
end
|
213
232
|
|
@@ -276,6 +295,8 @@ module Minitest
|
|
276
295
|
assert_respond_to matcher, :"=~"
|
277
296
|
matcher = Regexp.new Regexp.escape matcher if String === matcher
|
278
297
|
assert matcher =~ obj, msg
|
298
|
+
|
299
|
+
Regexp.last_match
|
279
300
|
end
|
280
301
|
|
281
302
|
##
|
@@ -324,6 +345,44 @@ module Minitest
|
|
324
345
|
x = send out_msg, stdout, out, "In stdout" if out_msg
|
325
346
|
|
326
347
|
(!stdout || x) && (!stderr || y)
|
348
|
+
rescue Assertion
|
349
|
+
raise
|
350
|
+
rescue => e
|
351
|
+
raise UnexpectedError, e
|
352
|
+
end
|
353
|
+
|
354
|
+
##
|
355
|
+
# Fails unless +path+ exists.
|
356
|
+
|
357
|
+
def assert_path_exists path, msg = nil
|
358
|
+
msg = message(msg) { "Expected path '#{path}' to exist" }
|
359
|
+
assert File.exist?(path), msg
|
360
|
+
end
|
361
|
+
|
362
|
+
##
|
363
|
+
# For testing with pattern matching (only supported with Ruby 3.0 and later)
|
364
|
+
#
|
365
|
+
# # pass
|
366
|
+
# assert_pattern { [1,2,3] => [Integer, Integer, Integer] }
|
367
|
+
#
|
368
|
+
# # fail "length mismatch (given 3, expected 1)"
|
369
|
+
# assert_pattern { [1,2,3] => [Integer] }
|
370
|
+
#
|
371
|
+
# The bare <tt>=></tt> pattern will raise a NoMatchingPatternError on failure, which would
|
372
|
+
# normally be counted as a test error. This assertion rescues NoMatchingPatternError and
|
373
|
+
# generates a test failure. Any other exception will be raised as normal and generate a test
|
374
|
+
# error.
|
375
|
+
|
376
|
+
def assert_pattern
|
377
|
+
raise NotImplementedError, "only available in Ruby 3.0+" unless RUBY_VERSION >= "3.0"
|
378
|
+
flunk "assert_pattern requires a block to capture errors." unless block_given?
|
379
|
+
|
380
|
+
begin # TODO: remove after ruby 2.6 dropped
|
381
|
+
yield
|
382
|
+
pass
|
383
|
+
rescue NoMatchingPatternError => e
|
384
|
+
flunk e.message
|
385
|
+
end
|
327
386
|
end
|
328
387
|
|
329
388
|
##
|
@@ -346,7 +405,21 @@ module Minitest
|
|
346
405
|
#
|
347
406
|
# +exp+ takes an optional message on the end to help explain
|
348
407
|
# failures and defaults to StandardError if no exception class is
|
349
|
-
# passed.
|
408
|
+
# passed. Eg:
|
409
|
+
#
|
410
|
+
# assert_raises(CustomError) { method_with_custom_error }
|
411
|
+
#
|
412
|
+
# With custom error message:
|
413
|
+
#
|
414
|
+
# assert_raises(CustomError, 'This should have raised CustomError') { method_with_custom_error }
|
415
|
+
#
|
416
|
+
# Using the returned object:
|
417
|
+
#
|
418
|
+
# error = assert_raises(CustomError) do
|
419
|
+
# raise CustomError, 'This is really bad'
|
420
|
+
# end
|
421
|
+
#
|
422
|
+
# assert_equal 'This is really bad', error.message
|
350
423
|
|
351
424
|
def assert_raises *exp
|
352
425
|
flunk "assert_raises requires a block to capture errors." unless
|
@@ -360,7 +433,7 @@ module Minitest
|
|
360
433
|
rescue *exp => e
|
361
434
|
pass # count assertion
|
362
435
|
return e
|
363
|
-
rescue Minitest::Skip
|
436
|
+
rescue Minitest::Assertion # incl Skip & UnexpectedError
|
364
437
|
# don't count assertion
|
365
438
|
raise
|
366
439
|
rescue SignalException, SystemExit
|
@@ -378,12 +451,13 @@ module Minitest
|
|
378
451
|
|
379
452
|
##
|
380
453
|
# Fails unless +obj+ responds to +meth+.
|
454
|
+
# include_all defaults to false to match Object#respond_to?
|
381
455
|
|
382
|
-
def assert_respond_to obj, meth, msg = nil
|
456
|
+
def assert_respond_to obj, meth, msg = nil, include_all: false
|
383
457
|
msg = message(msg) {
|
384
458
|
"Expected #{mu_pp(obj)} (#{obj.class}) to respond to ##{meth}"
|
385
459
|
}
|
386
|
-
assert obj.respond_to?(meth), msg
|
460
|
+
assert obj.respond_to?(meth, include_all), msg
|
387
461
|
end
|
388
462
|
|
389
463
|
##
|
@@ -403,9 +477,7 @@ module Minitest
|
|
403
477
|
# Fails unless the call returns a true value
|
404
478
|
|
405
479
|
def assert_send send_ary, m = nil
|
406
|
-
|
407
|
-
where = where.split(/:in /, 2).first # clean up noise
|
408
|
-
warn "DEPRECATED: assert_send. From #{where}"
|
480
|
+
warn "DEPRECATED: assert_send. From #{_where}"
|
409
481
|
|
410
482
|
recv, msg, *args = send_ary
|
411
483
|
m = message(m) {
|
@@ -430,7 +502,7 @@ module Minitest
|
|
430
502
|
def assert_throws sym, msg = nil
|
431
503
|
default = "Expected #{mu_pp(sym)} to have been thrown"
|
432
504
|
caught = true
|
433
|
-
catch(sym) do
|
505
|
+
value = catch(sym) do
|
434
506
|
begin
|
435
507
|
yield
|
436
508
|
rescue ThreadError => e # wtf?!? 1.8 + threads == suck
|
@@ -446,6 +518,11 @@ module Minitest
|
|
446
518
|
end
|
447
519
|
|
448
520
|
assert caught, message(msg) { default }
|
521
|
+
value
|
522
|
+
rescue Assertion
|
523
|
+
raise
|
524
|
+
rescue => e
|
525
|
+
raise UnexpectedError, e
|
449
526
|
end
|
450
527
|
|
451
528
|
##
|
@@ -514,10 +591,13 @@ module Minitest
|
|
514
591
|
|
515
592
|
return captured_stdout.read, captured_stderr.read
|
516
593
|
ensure
|
517
|
-
captured_stdout.unlink
|
518
|
-
captured_stderr.unlink
|
519
594
|
$stdout.reopen orig_stdout
|
520
595
|
$stderr.reopen orig_stderr
|
596
|
+
|
597
|
+
orig_stdout.close
|
598
|
+
orig_stderr.close
|
599
|
+
captured_stdout.close!
|
600
|
+
captured_stderr.close!
|
521
601
|
end
|
522
602
|
end
|
523
603
|
end
|
@@ -537,7 +617,16 @@ module Minitest
|
|
537
617
|
end
|
538
618
|
|
539
619
|
##
|
540
|
-
# Fails
|
620
|
+
# Fails after a given date (in the local time zone). This allows
|
621
|
+
# you to put time-bombs in your tests if you need to keep
|
622
|
+
# something around until a later date lest you forget about it.
|
623
|
+
|
624
|
+
def fail_after y,m,d,msg
|
625
|
+
flunk msg if Time.now > Time.local(y, m, d)
|
626
|
+
end
|
627
|
+
|
628
|
+
##
|
629
|
+
# Fails with +msg+.
|
541
630
|
|
542
631
|
def flunk msg = nil
|
543
632
|
msg ||= "Epic Fail!"
|
@@ -567,7 +656,7 @@ module Minitest
|
|
567
656
|
|
568
657
|
def refute test, msg = nil
|
569
658
|
msg ||= message { "Expected #{mu_pp(test)} to not be truthy" }
|
570
|
-
|
659
|
+
assert !test, msg
|
571
660
|
end
|
572
661
|
|
573
662
|
##
|
@@ -659,6 +748,30 @@ module Minitest
|
|
659
748
|
refute obj.nil?, msg
|
660
749
|
end
|
661
750
|
|
751
|
+
##
|
752
|
+
# For testing with pattern matching (only supported with Ruby 3.0 and later)
|
753
|
+
#
|
754
|
+
# # pass
|
755
|
+
# refute_pattern { [1,2,3] => [String] }
|
756
|
+
#
|
757
|
+
# # fail "NoMatchingPatternError expected, but nothing was raised."
|
758
|
+
# refute_pattern { [1,2,3] => [Integer, Integer, Integer] }
|
759
|
+
#
|
760
|
+
# This assertion expects a NoMatchingPatternError exception, and will fail if none is raised. Any
|
761
|
+
# other exceptions will be raised as normal and generate a test error.
|
762
|
+
|
763
|
+
def refute_pattern
|
764
|
+
raise NotImplementedError, "only available in Ruby 3.0+" unless RUBY_VERSION >= "3.0"
|
765
|
+
flunk "refute_pattern requires a block to capture errors." unless block_given?
|
766
|
+
|
767
|
+
begin
|
768
|
+
yield
|
769
|
+
flunk("NoMatchingPatternError expected, but nothing was raised.")
|
770
|
+
rescue NoMatchingPatternError
|
771
|
+
pass
|
772
|
+
end
|
773
|
+
end
|
774
|
+
|
662
775
|
##
|
663
776
|
# Fails if +o1+ is not +op+ +o2+. Eg:
|
664
777
|
#
|
@@ -671,6 +784,14 @@ module Minitest
|
|
671
784
|
refute o1.__send__(op, o2), msg
|
672
785
|
end
|
673
786
|
|
787
|
+
##
|
788
|
+
# Fails if +path+ exists.
|
789
|
+
|
790
|
+
def refute_path_exists path, msg = nil
|
791
|
+
msg = message(msg) { "Expected path '#{path}' to not exist" }
|
792
|
+
refute File.exist?(path), msg
|
793
|
+
end
|
794
|
+
|
674
795
|
##
|
675
796
|
# For testing with predicates.
|
676
797
|
#
|
@@ -687,11 +808,12 @@ module Minitest
|
|
687
808
|
|
688
809
|
##
|
689
810
|
# Fails if +obj+ responds to the message +meth+.
|
811
|
+
# include_all defaults to false to match Object#respond_to?
|
690
812
|
|
691
|
-
def refute_respond_to obj, meth, msg = nil
|
813
|
+
def refute_respond_to obj, meth, msg = nil, include_all: false
|
692
814
|
msg = message(msg) { "Expected #{mu_pp(obj)} to not respond to #{meth}" }
|
693
815
|
|
694
|
-
refute obj.respond_to?(meth), msg
|
816
|
+
refute obj.respond_to?(meth, include_all), msg
|
695
817
|
end
|
696
818
|
|
697
819
|
##
|
@@ -710,10 +832,22 @@ module Minitest
|
|
710
832
|
# gets listed at the end of the run but doesn't cause a failure
|
711
833
|
# exit code.
|
712
834
|
|
713
|
-
def skip msg = nil,
|
835
|
+
def skip msg = nil, _ignored = nil
|
714
836
|
msg ||= "Skipped, no message given"
|
715
837
|
@skip = true
|
716
|
-
raise Minitest::Skip, msg
|
838
|
+
raise Minitest::Skip, msg
|
839
|
+
end
|
840
|
+
|
841
|
+
##
|
842
|
+
# Skips the current run until a given date (in the local time
|
843
|
+
# zone). This allows you to put some fixes on hold until a later
|
844
|
+
# date, but still holds you accountable and prevents you from
|
845
|
+
# forgetting it.
|
846
|
+
|
847
|
+
def skip_until y,m,d,msg
|
848
|
+
skip msg if Time.now < Time.local(y, m, d)
|
849
|
+
where = caller.first.rpartition(':in').reject(&:empty?).first
|
850
|
+
warn "Stale skip_until %p at %s" % [msg, where]
|
717
851
|
end
|
718
852
|
|
719
853
|
##
|
data/lib/minitest/benchmark.rb
CHANGED
@@ -109,8 +109,8 @@ module Minitest
|
|
109
109
|
# is applied against the slope itself. As such, you probably want
|
110
110
|
# to tighten it from the default.
|
111
111
|
#
|
112
|
-
# See
|
113
|
-
# more details.
|
112
|
+
# See https://www.graphpad.com/guides/prism/8/curve-fitting/reg_intepretingnonlinr2.htm
|
113
|
+
# for more details.
|
114
114
|
#
|
115
115
|
# Fit is calculated by #fit_linear.
|
116
116
|
#
|
@@ -217,7 +217,7 @@ module Minitest
|
|
217
217
|
##
|
218
218
|
# Takes an array of x/y pairs and calculates the general R^2 value.
|
219
219
|
#
|
220
|
-
# See:
|
220
|
+
# See: https://en.wikipedia.org/wiki/Coefficient_of_determination
|
221
221
|
|
222
222
|
def fit_error xys
|
223
223
|
y_bar = sigma(xys) { |_, y| y } / xys.size.to_f
|
@@ -232,7 +232,7 @@ module Minitest
|
|
232
232
|
#
|
233
233
|
# Takes x and y values and returns [a, b, r^2].
|
234
234
|
#
|
235
|
-
# See:
|
235
|
+
# See: https://mathworld.wolfram.com/LeastSquaresFittingExponential.html
|
236
236
|
|
237
237
|
def fit_exponential xs, ys
|
238
238
|
n = xs.size
|
@@ -254,7 +254,7 @@ module Minitest
|
|
254
254
|
#
|
255
255
|
# Takes x and y values and returns [a, b, r^2].
|
256
256
|
#
|
257
|
-
# See:
|
257
|
+
# See: https://mathworld.wolfram.com/LeastSquaresFittingLogarithmic.html
|
258
258
|
|
259
259
|
def fit_logarithmic xs, ys
|
260
260
|
n = xs.size
|
@@ -276,7 +276,7 @@ module Minitest
|
|
276
276
|
#
|
277
277
|
# Takes x and y values and returns [a, b, r^2].
|
278
278
|
#
|
279
|
-
# See:
|
279
|
+
# See: https://mathworld.wolfram.com/LeastSquaresFitting.html
|
280
280
|
|
281
281
|
def fit_linear xs, ys
|
282
282
|
n = xs.size
|
@@ -298,7 +298,7 @@ module Minitest
|
|
298
298
|
#
|
299
299
|
# Takes x and y values and returns [a, b, r^2].
|
300
300
|
#
|
301
|
-
# See:
|
301
|
+
# See: https://mathworld.wolfram.com/LeastSquaresFittingPowerLaw.html
|
302
302
|
|
303
303
|
def fit_power xs, ys
|
304
304
|
n = xs.size
|
@@ -0,0 +1,94 @@
|
|
1
|
+
module Minitest
|
2
|
+
##
|
3
|
+
# Compresses backtraces.
|
4
|
+
|
5
|
+
module Compress
|
6
|
+
|
7
|
+
##
|
8
|
+
# Takes a backtrace (array of strings) and compresses repeating
|
9
|
+
# cycles in it to make it more readable.
|
10
|
+
|
11
|
+
def compress orig
|
12
|
+
ary = orig
|
13
|
+
|
14
|
+
eswo = ->(ary, n, off) { # each_slice_with_offset
|
15
|
+
if off.zero? then
|
16
|
+
ary.each_slice n
|
17
|
+
else
|
18
|
+
# [ ...off... [...n...] [...n...] ... ]
|
19
|
+
front, back = ary.take(off), ary.drop(off)
|
20
|
+
[front].chain back.each_slice n
|
21
|
+
end
|
22
|
+
}
|
23
|
+
|
24
|
+
3.times do # maybe don't use loop do here?
|
25
|
+
index = ary # [ a b c b c b c d ]
|
26
|
+
.size
|
27
|
+
.times # 0...size
|
28
|
+
.group_by { |i| ary[i] } # { a: [0] b: [1 3 5], c: [2 4 6], d: [7] }
|
29
|
+
|
30
|
+
order = index
|
31
|
+
.reject { |k, v| v.size == 1 } # { b: [1 3 5], c: [2 4 6] }
|
32
|
+
.sort_by { |k, ary| ### sort by max dist + min offset
|
33
|
+
d = ary.each_cons(2).sum { |a, b| b-a }
|
34
|
+
[-d, ary.first]
|
35
|
+
} # b: [1 3 5] c: [2 4 6]
|
36
|
+
|
37
|
+
ranges = order
|
38
|
+
.map { |k, ary| # [[1..2 3..4] [2..3 4..5]]
|
39
|
+
ary
|
40
|
+
.each_cons(2)
|
41
|
+
.map { |a, b| a..b-1 }
|
42
|
+
}
|
43
|
+
|
44
|
+
big_ranges = ranges
|
45
|
+
.flat_map { |a| # [1..2 3..4 2..3 4..5]
|
46
|
+
a.sort_by { |r| [-r.size, r.first] }.first 5
|
47
|
+
}
|
48
|
+
.first(100)
|
49
|
+
|
50
|
+
culprits = big_ranges
|
51
|
+
.map { |r|
|
52
|
+
eswo[ary, r.size, r.begin] # [o1 s1 s1 s2 s2]
|
53
|
+
.chunk_while { |a,b| a == b } # [[o1] [s1 s1] [s2 s2]]
|
54
|
+
.map { |a| [a.size, a.first] } # [[1 o1] [2 s1] [2 s2]]
|
55
|
+
}
|
56
|
+
.select { |chunks|
|
57
|
+
chunks.any? { |a| a.first > 1 } # compressed anything?
|
58
|
+
}
|
59
|
+
|
60
|
+
min = culprits
|
61
|
+
.min_by { |a| a.flatten.size } # most compressed
|
62
|
+
|
63
|
+
break unless min
|
64
|
+
|
65
|
+
ary = min.flat_map { |(n, lines)|
|
66
|
+
if n > 1 then
|
67
|
+
[[n, compress(lines)]] # [o1 [2 s1] [2 s2]]
|
68
|
+
else
|
69
|
+
lines
|
70
|
+
end
|
71
|
+
}
|
72
|
+
end
|
73
|
+
|
74
|
+
format = ->(lines) {
|
75
|
+
lines.flat_map { |line|
|
76
|
+
case line
|
77
|
+
when Array then
|
78
|
+
n, lines = line
|
79
|
+
lines = format[lines]
|
80
|
+
[
|
81
|
+
" +->> #{n} cycles of #{lines.size} lines:",
|
82
|
+
*lines.map { |s| " | #{s}" },
|
83
|
+
" +-<<",
|
84
|
+
]
|
85
|
+
else
|
86
|
+
line
|
87
|
+
end
|
88
|
+
}
|
89
|
+
}
|
90
|
+
|
91
|
+
format[ary]
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|