megatest 0.1.0
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/CHANGELOG.md +5 -0
- data/README.md +156 -0
- data/TODO.md +17 -0
- data/exe/megatest +7 -0
- data/lib/megatest/assertions.rb +474 -0
- data/lib/megatest/backtrace.rb +70 -0
- data/lib/megatest/cli.rb +249 -0
- data/lib/megatest/compat.rb +74 -0
- data/lib/megatest/config.rb +281 -0
- data/lib/megatest/differ.rb +136 -0
- data/lib/megatest/dsl.rb +164 -0
- data/lib/megatest/executor.rb +104 -0
- data/lib/megatest/multi_process.rb +263 -0
- data/lib/megatest/output.rb +158 -0
- data/lib/megatest/patience_diff.rb +340 -0
- data/lib/megatest/pretty_print.rb +309 -0
- data/lib/megatest/queue.rb +239 -0
- data/lib/megatest/queue_monitor.rb +35 -0
- data/lib/megatest/queue_reporter.rb +42 -0
- data/lib/megatest/redis_queue.rb +459 -0
- data/lib/megatest/reporters.rb +266 -0
- data/lib/megatest/runner.rb +119 -0
- data/lib/megatest/runtime.rb +168 -0
- data/lib/megatest/selector.rb +293 -0
- data/lib/megatest/state.rb +708 -0
- data/lib/megatest/subprocess/main.rb +8 -0
- data/lib/megatest/subprocess.rb +48 -0
- data/lib/megatest/test.rb +115 -0
- data/lib/megatest/test_task.rb +132 -0
- data/lib/megatest/version.rb +5 -0
- data/lib/megatest.rb +123 -0
- metadata +80 -0
|
@@ -0,0 +1,708 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# :stopdoc:
|
|
4
|
+
|
|
5
|
+
module Megatest
|
|
6
|
+
@registry = nil
|
|
7
|
+
|
|
8
|
+
class << self
|
|
9
|
+
def registry
|
|
10
|
+
raise Error, "Can't define tests without a registry set" unless @registry
|
|
11
|
+
|
|
12
|
+
@registry
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def with_registry(registry = Registry.new)
|
|
16
|
+
@registry = registry
|
|
17
|
+
begin
|
|
18
|
+
yield
|
|
19
|
+
ensure
|
|
20
|
+
@registry = nil
|
|
21
|
+
end
|
|
22
|
+
registry
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
module State
|
|
27
|
+
using Compat::Name unless Symbol.method_defined?(:name)
|
|
28
|
+
using Compat::StartWith unless Symbol.method_defined?(:start_with?)
|
|
29
|
+
|
|
30
|
+
class Suite
|
|
31
|
+
attr_reader :setup_callback, :teardown_callback, :around_callback
|
|
32
|
+
|
|
33
|
+
def initialize(registry)
|
|
34
|
+
@registry = registry
|
|
35
|
+
@tags = nil
|
|
36
|
+
@setup_callback = nil
|
|
37
|
+
@teardown_callback = nil
|
|
38
|
+
@around_callback = nil
|
|
39
|
+
@current_context = nil
|
|
40
|
+
@current_tags = nil
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def with_context(context, tags)
|
|
44
|
+
previous_context = @current_context
|
|
45
|
+
@current_context = [@current_context, context].compact.join(" ")
|
|
46
|
+
|
|
47
|
+
previous_tags = @current_tags
|
|
48
|
+
if tags
|
|
49
|
+
@current_tags = @current_tags ? @current_tags.merge(tags) : tags
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
begin
|
|
53
|
+
yield
|
|
54
|
+
ensure
|
|
55
|
+
@current_context = previous_context
|
|
56
|
+
@current_tags = previous_tags
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def add_tags(tags)
|
|
61
|
+
return if tags.empty?
|
|
62
|
+
|
|
63
|
+
@tags ||= {}
|
|
64
|
+
@tags.merge!(tags)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def tag?(name)
|
|
68
|
+
@tags&.key?(name)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def own_tags
|
|
72
|
+
@tags
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def build_test_case(name, callable, tags, source_location)
|
|
76
|
+
name = [*@current_context, name].join(" ")
|
|
77
|
+
tags = if tags
|
|
78
|
+
@current_tags ? @current_tags.merge(tags) : tags
|
|
79
|
+
else
|
|
80
|
+
@current_tags
|
|
81
|
+
end
|
|
82
|
+
if callable.is_a?(UnboundMethod)
|
|
83
|
+
MethodTest.new(self, @klass, name, callable, tags, source_location)
|
|
84
|
+
else
|
|
85
|
+
BlockTest.new(self, @klass, name, callable, tags, source_location)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def on_setup(block)
|
|
90
|
+
raise Error, "The setup block is already defined" if @setup_callback
|
|
91
|
+
raise Error, "setup blocks can't be defined in context blocks" if @current_context
|
|
92
|
+
|
|
93
|
+
@setup_callback = block
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def on_around(block)
|
|
97
|
+
raise Error, "The around block is already defined" if @around_callback
|
|
98
|
+
raise Error, "around blocks can't be defined in context blocks" if @current_context
|
|
99
|
+
|
|
100
|
+
@around_callback = block
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def on_teardown(block)
|
|
104
|
+
raise Error, "The teardown block is already defined" if @teardown_callback
|
|
105
|
+
raise Error, "teardown blocks can't be defined in context blocks" if @current_context
|
|
106
|
+
|
|
107
|
+
@teardown_callback = block
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# A test suite is a group of tests. It's a class that inherits Megatest::Test
|
|
112
|
+
# A test case is the smaller runable unit, it's a block defined with `test`
|
|
113
|
+
# or a method with a name starting with `test_`.
|
|
114
|
+
class TestSuite < Suite
|
|
115
|
+
attr_reader :klass, :source_file, :source_line
|
|
116
|
+
|
|
117
|
+
def initialize(registry, test_suite, location)
|
|
118
|
+
super(registry)
|
|
119
|
+
@klass = test_suite
|
|
120
|
+
@source_file, @source_line = location
|
|
121
|
+
@ancestors = nil
|
|
122
|
+
@test_cases = if test_suite.is_a?(Class) && test_suite.superclass < ::Megatest::Test
|
|
123
|
+
registry.suite(test_suite.superclass).test_cases.to_h do |t|
|
|
124
|
+
test = t.inherited_by(self)
|
|
125
|
+
[test, test]
|
|
126
|
+
end
|
|
127
|
+
else
|
|
128
|
+
{}
|
|
129
|
+
end
|
|
130
|
+
@test_cases.each_key do |test|
|
|
131
|
+
@registry.register_test_case(test)
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def tags
|
|
136
|
+
tags = {}
|
|
137
|
+
tags.merge!(*ancestors.reverse.map(&:own_tags).compact)
|
|
138
|
+
tags.merge!(@tags) if @tags
|
|
139
|
+
tags
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def tag(name)
|
|
143
|
+
if @tags&.key?(name)
|
|
144
|
+
@tags[name]
|
|
145
|
+
else
|
|
146
|
+
ancestors.each do |ancestor|
|
|
147
|
+
return ancestor.tag(name) if ancestor.tag?(name)
|
|
148
|
+
end
|
|
149
|
+
nil
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def shared?
|
|
154
|
+
false
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def ancestors
|
|
158
|
+
@ancestors ||= @registry.ancestors(@klass)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def test_cases
|
|
162
|
+
@test_cases.keys
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def register_test_case(name, callable, tags)
|
|
166
|
+
source_location = callable.source_location
|
|
167
|
+
if !shared? && source_location[0] != @source_file
|
|
168
|
+
# When a test class is reopened from a different file, or when a test is defined
|
|
169
|
+
# using some sort of class method macro, the resulting `source_file` doesn't match
|
|
170
|
+
# the test suite, hence can't be used to point to the test as it would
|
|
171
|
+
# have a `source_file` that can't be used to run a single test file.
|
|
172
|
+
#
|
|
173
|
+
# So we need some work to try to figure out the actual test definition location,
|
|
174
|
+
# and if we really can't, then we fallback to the suite location.
|
|
175
|
+
source_location = fixed_source_location || [@source_file, @source_line]
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
test = build_test_case(name, callable, tags, source_location)
|
|
179
|
+
add_test(test)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
if Thread.respond_to?(:each_caller_location)
|
|
183
|
+
def fixed_source_location
|
|
184
|
+
Thread.each_caller_location do |location|
|
|
185
|
+
if location.path == @source_file
|
|
186
|
+
return [location.path, location.lineno]
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
nil
|
|
190
|
+
end
|
|
191
|
+
else
|
|
192
|
+
def fixed_source_location
|
|
193
|
+
caller_locations.each do |location|
|
|
194
|
+
if location.path == @source_file
|
|
195
|
+
return [location.path, location.lineno]
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
nil
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def add_test(test)
|
|
203
|
+
if duplicate = @test_cases[test]
|
|
204
|
+
# It was late defined in an parent class we can just ignore it.
|
|
205
|
+
return test if test.inherited?
|
|
206
|
+
|
|
207
|
+
if duplicate.inherited?
|
|
208
|
+
@test_cases.delete(duplicate)
|
|
209
|
+
@registry.remove_test_case(duplicate)
|
|
210
|
+
else
|
|
211
|
+
# If the pre-existing test wasn't inherited, it means we're defining the
|
|
212
|
+
# same test twice, that's a mistake.
|
|
213
|
+
raise AlreadyDefinedError,
|
|
214
|
+
"`#{test.id}` already defined at #{Megatest.relative_path(test.source_file)}:#{test.source_line}"
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
@test_cases[test] = test
|
|
219
|
+
@registry.register_test_case(test)
|
|
220
|
+
test
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def inherit_test_case(test_case)
|
|
224
|
+
add_test(test_case.inherited_by(self))
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def include_test_case(test_case, include_location)
|
|
228
|
+
add_test(test_case.included_by(self, include_location))
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
class SharedSuite < Suite
|
|
233
|
+
def initialize(registry, test_suite)
|
|
234
|
+
super(registry)
|
|
235
|
+
@mod = test_suite
|
|
236
|
+
@test_cases = {}
|
|
237
|
+
test_suite.instance_methods.each do |name|
|
|
238
|
+
if name.start_with?("test_")
|
|
239
|
+
register_test_case(name, test_suite.instance_method(name), nil)
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def shared?
|
|
245
|
+
true
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def included_by(klass_or_module, include_location)
|
|
249
|
+
if klass_or_module.is_a?(Class)
|
|
250
|
+
suite = @registry.suite(klass_or_module)
|
|
251
|
+
@test_cases.each_key do |test_case|
|
|
252
|
+
suite.include_test_case(test_case, include_location)
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def register_test_case(name, callable, tags)
|
|
258
|
+
test = build_test_case(name, callable, tags, callable.source_location)
|
|
259
|
+
|
|
260
|
+
if @test_cases[test]
|
|
261
|
+
raise AlreadyDefinedError,
|
|
262
|
+
"`#{test.id}` already defined at #{Megatest.relative_path(test.source_file)}:#{test.source_line}"
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
@test_cases[test] = test
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# :startdoc:
|
|
270
|
+
|
|
271
|
+
class Test
|
|
272
|
+
attr_reader :klass, :name, :source_file, :source_line
|
|
273
|
+
|
|
274
|
+
# :stopdoc:
|
|
275
|
+
attr_accessor :index
|
|
276
|
+
|
|
277
|
+
def initialize(test_suite, klass, name, callable, tags, location)
|
|
278
|
+
@test_suite = test_suite
|
|
279
|
+
@klass = klass
|
|
280
|
+
@name = name
|
|
281
|
+
@callable = callable
|
|
282
|
+
@source_file, @source_line = location
|
|
283
|
+
@id = nil
|
|
284
|
+
@index = nil
|
|
285
|
+
@inherited = false
|
|
286
|
+
@tags = tags
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# :startdoc:
|
|
290
|
+
|
|
291
|
+
##
|
|
292
|
+
# Returns a unique identifier string for that test, in the form of `klass#name`
|
|
293
|
+
def id
|
|
294
|
+
if klass.name
|
|
295
|
+
@id ||= "#{klass.name}##{name}"
|
|
296
|
+
else
|
|
297
|
+
"#{klass}##{name}"
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
##
|
|
302
|
+
# Lookup a tag for that test. Returns +nil+ if the tag isn't set.
|
|
303
|
+
def tag(name)
|
|
304
|
+
if @tags&.key?(name)
|
|
305
|
+
@tags[name]
|
|
306
|
+
else
|
|
307
|
+
@test_suite.tag(name)
|
|
308
|
+
end
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
# :stopdoc:
|
|
312
|
+
|
|
313
|
+
def inspect
|
|
314
|
+
if klass.name
|
|
315
|
+
"#<#{self.class}: #{id} @ #{location_id}>"
|
|
316
|
+
else
|
|
317
|
+
"#<#{self.class}: #{klass.inspect}##{name} @ #{location_id}>"
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def tags
|
|
322
|
+
@test_suite.tags.merge(@tags || {})
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def location_id
|
|
326
|
+
if @index
|
|
327
|
+
"#{@source_file}:#{@source_line}~#{@index}"
|
|
328
|
+
else
|
|
329
|
+
"#{@source_file}:#{@source_line}"
|
|
330
|
+
end
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
def inherited?
|
|
334
|
+
@inherited
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
def inherited_by(test_suite)
|
|
338
|
+
copy = dup
|
|
339
|
+
copy.test_suite = test_suite
|
|
340
|
+
copy.source_file = test_suite.source_file
|
|
341
|
+
copy.source_line = test_suite.source_line
|
|
342
|
+
copy.inherited = true
|
|
343
|
+
copy
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
def included_by(test_suite, include_location)
|
|
347
|
+
copy = dup
|
|
348
|
+
copy.test_suite = test_suite
|
|
349
|
+
copy.source_file, copy.source_line = include_location
|
|
350
|
+
copy.inherited = true
|
|
351
|
+
copy
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
def ==(other)
|
|
355
|
+
other.is_a?(Test) &&
|
|
356
|
+
@klass == other.klass &&
|
|
357
|
+
@name == other.name
|
|
358
|
+
end
|
|
359
|
+
alias_method :eql?, :==
|
|
360
|
+
|
|
361
|
+
def hash
|
|
362
|
+
[Test, @klass, @name].hash
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
def <=>(other)
|
|
366
|
+
cmp = @klass.name <=> other.klass.name
|
|
367
|
+
cmp = @name <=> other.name if cmp&.zero?
|
|
368
|
+
cmp || 0
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
def each_setup_callback
|
|
372
|
+
@test_suite.ancestors.reverse_each do |test_suite|
|
|
373
|
+
yield test_suite.setup_callback if test_suite.setup_callback
|
|
374
|
+
end
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
using Compat::FilterMap unless Enumerable.method_defined?(:filter_map)
|
|
378
|
+
|
|
379
|
+
def around_callbacks
|
|
380
|
+
@test_suite.ancestors.filter_map(&:around_callback)
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
def each_teardown_callback
|
|
384
|
+
@test_suite.ancestors.each do |test_suite|
|
|
385
|
+
yield test_suite.teardown_callback if test_suite.teardown_callback
|
|
386
|
+
end
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
protected
|
|
390
|
+
|
|
391
|
+
attr_writer :inherited, :source_file, :source_line
|
|
392
|
+
|
|
393
|
+
def test_suite=(test_suite)
|
|
394
|
+
@id = nil
|
|
395
|
+
@test_suite = test_suite
|
|
396
|
+
@klass = test_suite.klass
|
|
397
|
+
end
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
# :stopdoc:
|
|
401
|
+
|
|
402
|
+
class BlockTest < Test
|
|
403
|
+
def execute(runtime, instance)
|
|
404
|
+
runtime.record_failures(downlevel: 2) { instance.instance_exec(&@callable) }
|
|
405
|
+
end
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
class MethodTest < Test
|
|
409
|
+
if UnboundMethod.method_defined?(:bind_call)
|
|
410
|
+
def execute(runtime, instance)
|
|
411
|
+
runtime.record_failures(downlevel: 2) { @callable.bind_call(instance) }
|
|
412
|
+
end
|
|
413
|
+
else
|
|
414
|
+
using Compat::BindCall
|
|
415
|
+
|
|
416
|
+
def execute(runtime, instance)
|
|
417
|
+
runtime.record_failures(downlevel: 3) { @callable.bind_call(instance) }
|
|
418
|
+
end
|
|
419
|
+
end
|
|
420
|
+
end
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
class Registry
|
|
424
|
+
def initialize
|
|
425
|
+
@test_suites = {}
|
|
426
|
+
@shared_suites = {}
|
|
427
|
+
@test_cases_by_location = {}
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
def shared_suite(test_suite)
|
|
431
|
+
@shared_suites[test_suite] ||= State::SharedSuite.new(self, test_suite)
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
def suite(klass)
|
|
435
|
+
@shared_suites[klass] || @test_suites.fetch(klass)
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
def ancestors(klass)
|
|
439
|
+
suites = []
|
|
440
|
+
klass.ancestors.each do |mod|
|
|
441
|
+
suite = @shared_suites[mod] || @test_suites[mod]
|
|
442
|
+
suites << suite if suite
|
|
443
|
+
|
|
444
|
+
break if mod == ::Megatest::Test
|
|
445
|
+
end
|
|
446
|
+
suites
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
if Class.method_defined?(:subclasses)
|
|
450
|
+
def register_suite(test_suite, location)
|
|
451
|
+
@test_suites[test_suite] ||= State::TestSuite.new(self, test_suite, location)
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
def each_subclass_of(klass, &block)
|
|
455
|
+
klass.subclasses.each(&block)
|
|
456
|
+
end
|
|
457
|
+
else
|
|
458
|
+
def register_suite(test_suite, location)
|
|
459
|
+
@test_suites[test_suite] ||= begin
|
|
460
|
+
if test_suite.is_a?(Class)
|
|
461
|
+
@subclasses ||= {}
|
|
462
|
+
(@subclasses[test_suite.superclass] ||= []) << test_suite
|
|
463
|
+
end
|
|
464
|
+
State::TestSuite.new(self, test_suite, location)
|
|
465
|
+
end
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
def each_subclass_of(klass, &block)
|
|
469
|
+
@subclasses[klass]&.each(&block)
|
|
470
|
+
end
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
def register_test_case(test_case)
|
|
474
|
+
path_index = @test_cases_by_location[test_case.source_file] ||= {}
|
|
475
|
+
line_tests = path_index[test_case.source_line] ||= []
|
|
476
|
+
|
|
477
|
+
unless line_tests.empty?
|
|
478
|
+
test_case.index = line_tests.size
|
|
479
|
+
if line_tests.size == 1
|
|
480
|
+
line_tests.first.index = 0
|
|
481
|
+
end
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
line_tests << test_case
|
|
485
|
+
|
|
486
|
+
each_subclass_of(test_case.klass) do |subclass|
|
|
487
|
+
suite(subclass).inherit_test_case(test_case)
|
|
488
|
+
end
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
def remove_test_case(test_case)
|
|
492
|
+
path_index = @test_cases_by_location[test_case.source_file]
|
|
493
|
+
line_tests = path_index[test_case.source_line]
|
|
494
|
+
remove_index = line_tests.index(test_case)
|
|
495
|
+
line_tests.delete_at(remove_index)
|
|
496
|
+
case line_tests.size
|
|
497
|
+
when 0
|
|
498
|
+
# noop
|
|
499
|
+
when 1
|
|
500
|
+
line_tests[0].index = nil
|
|
501
|
+
else
|
|
502
|
+
remove_index.upto(line_tests.size - 1) do |index|
|
|
503
|
+
line_tests[index].index -= 1
|
|
504
|
+
end
|
|
505
|
+
end
|
|
506
|
+
test_cases
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
def test_suites
|
|
510
|
+
@test_suites.values
|
|
511
|
+
end
|
|
512
|
+
|
|
513
|
+
def test_cases
|
|
514
|
+
@test_suites.flat_map do |_klass, suite|
|
|
515
|
+
suite.test_cases
|
|
516
|
+
end
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
def test_cases_by_path(path = nil)
|
|
520
|
+
if path
|
|
521
|
+
if index = @test_cases_by_location[path]
|
|
522
|
+
index.values.flatten
|
|
523
|
+
else
|
|
524
|
+
[]
|
|
525
|
+
end
|
|
526
|
+
else
|
|
527
|
+
@test_cases_by_location.transform_values do |line_index|
|
|
528
|
+
line_index.flat_map do |_line, test_cases|
|
|
529
|
+
test_cases
|
|
530
|
+
end
|
|
531
|
+
end
|
|
532
|
+
end
|
|
533
|
+
end
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
class Failure
|
|
537
|
+
attr_reader :name, :message, :backtrace, :cause
|
|
538
|
+
|
|
539
|
+
class << self
|
|
540
|
+
def load(members)
|
|
541
|
+
allocate._load(members)
|
|
542
|
+
end
|
|
543
|
+
end
|
|
544
|
+
|
|
545
|
+
if Exception.method_defined?(:detailed_message)
|
|
546
|
+
def initialize(exception)
|
|
547
|
+
@name = exception.class.name
|
|
548
|
+
@message = exception.detailed_message.sub(" (#{@name})", "")
|
|
549
|
+
@backtrace = exception.backtrace
|
|
550
|
+
@cause = exception.cause ? Failure.new(exception.cause) : nil
|
|
551
|
+
end
|
|
552
|
+
else
|
|
553
|
+
def initialize(exception)
|
|
554
|
+
@name = exception.class.name
|
|
555
|
+
@message = exception.message
|
|
556
|
+
@backtrace = exception.backtrace
|
|
557
|
+
@cause = exception.cause ? Failure.new(exception.cause) : nil
|
|
558
|
+
end
|
|
559
|
+
end
|
|
560
|
+
|
|
561
|
+
def _load(members)
|
|
562
|
+
@name = members[0]
|
|
563
|
+
@message = members[1]
|
|
564
|
+
@backtrace = members[2]
|
|
565
|
+
@cause = members[3] && Failure.load(members[3])
|
|
566
|
+
self
|
|
567
|
+
end
|
|
568
|
+
|
|
569
|
+
def dump
|
|
570
|
+
[@name, @message, @backtrace, @cause&.dump].compact
|
|
571
|
+
end
|
|
572
|
+
end
|
|
573
|
+
|
|
574
|
+
class TestCaseResult
|
|
575
|
+
class << self
|
|
576
|
+
def load(payload)
|
|
577
|
+
allocate._load(Marshal.load(payload))
|
|
578
|
+
end
|
|
579
|
+
end
|
|
580
|
+
|
|
581
|
+
attr_accessor :assertions_count
|
|
582
|
+
attr_reader :failures, :duration, :test_id, :test_location
|
|
583
|
+
|
|
584
|
+
def initialize(test_case)
|
|
585
|
+
@test_id = test_case.id
|
|
586
|
+
@test_location = test_case.location_id
|
|
587
|
+
@assertions_count = 0
|
|
588
|
+
@duration = nil
|
|
589
|
+
@retried = false
|
|
590
|
+
@failures = []
|
|
591
|
+
end
|
|
592
|
+
|
|
593
|
+
def _load(members)
|
|
594
|
+
@test_id = members[0]
|
|
595
|
+
@test_location = members[1]
|
|
596
|
+
@assertions_count = members[2]
|
|
597
|
+
@duration = members[3]
|
|
598
|
+
@failures = members[4]&.map { |m| Failure.load(m) } || []
|
|
599
|
+
@retried = members[5] || false
|
|
600
|
+
self
|
|
601
|
+
end
|
|
602
|
+
|
|
603
|
+
def dump
|
|
604
|
+
members = [
|
|
605
|
+
@test_id,
|
|
606
|
+
@test_location,
|
|
607
|
+
@assertions_count,
|
|
608
|
+
@duration,
|
|
609
|
+
@failures.empty? ? nil : @failures.map(&:dump),
|
|
610
|
+
@retried || nil,
|
|
611
|
+
]
|
|
612
|
+
members.compact!
|
|
613
|
+
Marshal.dump(members)
|
|
614
|
+
end
|
|
615
|
+
|
|
616
|
+
def record_time
|
|
617
|
+
start_time = Megatest.now
|
|
618
|
+
begin
|
|
619
|
+
yield
|
|
620
|
+
ensure
|
|
621
|
+
@duration = Megatest.now - start_time
|
|
622
|
+
end
|
|
623
|
+
self
|
|
624
|
+
end
|
|
625
|
+
|
|
626
|
+
def failure
|
|
627
|
+
@failures.first
|
|
628
|
+
end
|
|
629
|
+
|
|
630
|
+
def ok?
|
|
631
|
+
success? || retried? || skipped?
|
|
632
|
+
end
|
|
633
|
+
|
|
634
|
+
def bad?
|
|
635
|
+
!@retried && !@failures.empty?
|
|
636
|
+
end
|
|
637
|
+
|
|
638
|
+
def status
|
|
639
|
+
if skipped?
|
|
640
|
+
:skipped
|
|
641
|
+
elsif retried?
|
|
642
|
+
:retried
|
|
643
|
+
elsif error?
|
|
644
|
+
:error
|
|
645
|
+
elsif failed?
|
|
646
|
+
:failure
|
|
647
|
+
else
|
|
648
|
+
:success
|
|
649
|
+
end
|
|
650
|
+
end
|
|
651
|
+
|
|
652
|
+
def ensure_assertions
|
|
653
|
+
if @assertions_count.zero? && success?
|
|
654
|
+
@failures << Failure.new(NoAssertion.new)
|
|
655
|
+
end
|
|
656
|
+
self
|
|
657
|
+
end
|
|
658
|
+
|
|
659
|
+
def did_not_run(reason)
|
|
660
|
+
@failures << Failure.new(DidNotRun.new(reason))
|
|
661
|
+
self
|
|
662
|
+
end
|
|
663
|
+
|
|
664
|
+
def lost
|
|
665
|
+
@failures << Failure.new(LostTest.new(@test_id))
|
|
666
|
+
@duration = 0.0
|
|
667
|
+
self
|
|
668
|
+
end
|
|
669
|
+
|
|
670
|
+
def success?
|
|
671
|
+
@failures.empty?
|
|
672
|
+
end
|
|
673
|
+
|
|
674
|
+
def retried?
|
|
675
|
+
@retried
|
|
676
|
+
end
|
|
677
|
+
|
|
678
|
+
def failed?
|
|
679
|
+
!@failures.empty?
|
|
680
|
+
end
|
|
681
|
+
|
|
682
|
+
def failure?
|
|
683
|
+
!@retried && !skipped? && !@failures.empty? && @failures.first&.name != UnexpectedError.name
|
|
684
|
+
end
|
|
685
|
+
|
|
686
|
+
def error?
|
|
687
|
+
!@retried && @failures.first&.name == UnexpectedError.name
|
|
688
|
+
end
|
|
689
|
+
|
|
690
|
+
def lost?
|
|
691
|
+
@failures.first&.name == LostTest.name
|
|
692
|
+
end
|
|
693
|
+
|
|
694
|
+
def skipped?
|
|
695
|
+
@failures.first&.name == Skip.name
|
|
696
|
+
end
|
|
697
|
+
|
|
698
|
+
def retry
|
|
699
|
+
copy = dup
|
|
700
|
+
copy.retried = true
|
|
701
|
+
copy
|
|
702
|
+
end
|
|
703
|
+
|
|
704
|
+
protected
|
|
705
|
+
|
|
706
|
+
attr_writer :retried
|
|
707
|
+
end
|
|
708
|
+
end
|