ruptr 0.1.3
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 +7 -0
- data/bin/ruptr +10 -0
- data/lib/ruptr/adapters/assertions.rb +43 -0
- data/lib/ruptr/adapters/rr.rb +23 -0
- data/lib/ruptr/adapters/rspec_expect.rb +32 -0
- data/lib/ruptr/adapters/rspec_mocks.rb +29 -0
- data/lib/ruptr/adapters.rb +7 -0
- data/lib/ruptr/assertions.rb +493 -0
- data/lib/ruptr/autorun.rb +38 -0
- data/lib/ruptr/capture_output.rb +106 -0
- data/lib/ruptr/compat.rb +27 -0
- data/lib/ruptr/exceptions.rb +47 -0
- data/lib/ruptr/formatter.rb +78 -0
- data/lib/ruptr/golden_master.rb +143 -0
- data/lib/ruptr/instance.rb +37 -0
- data/lib/ruptr/main.rb +439 -0
- data/lib/ruptr/minitest/override.rb +4 -0
- data/lib/ruptr/minitest.rb +134 -0
- data/lib/ruptr/plain.rb +425 -0
- data/lib/ruptr/progress.rb +98 -0
- data/lib/ruptr/rake_task.rb +18 -0
- data/lib/ruptr/report.rb +104 -0
- data/lib/ruptr/result.rb +38 -0
- data/lib/ruptr/rspec/configuration.rb +191 -0
- data/lib/ruptr/rspec/example_group.rb +498 -0
- data/lib/ruptr/rspec/override.rb +4 -0
- data/lib/ruptr/rspec.rb +211 -0
- data/lib/ruptr/runner.rb +433 -0
- data/lib/ruptr/sink.rb +58 -0
- data/lib/ruptr/stringified.rb +57 -0
- data/lib/ruptr/suite.rb +188 -0
- data/lib/ruptr/surrogate_exception.rb +71 -0
- data/lib/ruptr/tabular.rb +21 -0
- data/lib/ruptr/tap.rb +51 -0
- data/lib/ruptr/testunit/override.rb +4 -0
- data/lib/ruptr/testunit.rb +117 -0
- data/lib/ruptr/timing_cache.rb +100 -0
- data/lib/ruptr/tty_colors.rb +60 -0
- data/lib/ruptr/utils.rb +57 -0
- metadata +77 -0
data/lib/ruptr/plain.rb
ADDED
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'pp'
|
|
4
|
+
begin
|
|
5
|
+
require 'diff/lcs'
|
|
6
|
+
require 'diff/lcs/hunk'
|
|
7
|
+
rescue LoadError
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
require_relative 'formatter'
|
|
11
|
+
require_relative 'suite'
|
|
12
|
+
require_relative 'result'
|
|
13
|
+
require_relative 'tty_colors'
|
|
14
|
+
require_relative 'surrogate_exception'
|
|
15
|
+
require_relative 'stringified'
|
|
16
|
+
require_relative 'assertions'
|
|
17
|
+
|
|
18
|
+
module Ruptr
|
|
19
|
+
class Formatter::Plain < Formatter
|
|
20
|
+
self.formatter_name = :plain
|
|
21
|
+
|
|
22
|
+
include Formatter::Colorizing
|
|
23
|
+
include Formatter::Verbosity
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def initialize(output, indent: "\t", heading_width: 80, unicode: false, **opts)
|
|
28
|
+
super(**opts)
|
|
29
|
+
@output = output
|
|
30
|
+
@indent = indent
|
|
31
|
+
@heading_width = heading_width
|
|
32
|
+
@unicode = unicode
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def write(s) = @output << s
|
|
36
|
+
|
|
37
|
+
def newline = @output << "\n"
|
|
38
|
+
|
|
39
|
+
def line(s) = @output << @line_prefix << s << "\n"
|
|
40
|
+
|
|
41
|
+
def lines(s)
|
|
42
|
+
last_line = nil
|
|
43
|
+
s.each_line do |line|
|
|
44
|
+
@output << @line_prefix << line
|
|
45
|
+
last_line = line
|
|
46
|
+
end
|
|
47
|
+
# NOTE: This can misbehave if there are ANSI terminal codes after the newlines!
|
|
48
|
+
@output << "\n" if last_line && !last_line.end_with?("\n")
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def indent
|
|
52
|
+
if block_given?
|
|
53
|
+
saved = @line_prefix
|
|
54
|
+
@line_prefix += @indent
|
|
55
|
+
begin
|
|
56
|
+
yield
|
|
57
|
+
ensure
|
|
58
|
+
@line_prefix = saved
|
|
59
|
+
end
|
|
60
|
+
else
|
|
61
|
+
@output << @line_prefix
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def location_path_relative_from = @location_path_relative_from ||= Dir.pwd
|
|
66
|
+
|
|
67
|
+
def render_backtrace_location(loc)
|
|
68
|
+
if loc =~ /\A([^:]*)(:.*)\z/ && $1.start_with?((base_path = location_path_relative_from + '/'))
|
|
69
|
+
rel_path = $1[base_path.length..]
|
|
70
|
+
colorize(base_path + colorize(rel_path, bright: true) + $2, color: :cyan)
|
|
71
|
+
else
|
|
72
|
+
colorize(loc, color: :cyan)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def render_exception(heading_prefix, ex)
|
|
77
|
+
line("#{heading_prefix}: " +
|
|
78
|
+
if ex.is_a?(SurrogateException) && ex.original_class_name
|
|
79
|
+
"#{ex.original_class_name} (#{colorize(ex.class.name, color: :magenta)})"
|
|
80
|
+
else
|
|
81
|
+
colorize(ex.class.name, color: case ex
|
|
82
|
+
when SkippedExceptionMixin,
|
|
83
|
+
PendingSkippedMixin then :yellow
|
|
84
|
+
when StandardError then :red
|
|
85
|
+
else :magenta
|
|
86
|
+
end)
|
|
87
|
+
end)
|
|
88
|
+
indent do
|
|
89
|
+
if (msg = if ex.respond_to?(:detailed_message)
|
|
90
|
+
ex.detailed_message(highlight: @colorizer.is_a?(TTYColors::ANSICodes)) || ex.message
|
|
91
|
+
else
|
|
92
|
+
ex.message
|
|
93
|
+
end)
|
|
94
|
+
line("Message:")
|
|
95
|
+
indent do
|
|
96
|
+
lines(msg)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
case ex
|
|
100
|
+
when Assertions::EquivalenceAssertionError
|
|
101
|
+
render_exception_diff(ex.actual, ex.expected)
|
|
102
|
+
when Assertions::EquivalenceRefutationError
|
|
103
|
+
render_exception_value("Actual", ex.actual)
|
|
104
|
+
end
|
|
105
|
+
if ex.backtrace
|
|
106
|
+
line("Backtrace:")
|
|
107
|
+
indent do
|
|
108
|
+
ex.backtrace.each { |loc| line(render_backtrace_location(loc)) }
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def render_stringified_value(label, stringified)
|
|
115
|
+
extra = []
|
|
116
|
+
extra << stringified.original_class_name
|
|
117
|
+
extra << "<#{stringified.string.encoding.name}>" if stringified.originally_a_string?
|
|
118
|
+
m = stringified.stringification_method
|
|
119
|
+
s = stringified.string_for_io(@output)
|
|
120
|
+
unless m || (s.end_with?("\n") && s.match?(/\A[[:print:]\t\n]*\z/))
|
|
121
|
+
s = s.public_send((m = s.count("\n") > 1 ? :pretty_inspect : :inspect))
|
|
122
|
+
end
|
|
123
|
+
extra << "##{m}" if m
|
|
124
|
+
line("#{label} (#{extra.join(' ')}):")
|
|
125
|
+
indent do
|
|
126
|
+
lines(s)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def render_exception_value(label, value)
|
|
131
|
+
render_stringified_value(label, Stringified.from(value))
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def diff_lcs_available?
|
|
135
|
+
Object.const_defined?(:Diff) && Diff.const_defined?(:LCS) && Diff::LCS.const_defined?(:Hunk)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def render_exception_diff(actual, expected)
|
|
139
|
+
actual_stringified = Stringified.from(actual)
|
|
140
|
+
expected_stringified = Stringified.from(expected)
|
|
141
|
+
|
|
142
|
+
can_diff = defined?(Diff::LCS::Hunk) &&
|
|
143
|
+
actual_stringified.compatible_with_io?(@output) &&
|
|
144
|
+
expected_stringified.compatible_with_io?(@output) &&
|
|
145
|
+
begin
|
|
146
|
+
actual_lines = actual_stringified.string.lines
|
|
147
|
+
expected_lines = expected_stringified.string.lines
|
|
148
|
+
actual_lines.size > 1 || expected_lines.size > 1
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
if !can_diff || verbose?(1)
|
|
152
|
+
render_stringified_value("Actual", actual_stringified)
|
|
153
|
+
render_stringified_value("Expected", expected_stringified)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
return unless can_diff
|
|
157
|
+
|
|
158
|
+
diff_colors = if !TTYColors.seems_to_contain_formatting_codes?(actual_stringified.string) &&
|
|
159
|
+
!TTYColors.seems_to_contain_formatting_codes?(expected_stringified.string)
|
|
160
|
+
{ '@' => :cyan, '+' => :green, '-' => :red }
|
|
161
|
+
else
|
|
162
|
+
{}
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
line("Difference:")
|
|
166
|
+
indent do
|
|
167
|
+
render_hunk = lambda do |hunk, last = false|
|
|
168
|
+
hunk.diff(:unified, last).each_line(chomp: true) do |s|
|
|
169
|
+
line(colorize(s, color: diff_colors[s[0]]))
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
offset = 0
|
|
173
|
+
last_hunk = nil
|
|
174
|
+
Diff::LCS.diff(expected_lines, actual_lines).each do |piece|
|
|
175
|
+
hunk = Diff::LCS::Hunk.new(expected_lines, actual_lines, piece, 3, offset)
|
|
176
|
+
offset = hunk.file_length_difference
|
|
177
|
+
render_hunk.call(last_hunk) if last_hunk && !hunk.merge(last_hunk)
|
|
178
|
+
last_hunk = hunk
|
|
179
|
+
end
|
|
180
|
+
render_hunk.call(last_hunk, true) if last_hunk
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
STATUS_COLORS = { passed: :green, skipped: :yellow, failed: :red, blocked: :magenta }
|
|
185
|
+
.tap { |h| h.default = :magenta }.freeze
|
|
186
|
+
|
|
187
|
+
def render_element_details(te)
|
|
188
|
+
labels = te.path_labels.compact
|
|
189
|
+
labels.each_with_index do |label, index|
|
|
190
|
+
if @unicode
|
|
191
|
+
s = +''
|
|
192
|
+
s << ' ' * (index - 1) + '└─' unless index.zero?
|
|
193
|
+
s << (index == labels.size - 1 ? "─" : index == 0 ? '┌' : "┬") << '╼'
|
|
194
|
+
s << " " << label
|
|
195
|
+
else
|
|
196
|
+
s = ' ' * index + label
|
|
197
|
+
end
|
|
198
|
+
line(s)
|
|
199
|
+
end
|
|
200
|
+
newline
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def render_result_details(tr)
|
|
204
|
+
unless quiet?(1)
|
|
205
|
+
line("Status: #{colorize(tr.status.to_s.upcase, color: STATUS_COLORS[tr.status])}")
|
|
206
|
+
a = []
|
|
207
|
+
a << "#{'%0.6f' % tr.user_time} user" if tr.user_time
|
|
208
|
+
a << "#{'%0.6f' % tr.system_time} system" if tr.system_time
|
|
209
|
+
line("Processor time: #{a.join(', ')}") unless a.empty?
|
|
210
|
+
line("Assertion count: #{tr.assertions}") if tr.assertions && !tr.assertions.zero?
|
|
211
|
+
newline
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
space = false
|
|
215
|
+
if tr.exception
|
|
216
|
+
each_exception_cause_innermost_first(tr.exception).with_index do |ex, index|
|
|
217
|
+
render_exception("Exception ##{index + 1}", ex)
|
|
218
|
+
end
|
|
219
|
+
space = true
|
|
220
|
+
elsif tr.failed?
|
|
221
|
+
line("Problem:")
|
|
222
|
+
indent do
|
|
223
|
+
line(colorize("Expected test to fail", color: :red))
|
|
224
|
+
end
|
|
225
|
+
space = true
|
|
226
|
+
end
|
|
227
|
+
newline if space
|
|
228
|
+
|
|
229
|
+
space = false
|
|
230
|
+
if tr.captured_stderr?
|
|
231
|
+
line("Captured stderr:")
|
|
232
|
+
indent do
|
|
233
|
+
tr.captured_stderr.each_line do |s|
|
|
234
|
+
line(colorize(s.chomp, color: :yellow))
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
space = true
|
|
238
|
+
end
|
|
239
|
+
if tr.captured_stdout?
|
|
240
|
+
line("Captured stdout:")
|
|
241
|
+
indent do
|
|
242
|
+
tr.captured_stdout.each_line do |s|
|
|
243
|
+
line(colorize(s.chomp, color: :blue))
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
space = true
|
|
247
|
+
end
|
|
248
|
+
newline if space
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def format_heading(title)
|
|
252
|
+
w = @heading_width
|
|
253
|
+
if @unicode
|
|
254
|
+
["╔#{'═' * (w - 2)}╗", "║#{title.center(w - 2)}║", "╚#{'═' * (w - 2)}╝"]
|
|
255
|
+
else
|
|
256
|
+
b = 4
|
|
257
|
+
['#' * w, "#{'#' * b}#{title.center(w - b * 2)}#{'#' * b}", '#' * w]
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def heading(title, **style)
|
|
262
|
+
format_heading(title).each { |s| line(colorize(s, **style)) }
|
|
263
|
+
@heading_count += 1
|
|
264
|
+
newline unless @unicode
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def render_element(te, tr)
|
|
268
|
+
case
|
|
269
|
+
when tr.passed?
|
|
270
|
+
@total_passed_cases += 1 if te.test_case?
|
|
271
|
+
@total_passed_groups += 1 if te.test_group?
|
|
272
|
+
if tr.captured_stderr?
|
|
273
|
+
@total_passed_warned_cases += 1 if te.test_case?
|
|
274
|
+
@total_passed_warned_groups += 1 if te.test_group?
|
|
275
|
+
return if quiet?(2)
|
|
276
|
+
title = "WARNING ##{@warned_index}"
|
|
277
|
+
@warned_index += 1
|
|
278
|
+
color = :yellow
|
|
279
|
+
else
|
|
280
|
+
return unless verbose?(3)
|
|
281
|
+
title = "PASSED ##{@passed_index}"
|
|
282
|
+
@passed_index += 1
|
|
283
|
+
color = :green
|
|
284
|
+
end
|
|
285
|
+
when tr.skipped?
|
|
286
|
+
@total_skipped_cases += 1 if te.test_case?
|
|
287
|
+
@total_skipped_groups += 1 if te.test_group?
|
|
288
|
+
if tr.exception.is_a?(PendingSkippedMixin)
|
|
289
|
+
@total_skipped_pending_cases += 1 if te.test_case?
|
|
290
|
+
@total_skipped_pending_groups += 1 if te.test_group?
|
|
291
|
+
return unless verbose?(2)
|
|
292
|
+
title = "PENDING ##{@pending_index}"
|
|
293
|
+
@pending_index += 1
|
|
294
|
+
else
|
|
295
|
+
return unless verbose?(2)
|
|
296
|
+
title = "SKIPPED ##{@skipped_index}"
|
|
297
|
+
@skipped_index += 1
|
|
298
|
+
end
|
|
299
|
+
color = :yellow
|
|
300
|
+
when tr.blocked?
|
|
301
|
+
@total_blocked_cases += 1 if te.test_case?
|
|
302
|
+
@total_blocked_groups += 1 if te.test_group?
|
|
303
|
+
return unless verbose?(2)
|
|
304
|
+
title = "BLOCKED ##{@blocked_index}"
|
|
305
|
+
@blocked_index += 1
|
|
306
|
+
color = :magenta
|
|
307
|
+
when tr.failed?
|
|
308
|
+
@total_failed_cases += 1 if te.test_case?
|
|
309
|
+
@total_failed_groups += 1 if te.test_group?
|
|
310
|
+
return if quiet?(2)
|
|
311
|
+
title = "FAILURE ##{@failed_index}"
|
|
312
|
+
@failed_index += 1
|
|
313
|
+
color = :red
|
|
314
|
+
else
|
|
315
|
+
@total_failed_cases += 1 if te.test_case?
|
|
316
|
+
@total_failed_groups += 1 if te.test_group?
|
|
317
|
+
return if quiet?(2)
|
|
318
|
+
title = "UNKNOWN ##{@unknown_index}"
|
|
319
|
+
@unknown_index += 1
|
|
320
|
+
color = :magenta
|
|
321
|
+
end
|
|
322
|
+
heading(title, color:)
|
|
323
|
+
render_element_details(te)
|
|
324
|
+
render_result_details(tr)
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def render_timing(fields)
|
|
328
|
+
return unless verbose?(1)
|
|
329
|
+
a = [
|
|
330
|
+
*["User time", "System time", "Real time"],
|
|
331
|
+
*%i[test_files_load_user_time test_files_load_system_time test_files_load_real_time
|
|
332
|
+
test_suite_run_user_time test_suite_run_system_time test_suite_run_real_time
|
|
333
|
+
overall_user_time overall_system_time overall_real_time].map { |s| fields[s] || Float::NAN },
|
|
334
|
+
]
|
|
335
|
+
lines(format(<<~PRINTF, *a))
|
|
336
|
+
%14s %14s %14s
|
|
337
|
+
Loading test files: %14.6f %14.6f %14.6f
|
|
338
|
+
Running test suite: %14.6f %14.6f %14.6f
|
|
339
|
+
Overall: %14.6f %14.6f %14.6f
|
|
340
|
+
|
|
341
|
+
PRINTF
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
def render_summary
|
|
345
|
+
return if quiet?(3)
|
|
346
|
+
|
|
347
|
+
s = +''
|
|
348
|
+
s << "Ran #{@total_test_cases} test cases"
|
|
349
|
+
s << " with #{@total_assertions} assertions" unless @total_assertions.zero?
|
|
350
|
+
s << ": "
|
|
351
|
+
|
|
352
|
+
s << colorize("#{@total_passed_cases} passed",
|
|
353
|
+
color: if @total_failed_cases.zero? &&
|
|
354
|
+
@total_failed_groups.zero? &&
|
|
355
|
+
!@total_passed_cases.zero?
|
|
356
|
+
:green
|
|
357
|
+
end)
|
|
358
|
+
a = []
|
|
359
|
+
a << colorize("#{@total_passed_warned_cases} warned", color: :yellow) \
|
|
360
|
+
unless @total_passed_warned_cases.zero?
|
|
361
|
+
a << colorize("#{@total_passed_warned_groups} warned test groups", color: :yellow) \
|
|
362
|
+
unless @total_passed_warned_groups.zero?
|
|
363
|
+
s << " (#{a.join(', ')})" unless a.empty?
|
|
364
|
+
|
|
365
|
+
s << ", " << colorize("#{@total_skipped_cases} skipped", color: :yellow) \
|
|
366
|
+
unless @total_skipped_cases.zero?
|
|
367
|
+
a = []
|
|
368
|
+
a << colorize("#{@total_skipped_pending_cases} pending", color: :yellow) \
|
|
369
|
+
unless @total_skipped_pending_cases.zero?
|
|
370
|
+
s << " (#{a.join(', ')})" unless a.empty?
|
|
371
|
+
|
|
372
|
+
s << ", " << colorize("#{@total_failed_cases} failed", color: :red) \
|
|
373
|
+
unless @total_failed_cases.zero?
|
|
374
|
+
|
|
375
|
+
a = []
|
|
376
|
+
a << colorize("#{@total_failed_groups} failed test groups", color: :red) \
|
|
377
|
+
unless @total_failed_groups.zero?
|
|
378
|
+
a << colorize("#{@total_skipped_groups} skipped test groups", color: :yellow) \
|
|
379
|
+
unless @total_skipped_groups.zero?
|
|
380
|
+
a << colorize("#{@total_blocked_groups} blocked test groups", color: :magenta) \
|
|
381
|
+
unless @total_blocked_groups.zero?
|
|
382
|
+
unless @total_blocked_cases.zero? && a.empty?
|
|
383
|
+
s << ", " << colorize("#{@total_blocked_cases} blocked", color: :magenta)
|
|
384
|
+
s << " (#{a.join(', ')})" unless a.empty?
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
line(s << ".")
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
public
|
|
391
|
+
|
|
392
|
+
def begin_plan(_)
|
|
393
|
+
@line_prefix = ''
|
|
394
|
+
@heading_count = 0
|
|
395
|
+
@unknown_index = @failed_index = @blocked_index =
|
|
396
|
+
@skipped_index = @pending_index =
|
|
397
|
+
@passed_index = @warned_index = 1
|
|
398
|
+
@total_test_cases = @total_test_groups = 0
|
|
399
|
+
@total_failed_cases = @total_blocked_cases =
|
|
400
|
+
@total_skipped_cases = @total_skipped_pending_cases =
|
|
401
|
+
@total_passed_cases = @total_passed_warned_cases = 0
|
|
402
|
+
@total_failed_groups = @total_blocked_groups =
|
|
403
|
+
@total_skipped_groups = @total_skipped_pending_groups =
|
|
404
|
+
@total_passed_groups = @total_passed_warned_groups = 0
|
|
405
|
+
@total_assertions = 0
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
def finish_plan(fields)
|
|
409
|
+
unless @heading_count.zero?
|
|
410
|
+
newline
|
|
411
|
+
line('=' * @heading_width)
|
|
412
|
+
newline
|
|
413
|
+
end
|
|
414
|
+
render_timing(fields)
|
|
415
|
+
render_summary
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
def finish_element(te, tr)
|
|
419
|
+
@total_test_cases += 1 if te.test_case?
|
|
420
|
+
@total_test_groups += 1 if te.test_group?
|
|
421
|
+
@total_assertions += tr.assertions
|
|
422
|
+
render_element(te, tr)
|
|
423
|
+
end
|
|
424
|
+
end
|
|
425
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'suite'
|
|
4
|
+
require_relative 'tty_colors'
|
|
5
|
+
|
|
6
|
+
module Ruptr
|
|
7
|
+
class Progress
|
|
8
|
+
include Sink
|
|
9
|
+
|
|
10
|
+
def initialize(output)
|
|
11
|
+
@output = output
|
|
12
|
+
@colorizer = TTYColors.for(output)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def begin_plan(fields)
|
|
16
|
+
@planned_test_case_count = fields[:planned_test_case_count]
|
|
17
|
+
progress_start
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def finish_plan(_) = progress_end
|
|
21
|
+
def finish_element(te, tr) = progress_result(te, tr)
|
|
22
|
+
|
|
23
|
+
class Dots < self
|
|
24
|
+
def progress_result(_te, tr)
|
|
25
|
+
@output << case
|
|
26
|
+
when tr.passed? then @colorizer.wrap('.', color: :green)
|
|
27
|
+
when tr.skipped? then @colorizer.wrap('_', color: :yellow)
|
|
28
|
+
when tr.failed? then @colorizer.wrap('!', color: :red)
|
|
29
|
+
else @colorizer.wrap('?', color: :magenta)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def progress_end
|
|
34
|
+
@output << "\n"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
class StatusLine < self
|
|
39
|
+
def progress_start
|
|
40
|
+
@processor_time = 0
|
|
41
|
+
@test_cases = @assertions = 0
|
|
42
|
+
@passed = @skipped = @failed = @blocked = 0
|
|
43
|
+
@last_line_width = 0
|
|
44
|
+
@twirls = @last_twirl_processor_time = 0
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private def colorize(s, **opts) = @colorizer.wrap(s, **opts)
|
|
48
|
+
|
|
49
|
+
CHARS = %w[\\ | / -].freeze
|
|
50
|
+
|
|
51
|
+
def progress_result(te, tr)
|
|
52
|
+
@processor_time += tr.processor_time || 0
|
|
53
|
+
@assertions += tr.assertions || 0
|
|
54
|
+
if te.test_case?
|
|
55
|
+
@test_cases += 1
|
|
56
|
+
@passed += 1 if tr.passed?
|
|
57
|
+
@skipped += 1 if tr.skipped?
|
|
58
|
+
@failed += 1 if tr.failed?
|
|
59
|
+
@blocked += 1 if tr.blocked?
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
line = +"\r"
|
|
63
|
+
line << "Running tests... "
|
|
64
|
+
if (@processor_time - @last_twirl_processor_time) >= 0.25
|
|
65
|
+
@twirls += 1
|
|
66
|
+
@last_twirl_processor_time = @processor_time
|
|
67
|
+
end
|
|
68
|
+
line << CHARS[@twirls % CHARS.size]
|
|
69
|
+
line << " ptime:" << colorize('%.03fs' % @processor_time, color: :cyan)
|
|
70
|
+
line << " cases:" << colorize(@test_cases.to_s, color: :cyan)
|
|
71
|
+
if (n = @planned_test_case_count)
|
|
72
|
+
line << "/#{n}"
|
|
73
|
+
line << " (#{@test_cases * 100 / n}%)" unless n.zero?
|
|
74
|
+
end
|
|
75
|
+
line << " asserts:" << colorize(@assertions.to_s, color: :cyan) unless @assertions.zero?
|
|
76
|
+
line << " passed:" << colorize(@passed.to_s, color: :green) unless @passed.zero?
|
|
77
|
+
line << " skipped:" << colorize(@skipped.to_s, color: :yellow) unless @skipped.zero?
|
|
78
|
+
line << " failed:" << colorize(@failed.to_s, color: :red) unless @failed.zero?
|
|
79
|
+
line << " blocked:" << colorize(@blocked.to_s, color: :magenta) unless @blocked.zero?
|
|
80
|
+
line << ' '
|
|
81
|
+
width = line.length - 1
|
|
82
|
+
if width < @last_line_width
|
|
83
|
+
n = @last_line_width - width + 1
|
|
84
|
+
@last_line_width = width
|
|
85
|
+
line << ' ' * n << "\b" * n
|
|
86
|
+
else
|
|
87
|
+
@last_line_width = width
|
|
88
|
+
end
|
|
89
|
+
@output << line
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def progress_end
|
|
93
|
+
n = @last_line_width
|
|
94
|
+
@output << ("\b" * n + ' ' * (n + 1) + "\b" * (n + 1))
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rake/tasklib'
|
|
4
|
+
|
|
5
|
+
require_relative 'main'
|
|
6
|
+
|
|
7
|
+
module Ruptr
|
|
8
|
+
class RakeTask < Rake::TaskLib
|
|
9
|
+
def initialize(name = :ruptr, &)
|
|
10
|
+
super()
|
|
11
|
+
task(name) do
|
|
12
|
+
main = Main.new
|
|
13
|
+
instance_exec(main, &) if block_given?
|
|
14
|
+
main.run
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
data/lib/ruptr/report.rb
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'suite'
|
|
4
|
+
require_relative 'result'
|
|
5
|
+
require_relative 'sink'
|
|
6
|
+
|
|
7
|
+
module Ruptr
|
|
8
|
+
class Report
|
|
9
|
+
def initialize
|
|
10
|
+
@results = {}
|
|
11
|
+
@failed = false
|
|
12
|
+
@total_assertions = 0
|
|
13
|
+
@total_test_cases = 0
|
|
14
|
+
@total_test_cases_by_status = Hash.new(0)
|
|
15
|
+
@total_test_groups = 0
|
|
16
|
+
@total_test_groups_by_status = Hash.new(0)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
attr_reader :total_assertions,
|
|
20
|
+
:total_test_cases,
|
|
21
|
+
:total_test_groups
|
|
22
|
+
|
|
23
|
+
def failed? = @failed
|
|
24
|
+
def passed? = !failed?
|
|
25
|
+
|
|
26
|
+
TestResult::VALID_STATUSES.each do |status|
|
|
27
|
+
define_method(:"total_#{status}_test_cases") { @total_test_cases_by_status[status] }
|
|
28
|
+
define_method(:"total_#{status}_test_groups") { @total_test_groups_by_status[status] }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def each_test_element_result(klass = TestElement, &)
|
|
32
|
+
return to_enum __method__, klass unless block_given?
|
|
33
|
+
@results.each { |te, tr| yield te, tr if te.is_a?(klass) }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def each_test_case_result(&) = each_test_element_result(TestCase, &)
|
|
37
|
+
def each_test_group_result(&) = each_test_element_result(TestGroup, &)
|
|
38
|
+
|
|
39
|
+
def [](k) = @results[k]
|
|
40
|
+
|
|
41
|
+
def []=(k, v)
|
|
42
|
+
raise ArgumentError unless k.is_a?(Symbol)
|
|
43
|
+
@results[k] = v
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def bump(k, n = 1)
|
|
47
|
+
raise ArgumentError unless k.is_a?(Symbol)
|
|
48
|
+
@results[k] = (v = @results[k]).nil? ? n : v + n
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def record_result(te, tr)
|
|
52
|
+
raise ArgumentError, "result already recorded" if @results[te]
|
|
53
|
+
case
|
|
54
|
+
when te.test_case?
|
|
55
|
+
@total_test_cases += 1
|
|
56
|
+
@total_test_cases_by_status[tr.status] += 1
|
|
57
|
+
when te.test_group?
|
|
58
|
+
@total_test_groups += 1
|
|
59
|
+
@total_test_groups_by_status[tr.status] += 1
|
|
60
|
+
else
|
|
61
|
+
raise ArgumentError
|
|
62
|
+
end
|
|
63
|
+
@total_assertions += tr.assertions || 0
|
|
64
|
+
@failed ||= tr.failed?
|
|
65
|
+
@results[te] = tr
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def freeze
|
|
69
|
+
@results.freeze
|
|
70
|
+
@total_test_cases_by_status.freeze
|
|
71
|
+
@total_test_groups_by_status.freeze
|
|
72
|
+
super
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def emit(sink)
|
|
76
|
+
sink.begin_plan({ planned_test_case_count: @total_test_cases })
|
|
77
|
+
each_test_group_result { |tg, tr| sink.submit_group(tg, tr) }
|
|
78
|
+
each_test_case_result { |tc, tr| sink.submit_case(tc, tr) }
|
|
79
|
+
sink.finish_plan(@results.filter { |k, _v| k.is_a?(Symbol) })
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
class Builder
|
|
83
|
+
include Sink
|
|
84
|
+
|
|
85
|
+
def initialize(report = Report.new)
|
|
86
|
+
@report = report
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
attr_accessor :report
|
|
90
|
+
|
|
91
|
+
def begin_plan(fields)
|
|
92
|
+
fields.each { |k, v| report[k] = v }
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def finish_plan(fields)
|
|
96
|
+
fields.each { |k, v| report[k] = v }
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def finish_element(te, tr)
|
|
100
|
+
report.record_result(te, tr)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
data/lib/ruptr/result.rb
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'suite'
|
|
4
|
+
require_relative 'sink'
|
|
5
|
+
|
|
6
|
+
module Ruptr
|
|
7
|
+
class TestResult
|
|
8
|
+
# NOTE: Objects of this class will get dumped/loaded with Marshal.
|
|
9
|
+
|
|
10
|
+
VALID_STATUSES = %i[passed skipped failed blocked].freeze
|
|
11
|
+
|
|
12
|
+
def initialize(status,
|
|
13
|
+
assertions: 0, user_time: nil, system_time: nil, exception: nil,
|
|
14
|
+
captured_stdout: nil, captured_stderr: nil)
|
|
15
|
+
raise ArgumentError unless VALID_STATUSES.include?(status)
|
|
16
|
+
raise ArgumentError if exception && status == :passed
|
|
17
|
+
@status = status
|
|
18
|
+
@assertions = assertions
|
|
19
|
+
@captured_stdout = captured_stdout
|
|
20
|
+
@captured_stderr = captured_stderr
|
|
21
|
+
@user_time = user_time
|
|
22
|
+
@system_time = system_time
|
|
23
|
+
@exception = exception
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
attr_reader :status, :assertions, :user_time, :system_time, :exception, :captured_stdout, :captured_stderr
|
|
27
|
+
|
|
28
|
+
def passed? = @status == :passed
|
|
29
|
+
def skipped? = @status == :skipped
|
|
30
|
+
def failed? = @status == :failed
|
|
31
|
+
def blocked? = @status == :blocked
|
|
32
|
+
|
|
33
|
+
def processor_time = !@user_time ? @system_time : !@system_time ? @user_time : @user_time + @system_time
|
|
34
|
+
|
|
35
|
+
def captured_stdout? = @captured_stdout && !@captured_stdout.empty?
|
|
36
|
+
def captured_stderr? = @captured_stderr && !@captured_stderr.empty?
|
|
37
|
+
end
|
|
38
|
+
end
|