minitest 5.12.0 → 5.22.2
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 +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
|