spectre-core 1.15.2 → 2.0.1
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
- data/exe/spectre +271 -463
- data/lib/spectre/assertion.rb +118 -250
- data/lib/spectre/expectation.rb +89 -0
- data/lib/spectre/helpers.rb +56 -59
- data/lib/spectre/version.rb +3 -0
- data/lib/spectre.rb +1510 -334
- metadata +57 -32
- data/lib/spectre/async.rb +0 -31
- data/lib/spectre/bag.rb +0 -17
- data/lib/spectre/curl.rb +0 -398
- data/lib/spectre/diagnostic.rb +0 -39
- data/lib/spectre/environment.rb +0 -30
- data/lib/spectre/http/basic_auth.rb +0 -25
- data/lib/spectre/http/keystone.rb +0 -99
- data/lib/spectre/http.rb +0 -394
- data/lib/spectre/logging/console.rb +0 -156
- data/lib/spectre/logging/file.rb +0 -106
- data/lib/spectre/logging.rb +0 -183
- data/lib/spectre/mixin.rb +0 -61
- data/lib/spectre/reporter/console.rb +0 -104
- data/lib/spectre/reporter.rb +0 -17
- data/lib/spectre/resources.rb +0 -53
data/lib/spectre.rb
CHANGED
@@ -1,514 +1,1690 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'ostruct'
|
4
|
+
require 'yaml'
|
5
|
+
require 'json'
|
6
|
+
require 'optparse'
|
7
|
+
require 'ectoplasm'
|
8
|
+
require 'date'
|
9
|
+
require 'fileutils'
|
10
|
+
require 'securerandom'
|
11
|
+
require 'logger'
|
12
|
+
require 'stringio'
|
13
|
+
|
14
|
+
require_relative 'spectre/version'
|
15
|
+
require_relative 'spectre/assertion'
|
16
|
+
require_relative 'spectre/helpers'
|
17
|
+
|
18
|
+
def get_call_location call_stack
|
19
|
+
loc = (call_stack || caller_locations)
|
20
|
+
.find { |x| x.label.include? 'Spectre::Engine#load_files' or x.base_label == '<top (required)>' }
|
21
|
+
|
22
|
+
[
|
23
|
+
loc.path.sub(Dir.pwd, '.'),
|
24
|
+
loc.lineno
|
25
|
+
]
|
26
|
+
end
|
27
|
+
|
28
|
+
class Hash
|
29
|
+
# :nodoc:
|
30
|
+
def deep_merge!(second)
|
31
|
+
return unless second.is_a?(Hash)
|
32
|
+
|
33
|
+
merger = proc { |_key, v1, v2| v1.is_a?(Hash) && v2.is_a?(Hash) ? v1.merge!(v2, &merger) : v2 }
|
34
|
+
merge!(second, &merger)
|
35
|
+
end
|
36
|
+
|
37
|
+
# :nodoc:
|
38
|
+
def to_recursive_struct
|
39
|
+
OpenStruct.new(
|
40
|
+
transform_values do |val|
|
41
|
+
case val
|
42
|
+
when Hash, Array
|
43
|
+
val.to_recursive_struct
|
44
|
+
else
|
45
|
+
val
|
46
|
+
end
|
47
|
+
end
|
48
|
+
)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
class Array
|
53
|
+
# :nodoc:
|
54
|
+
def to_recursive_struct
|
55
|
+
map(&:to_recursive_struct)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
class Object
|
60
|
+
# :nodoc:
|
61
|
+
def to_recursive_struct
|
62
|
+
self
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
class String
|
67
|
+
alias error red
|
68
|
+
alias failed red
|
69
|
+
alias fatal red
|
70
|
+
alias warn yellow
|
71
|
+
alias ok green
|
72
|
+
alias info blue
|
73
|
+
alias debug grey
|
74
|
+
alias skipped grey
|
75
|
+
end
|
76
|
+
|
77
|
+
##
|
78
|
+
# The main module containing all logic for the framework
|
79
|
+
#
|
1
80
|
module Spectre
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
81
|
+
##
|
82
|
+
# Exception to throw in order to abort a spec run
|
83
|
+
#
|
84
|
+
class AbortException < StandardError
|
6
85
|
end
|
7
86
|
|
8
|
-
|
87
|
+
##
|
88
|
+
# Extension methods used by Spectre modules to expose
|
89
|
+
# registered methods in all Spectre and module scopes.
|
90
|
+
#
|
91
|
+
module Delegate
|
92
|
+
@@methods = {}
|
93
|
+
|
94
|
+
def instance_eval &block
|
95
|
+
@outer_scope = eval('self', block.binding)
|
96
|
+
super
|
97
|
+
end
|
98
|
+
|
99
|
+
def instance_exec(*, **, &block)
|
100
|
+
@outer_scope = eval('self', block.binding)
|
101
|
+
super
|
102
|
+
end
|
103
|
+
|
104
|
+
def respond_to_missing?(method, *)
|
105
|
+
@engine.respond_to?(method) or
|
106
|
+
@outer_scope.respond_to?(method)
|
107
|
+
end
|
9
108
|
|
109
|
+
def method_missing(method, *, &)
|
110
|
+
return @engine.send(method, *, &) if @engine.respond_to?(method)
|
111
|
+
return @outer_scope.send(method, *, &) if @outer_scope.respond_to?(method)
|
10
112
|
|
11
|
-
|
12
|
-
def deep_merge!(second)
|
13
|
-
merger = proc { |_key, v1, v2| Hash === v1 && Hash === v2 ? v1.merge!(v2, &merger) : v2 }
|
14
|
-
self.merge!(second, &merger)
|
113
|
+
super
|
15
114
|
end
|
16
115
|
end
|
17
116
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
117
|
+
##
|
118
|
+
# The failure class containing information about spec failures
|
119
|
+
# created by +assert+ and +expect+.
|
120
|
+
#
|
121
|
+
class Failure < StandardError
|
122
|
+
# A message describing the failure
|
123
|
+
attr_reader :message
|
124
|
+
# The file path where the failure occured
|
125
|
+
attr_reader :file
|
126
|
+
# The line where the failure occured
|
127
|
+
attr_reader :line
|
128
|
+
|
129
|
+
##
|
130
|
+
# Constructs a new +Failure+ instance with the given message
|
131
|
+
# and determines the +file+ and +line+ from the given call stack
|
132
|
+
def initialize message, call_stack = nil
|
133
|
+
super(message)
|
134
|
+
|
135
|
+
@file, @line = get_call_location(call_stack || caller_locations)
|
136
|
+
@message = message
|
23
137
|
end
|
24
138
|
|
25
|
-
|
26
|
-
|
139
|
+
# :nodoc:
|
140
|
+
def to_s
|
141
|
+
"#{@message} - in #{@file}:#{@line}"
|
27
142
|
end
|
28
143
|
end
|
29
144
|
|
145
|
+
##
|
146
|
+
# This context will be created by +assert+ and +expect+
|
147
|
+
# methods. Failures are reported here.
|
148
|
+
#
|
149
|
+
class EvaluationContext
|
150
|
+
include Delegate
|
30
151
|
|
31
|
-
|
32
|
-
|
33
|
-
###########################################
|
152
|
+
# :stopdoc:
|
153
|
+
attr_reader :desc, :failures
|
34
154
|
|
155
|
+
def initialize(engine, desc, &)
|
156
|
+
@desc = desc
|
157
|
+
@failures = []
|
35
158
|
|
36
|
-
|
37
|
-
|
159
|
+
engine.formatter.log(:info, desc) do
|
160
|
+
instance_eval(&)
|
38
161
|
|
39
|
-
|
40
|
-
|
162
|
+
if @failures.any?
|
163
|
+
engine.logger.error("#{desc} - failed")
|
164
|
+
[:error, :failed, nil]
|
165
|
+
else
|
166
|
+
engine.logger.info("#{desc} - ok")
|
167
|
+
[:info, :ok, nil]
|
168
|
+
end
|
169
|
+
rescue Failure => e
|
170
|
+
engine.logger.error("#{e.message} - failed")
|
171
|
+
@failures << e
|
172
|
+
[:error, :failed, nil]
|
173
|
+
end
|
174
|
+
end
|
41
175
|
|
42
|
-
|
43
|
-
|
44
|
-
|
176
|
+
# :startdoc:
|
177
|
+
|
178
|
+
##
|
179
|
+
# Report a failure within the +EvaluationContext+.
|
180
|
+
#
|
181
|
+
# assert 'everthing goes well' do
|
182
|
+
# report 'it does not' if some_thing_happens()
|
183
|
+
# end
|
184
|
+
#
|
185
|
+
def report failure
|
186
|
+
@failures << Failure.new(failure, caller_locations)
|
45
187
|
end
|
46
188
|
end
|
47
189
|
|
48
|
-
|
190
|
+
##
|
191
|
+
# An extended +Logger+ class providing additional Spectre run information
|
192
|
+
#
|
193
|
+
class Logger < ::Logger
|
194
|
+
def initialize(config, **)
|
195
|
+
log_file = config['log_file']
|
196
|
+
|
197
|
+
if log_file.is_a? String
|
198
|
+
log_file = log_file.gsub('<date>', DateTime.now.strftime('%Y-%m-%d_%H%M%S%3N'))
|
199
|
+
FileUtils.mkdir_p(File.dirname(log_file))
|
200
|
+
end
|
201
|
+
|
202
|
+
super(log_file, **)
|
203
|
+
|
204
|
+
if config['debug']
|
205
|
+
debug!
|
206
|
+
else
|
207
|
+
info!
|
208
|
+
end
|
209
|
+
|
210
|
+
@corr_ids = []
|
211
|
+
|
212
|
+
@formatter = proc do |severity, datetime, progname, message|
|
213
|
+
date_formatted = datetime.strftime(config['log_date_format'])
|
214
|
+
progname ||= 'spectre'
|
215
|
+
|
216
|
+
corr_id = @corr_ids.join
|
217
|
+
|
218
|
+
# Add log message also to the current executing run context
|
219
|
+
if RunContext.current.nil?
|
220
|
+
context_name = 'spectre'
|
221
|
+
else
|
222
|
+
RunContext.current.logs << [date_formatted, severity, progname, corr_id, message]
|
223
|
+
context_name = RunContext.current.name
|
224
|
+
end
|
225
|
+
|
226
|
+
format(config['log_message_format'],
|
227
|
+
date_formatted,
|
228
|
+
severity,
|
229
|
+
progname,
|
230
|
+
context_name,
|
231
|
+
corr_id,
|
232
|
+
message)
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
def correlate
|
237
|
+
@corr_ids.append(rand(36**4).to_s(36))
|
238
|
+
|
239
|
+
begin
|
240
|
+
yield
|
241
|
+
ensure
|
242
|
+
@corr_ids.pop
|
243
|
+
end
|
244
|
+
end
|
49
245
|
end
|
50
246
|
|
247
|
+
class SimpleFormatter
|
248
|
+
def initialize config
|
249
|
+
@out = config['stdout'] || $stdout
|
250
|
+
@level = 0
|
251
|
+
@width = 80
|
252
|
+
@indent = 2
|
253
|
+
@colors = [:blue, :magenta, :yellow, :green]
|
254
|
+
@debug = config['debug']
|
255
|
+
end
|
256
|
+
|
257
|
+
##
|
258
|
+
# Formats a list of specs in short form
|
259
|
+
#
|
260
|
+
def list specs
|
261
|
+
counter = 0
|
262
|
+
|
263
|
+
specs
|
264
|
+
.group_by { |x| x.parent.root }
|
265
|
+
.each_value do |spec_group|
|
266
|
+
spec_group.sort_by!(&:name)
|
267
|
+
spec_group.each do |spec|
|
268
|
+
spec_id = "[#{spec.name}]".send(@colors[counter % @colors.length])
|
269
|
+
@out.puts "#{spec_id} #{spec.full_desc} #{spec.tags.map { |x| "##{x}" }.join(' ').cyan}"
|
270
|
+
end
|
271
|
+
|
272
|
+
counter += 1
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
##
|
277
|
+
# Outputs all the specs for all contexts
|
278
|
+
#
|
279
|
+
def describe contexts, level = 0
|
280
|
+
contexts.each do |context|
|
281
|
+
@out.puts("#{' ' * level}#{context.desc.send(level.positive? ? :magenta : :blue)}")
|
282
|
+
|
283
|
+
context.specs.each do |spec|
|
284
|
+
@out.puts("#{' ' * (level + 1)}#{spec.desc}")
|
285
|
+
end
|
286
|
+
|
287
|
+
describe(context.children, level + 1)
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
291
|
+
##
|
292
|
+
# Formats the details of given specs
|
293
|
+
#
|
294
|
+
def details specs
|
295
|
+
counter = 0
|
296
|
+
|
297
|
+
specs
|
298
|
+
.group_by { |x| x.parent.root }
|
299
|
+
.each_value do |spec_group|
|
300
|
+
spec_group.each do |spec|
|
301
|
+
spec_id = "[#{spec.name}]".send(@colors[counter % @colors.length])
|
302
|
+
spec_detail = "#{spec_id}\n"
|
303
|
+
spec_detail += " subject..: #{spec.root.desc}\n"
|
304
|
+
spec_detail += " context..: #{spec.parent.desc}\n" unless spec.root == spec.parent
|
305
|
+
spec_detail += " desc.....: #{spec.desc}\n"
|
306
|
+
spec_detail += " tags.....: #{spec.tags.join(', ')}\n" if spec.tags.any?
|
307
|
+
spec_detail += " data.....: #{spec.data.to_json}\n" if spec.data
|
308
|
+
spec_detail += " file.....: #{spec.file}\n"
|
309
|
+
|
310
|
+
@out.puts("#{spec_detail}\n")
|
311
|
+
end
|
312
|
+
|
313
|
+
counter += 1
|
314
|
+
end
|
315
|
+
end
|
316
|
+
|
317
|
+
##
|
318
|
+
# Formats a list of mixins
|
319
|
+
#
|
320
|
+
def mixins mixins
|
321
|
+
paragraphs = []
|
322
|
+
|
323
|
+
mixins.each_value do |mixin|
|
324
|
+
output = "#{mixin.desc.yellow}\n"
|
325
|
+
output += " params.....: #{mixin.params.join ', '}\n" if mixin.params.any?
|
326
|
+
output += " location...: #{mixin.file.sub(Dir.pwd, '.')}:#{mixin.line}"
|
327
|
+
paragraphs << output
|
328
|
+
end
|
51
329
|
|
52
|
-
|
53
|
-
|
54
|
-
###########################################
|
330
|
+
@out.puts paragraphs.join("\n\n")
|
331
|
+
end
|
55
332
|
|
333
|
+
def collections engine
|
334
|
+
@out.puts engine.collections.pretty
|
335
|
+
end
|
56
336
|
|
57
|
-
|
58
|
-
|
59
|
-
def _evaluate &block
|
60
|
-
@__bound_self__ = eval('self', block.binding)
|
61
|
-
instance_eval(&block)
|
337
|
+
def environment env
|
338
|
+
@out.puts env.to_h.pretty
|
62
339
|
end
|
63
340
|
|
64
|
-
def
|
65
|
-
|
66
|
-
|
341
|
+
def scope desc, type
|
342
|
+
if desc
|
343
|
+
colored_desc = case type
|
344
|
+
when :before, :after, :setup, :teardown
|
345
|
+
desc.magenta
|
346
|
+
when :group
|
347
|
+
desc.grey
|
348
|
+
when Specification
|
349
|
+
desc.cyan
|
350
|
+
when :mixin
|
351
|
+
desc.yellow
|
352
|
+
when DefinitionContext
|
353
|
+
desc.blue
|
354
|
+
else
|
355
|
+
desc
|
356
|
+
end
|
357
|
+
|
358
|
+
write(colored_desc)
|
359
|
+
|
360
|
+
@out.puts "\n"
|
361
|
+
end
|
362
|
+
|
363
|
+
return unless block_given?
|
364
|
+
|
365
|
+
@level += 1
|
366
|
+
|
367
|
+
begin
|
368
|
+
yield
|
369
|
+
ensure
|
370
|
+
@level -= 1
|
371
|
+
end
|
67
372
|
end
|
68
373
|
|
69
|
-
def
|
70
|
-
if @
|
71
|
-
|
374
|
+
def log level, message, status = nil, desc = nil
|
375
|
+
return if @locked
|
376
|
+
|
377
|
+
color = [:fatal, :debug].include?(level) ? level : nil
|
378
|
+
|
379
|
+
write(message, fill: true, color:) if block_given? or @debug or level != :debug
|
380
|
+
|
381
|
+
error = nil
|
382
|
+
|
383
|
+
if block_given?
|
384
|
+
@locked = true
|
385
|
+
|
386
|
+
begin
|
387
|
+
level, status, desc = yield
|
388
|
+
rescue StandardError => e
|
389
|
+
level = :fatal
|
390
|
+
status = :error
|
391
|
+
desc = e.class
|
392
|
+
error = e
|
393
|
+
ensure
|
394
|
+
@locked = false
|
395
|
+
end
|
396
|
+
end
|
397
|
+
|
398
|
+
label = status || level
|
399
|
+
|
400
|
+
return unless block_given? or @debug or level != :debug
|
401
|
+
|
402
|
+
status_text = "[#{label}]"
|
403
|
+
|
404
|
+
if desc.nil?
|
405
|
+
@out.puts status_text.send(label)
|
72
406
|
else
|
73
|
-
|
407
|
+
@out.puts "#{status_text} - #{desc}".send(label)
|
74
408
|
end
|
409
|
+
|
410
|
+
raise error if error
|
75
411
|
end
|
76
|
-
end
|
77
412
|
|
78
|
-
|
79
|
-
attr_reader :name, :desc, :specs
|
413
|
+
private
|
80
414
|
|
81
|
-
def
|
82
|
-
@
|
83
|
-
@specs = []
|
84
|
-
@name = desc.downcase.gsub(/[^a-z0-9]+/, '_')
|
415
|
+
def indent
|
416
|
+
' ' * (@level * @indent)
|
85
417
|
end
|
86
418
|
|
87
|
-
def
|
88
|
-
|
89
|
-
|
419
|
+
def write message, fill: false, color: nil
|
420
|
+
output = if message.nil? or message.empty?
|
421
|
+
indent
|
422
|
+
else
|
423
|
+
message.lines.map do |line|
|
424
|
+
indent + line
|
425
|
+
end.join
|
426
|
+
end
|
427
|
+
|
428
|
+
output += '.' * (@width > output.length ? @width - output.length : 0) if fill
|
429
|
+
output = output.send(color) unless color.nil?
|
430
|
+
|
431
|
+
@out.print output
|
90
432
|
end
|
433
|
+
end
|
91
434
|
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
435
|
+
class SimpleReporter
|
436
|
+
def initialize config
|
437
|
+
@out = config['stdout'] || $stdout
|
438
|
+
@debug = config['debug']
|
439
|
+
end
|
440
|
+
|
441
|
+
def report runs
|
442
|
+
runs = runs.select { |x| x.parent.is_a? Specification }
|
443
|
+
|
444
|
+
errors = runs.count { |x| x.status == :error }
|
445
|
+
failed = runs.count { |x| x.status == :failed }
|
446
|
+
skipped = runs.count { |x| x.status == :skipped }
|
447
|
+
succeeded = runs.count - errors - failed - skipped
|
448
|
+
|
449
|
+
summary = "#{succeeded} succeeded"
|
450
|
+
summary += " #{failed} failures"
|
451
|
+
summary += " #{errors} errors"
|
452
|
+
summary += " #{skipped} skipped"
|
453
|
+
|
454
|
+
@out.puts(summary.send((errors + failed).positive? ? :red : :green))
|
455
|
+
|
456
|
+
output = "\n"
|
457
|
+
|
458
|
+
runs
|
459
|
+
.select { |x| [:error, :failed].include? x.status }
|
460
|
+
.each_with_index do |run, index|
|
461
|
+
index += 1
|
462
|
+
|
463
|
+
output += "#{index})"
|
464
|
+
output += " #{run.parent.full_desc}"
|
465
|
+
output += " (#{run.finished - run.started})"
|
466
|
+
output += " [#{run.parent.name}]"
|
467
|
+
|
468
|
+
output += "\n"
|
469
|
+
|
470
|
+
if run.error
|
471
|
+
file, line = get_call_location(run.error.backtrace_locations)
|
472
|
+
|
473
|
+
error_output = "but an unexpected error occurred during run\n"
|
474
|
+
error_output += " file.....: #{file}:#{line}\n" if file
|
475
|
+
error_output += " type.....: #{run.error.class.name}\n"
|
476
|
+
error_output += " message..: #{run.error.message}\n"
|
477
|
+
|
478
|
+
if @debug and run.error.backtrace
|
479
|
+
error_output += " backtrace:\n"
|
480
|
+
|
481
|
+
run.error.backtrace.each do |trace|
|
482
|
+
error_output += " #{trace}\n"
|
483
|
+
end
|
484
|
+
end
|
485
|
+
|
486
|
+
output += error_output.indent(5)
|
487
|
+
output += "\n\n"
|
488
|
+
end
|
489
|
+
|
490
|
+
next unless run.status == :failed
|
491
|
+
|
492
|
+
failed = run.evaluations
|
493
|
+
.select { |x| x.failures.any? }
|
494
|
+
|
495
|
+
failed.each_with_index do |eval, eval_idx|
|
496
|
+
output += if failed.count == 1
|
497
|
+
" #{eval.desc}, but"
|
498
|
+
else
|
499
|
+
" #{index}.#{eval_idx + 1}) #{eval.desc}, but"
|
500
|
+
end
|
501
|
+
|
502
|
+
if eval.failures.count == 1
|
503
|
+
output += " #{eval.failures.first.message}\n"
|
504
|
+
else
|
505
|
+
output += " #{eval.failures.count} failures occured\n"
|
506
|
+
|
507
|
+
eval.failures.each_with_index do |fail, fail_idx|
|
508
|
+
output += if failed.count == 1
|
509
|
+
" #{index}.#{fail_idx + 1}) #{fail.message}\n"
|
510
|
+
else
|
511
|
+
" #{index}.#{eval_idx + 1}.#{fail_idx + 1}) #{fail.message}\n"
|
512
|
+
end
|
513
|
+
end
|
514
|
+
end
|
515
|
+
end
|
516
|
+
|
517
|
+
output += "\n"
|
518
|
+
end
|
519
|
+
|
520
|
+
@out.puts output.red
|
98
521
|
end
|
99
522
|
end
|
100
523
|
|
101
|
-
class
|
102
|
-
|
524
|
+
class JsonFormatter
|
525
|
+
def initialize config
|
526
|
+
@config = config
|
527
|
+
@out = config['stdout'] || $stdout
|
528
|
+
@out.sync = true
|
529
|
+
@curr_scope = nil
|
530
|
+
end
|
103
531
|
|
104
|
-
def
|
105
|
-
@
|
106
|
-
@context = context
|
107
|
-
@data = data
|
108
|
-
@subject = subject
|
109
|
-
@desc = desc
|
110
|
-
@tags = tags
|
111
|
-
@block = block
|
112
|
-
@file = file
|
113
|
-
@line = line
|
532
|
+
def describe contexts
|
533
|
+
@out.puts JSON.dump de(contexts)
|
114
534
|
end
|
115
535
|
|
116
|
-
def
|
117
|
-
|
536
|
+
def de contexts
|
537
|
+
contexts.map do |context|
|
538
|
+
{
|
539
|
+
name: context.name,
|
540
|
+
desc: context.desc,
|
541
|
+
specs: context.specs.map do |spec|
|
542
|
+
{
|
543
|
+
name: spec.name,
|
544
|
+
desc: spec.desc,
|
545
|
+
tags: spec.tags,
|
546
|
+
file: spec.file,
|
547
|
+
data: spec.data,
|
548
|
+
}
|
549
|
+
end,
|
550
|
+
children: de(context.children),
|
551
|
+
}
|
552
|
+
end
|
118
553
|
end
|
119
554
|
|
120
|
-
def
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
}
|
555
|
+
def list specs
|
556
|
+
@out.puts JSON.pretty_generate(specs.map do |spec|
|
557
|
+
{
|
558
|
+
name: spec.name,
|
559
|
+
desc: spec.desc,
|
560
|
+
tags: spec.tags,
|
561
|
+
file: spec.file,
|
562
|
+
data: spec.data,
|
563
|
+
}
|
564
|
+
end)
|
131
565
|
end
|
132
|
-
end
|
133
566
|
|
134
|
-
|
135
|
-
|
136
|
-
|
567
|
+
def mixins mixins
|
568
|
+
@out.puts JSON.pretty_generate(mixins.each_value.map do |mixin|
|
569
|
+
{
|
570
|
+
desc: mixin.desc,
|
571
|
+
params: mixin.params,
|
572
|
+
file: mixin.file,
|
573
|
+
line: mixin.line,
|
574
|
+
}
|
575
|
+
end)
|
576
|
+
end
|
137
577
|
|
138
|
-
def
|
139
|
-
@
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
@log = []
|
147
|
-
@expectations = []
|
148
|
-
@properties = {}
|
578
|
+
def collections engine
|
579
|
+
@out.puts(engine.collections.map do |name, config|
|
580
|
+
{
|
581
|
+
name:,
|
582
|
+
config:,
|
583
|
+
specs: engine.list(config).map(&:name),
|
584
|
+
}
|
585
|
+
end.to_json)
|
149
586
|
end
|
150
587
|
|
151
|
-
def
|
152
|
-
@
|
588
|
+
def environment env
|
589
|
+
@out.puts env.to_json
|
153
590
|
end
|
154
591
|
|
155
|
-
def
|
156
|
-
|
592
|
+
def scope desc, type
|
593
|
+
id = SecureRandom.hex(8)
|
594
|
+
|
595
|
+
prev_scope = @curr_scope
|
596
|
+
|
597
|
+
if type.is_a?(Specification)
|
598
|
+
spec = type.name
|
599
|
+
type = :spec
|
600
|
+
end
|
601
|
+
|
602
|
+
if type.is_a?(DefinitionContext)
|
603
|
+
context = type.name
|
604
|
+
type = :context
|
605
|
+
end
|
606
|
+
|
607
|
+
@out.puts JSON.dump({
|
608
|
+
id: id,
|
609
|
+
type: 'scope',
|
610
|
+
desc: desc,
|
611
|
+
parent: @curr_scope,
|
612
|
+
scope: type,
|
613
|
+
spec: spec,
|
614
|
+
context: context,
|
615
|
+
})
|
616
|
+
|
617
|
+
@curr_scope = id
|
618
|
+
yield
|
619
|
+
ensure
|
620
|
+
@curr_scope = prev_scope
|
157
621
|
end
|
158
622
|
|
159
|
-
def
|
160
|
-
|
623
|
+
def log level, message, status = nil, desc = nil
|
624
|
+
id = SecureRandom.hex(8)
|
625
|
+
|
626
|
+
@out.puts JSON.dump({
|
627
|
+
id: id,
|
628
|
+
parent: @curr_scope,
|
629
|
+
type: 'log',
|
630
|
+
level: level,
|
631
|
+
message: message,
|
632
|
+
status: status,
|
633
|
+
desc: desc,
|
634
|
+
})
|
635
|
+
|
636
|
+
return unless block_given?
|
637
|
+
|
638
|
+
begin
|
639
|
+
level, status, desc = yield
|
640
|
+
rescue StandardError => e
|
641
|
+
level = :fatal
|
642
|
+
status = :error
|
643
|
+
desc = e.class
|
644
|
+
error = e
|
645
|
+
end
|
646
|
+
|
647
|
+
@out.puts JSON.dump({
|
648
|
+
id: id,
|
649
|
+
type: 'status',
|
650
|
+
level: level,
|
651
|
+
status: status,
|
652
|
+
desc: desc,
|
653
|
+
error: error,
|
654
|
+
})
|
161
655
|
end
|
656
|
+
end
|
162
657
|
|
163
|
-
|
164
|
-
|
658
|
+
class JsonReporter
|
659
|
+
def initialize config
|
660
|
+
@out = config['stdout'] || $stdout
|
661
|
+
@debug = config['debug']
|
165
662
|
end
|
166
663
|
|
167
|
-
def
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
664
|
+
def report runs
|
665
|
+
runs = runs.select { |x| x.parent.is_a? Specification }
|
666
|
+
|
667
|
+
errors = runs.count { |x| x.status == :error }
|
668
|
+
failed = runs.count { |x| x.status == :failed }
|
669
|
+
skipped = runs.count { |x| x.status == :skipped }
|
670
|
+
succeeded = runs.count - errors - failed - skipped
|
671
|
+
|
672
|
+
report = {
|
673
|
+
errors: errors,
|
674
|
+
failed: failed,
|
675
|
+
skipped: skipped,
|
676
|
+
succeeded: succeeded,
|
677
|
+
runs: runs.map do |run|
|
678
|
+
{
|
679
|
+
spec: run.parent.name,
|
680
|
+
desc: run.parent.full_desc,
|
681
|
+
duration: run.finished - run.started,
|
682
|
+
status: run.status,
|
683
|
+
logs: run.logs,
|
684
|
+
error: run.error,
|
685
|
+
evaluations: run.evaluations.map do |evaluation|
|
686
|
+
{
|
687
|
+
desc: evaluation.desc,
|
688
|
+
failures: evaluation.failures.map do |failure|
|
689
|
+
{
|
690
|
+
message: failure.message,
|
691
|
+
file: failure.file,
|
692
|
+
line: failure.line,
|
693
|
+
}
|
694
|
+
end
|
695
|
+
}
|
696
|
+
end
|
697
|
+
}
|
698
|
+
end
|
189
699
|
}
|
700
|
+
|
701
|
+
@out.puts JSON.dump(report)
|
190
702
|
end
|
191
703
|
end
|
192
704
|
|
193
|
-
class
|
194
|
-
|
195
|
-
|
705
|
+
class Mixin
|
706
|
+
include Delegate
|
707
|
+
|
708
|
+
# The description of the mixin. This value has to be unique
|
709
|
+
# as it is used for running the mixin.
|
710
|
+
attr_reader :desc
|
711
|
+
# A list of required parameters the mixin uses.
|
712
|
+
# When running the mixin, given params must contain the keys in this list.
|
713
|
+
attr_reader :params
|
714
|
+
# The file where the mixin is defined
|
715
|
+
attr_reader :file
|
716
|
+
# The line in the file where the mixin is defined
|
717
|
+
attr_reader :line
|
718
|
+
|
719
|
+
def initialize desc, required, block, file, line
|
720
|
+
@desc = desc
|
721
|
+
@params = required
|
722
|
+
@block = block
|
723
|
+
@file = file
|
724
|
+
@line = line
|
725
|
+
@given = {}
|
196
726
|
end
|
197
727
|
|
198
|
-
|
199
|
-
|
728
|
+
##
|
729
|
+
# Add execution paramters. Available within the block
|
730
|
+
# of a mixin execution.
|
731
|
+
#
|
732
|
+
# run 'some mixin' do
|
733
|
+
# with some_key: 'a value',
|
734
|
+
# a_number: 42
|
735
|
+
# end
|
736
|
+
#
|
737
|
+
def with **params
|
738
|
+
@given.merge! params
|
200
739
|
end
|
201
740
|
|
202
|
-
|
203
|
-
|
741
|
+
##
|
742
|
+
# Run the mixin with the given parameters in the context of the given +RunContext+
|
743
|
+
# All methods of the +RunContext+ are available within the mixin block
|
744
|
+
#
|
745
|
+
def run run_context, params
|
746
|
+
params ||= {}
|
204
747
|
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
748
|
+
case params
|
749
|
+
when Hash
|
750
|
+
params.merge! @given unless @given.empty?
|
751
|
+
|
752
|
+
if @params.any?
|
753
|
+
missing_params = @params - params.keys
|
754
|
+
raise "missing params: #{missing_params.join(', ')}" unless missing_params.empty?
|
212
755
|
end
|
756
|
+
|
757
|
+
params = [params.to_recursive_struct]
|
758
|
+
when Array
|
759
|
+
params = params.map(&:to_recursive_struct)
|
213
760
|
end
|
214
761
|
|
215
|
-
|
762
|
+
run_context.instance_exec(*params, &@block)
|
216
763
|
end
|
764
|
+
end
|
217
765
|
|
218
|
-
|
766
|
+
class RunContext
|
767
|
+
include Delegate
|
768
|
+
|
769
|
+
##
|
770
|
+
# A variable bag to store values across +setup+, +teardown+,
|
771
|
+
# +before+, +after+ and +it+ blocks.
|
772
|
+
#
|
773
|
+
# setup do
|
774
|
+
# bag.foo = 'bar'
|
775
|
+
# end
|
776
|
+
#
|
777
|
+
# it 'does something' do
|
778
|
+
# assert bag.foo.to be 'bar'
|
779
|
+
# end
|
780
|
+
#
|
781
|
+
attr_reader :bag
|
782
|
+
|
783
|
+
# :stopdoc:
|
784
|
+
|
785
|
+
attr_reader :name, :parent, :type, :logs, :error,
|
786
|
+
:evaluations, :started, :finished, :properties, :data
|
787
|
+
|
788
|
+
##
|
789
|
+
# The default identifier of +async+ blocks.
|
790
|
+
#
|
791
|
+
DEFAULT_ASYNC_NAME = 'default'
|
792
|
+
|
793
|
+
@@current = nil
|
794
|
+
@@location_cache = {}
|
795
|
+
@@skip_count = 0
|
796
|
+
|
797
|
+
##
|
798
|
+
# The current executing +RunContext+
|
799
|
+
#
|
800
|
+
def self.current
|
801
|
+
@@current
|
802
|
+
end
|
219
803
|
|
220
|
-
def
|
221
|
-
|
804
|
+
def initialize engine, parent, type, bag = nil
|
805
|
+
@engine = engine
|
806
|
+
@parent = parent
|
807
|
+
@type = type
|
808
|
+
@logs = []
|
222
809
|
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
810
|
+
@threads = {}
|
811
|
+
|
812
|
+
@name = parent.name
|
813
|
+
@name += "-#{type}" unless type == :spec
|
814
|
+
|
815
|
+
@bag = OpenStruct.new(bag)
|
816
|
+
|
817
|
+
@properties = {}
|
818
|
+
|
819
|
+
@evaluations = []
|
820
|
+
@error = nil
|
821
|
+
@skipped = false
|
822
|
+
|
823
|
+
@started = Time.now
|
228
824
|
|
229
825
|
begin
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
if spec.data.any?
|
234
|
-
spec.data
|
235
|
-
.map { |x| x.is_a?(Hash) ? OpenStruct.new(x) : x }
|
236
|
-
.each do |data|
|
237
|
-
Spectre::Logging.log_spec(spec, data) do
|
238
|
-
runs << run_spec(spec, data)
|
239
|
-
end
|
240
|
-
end
|
241
|
-
else
|
242
|
-
Spectre::Logging.log_spec(spec) do
|
243
|
-
runs << run_spec(spec)
|
244
|
-
end
|
245
|
-
end
|
246
|
-
end
|
826
|
+
@@current = self
|
827
|
+
yield self
|
247
828
|
ensure
|
248
|
-
|
249
|
-
|
250
|
-
end
|
829
|
+
@finished = Time.now
|
830
|
+
@@current = nil
|
251
831
|
end
|
832
|
+
end
|
252
833
|
|
253
|
-
|
834
|
+
##
|
835
|
+
# Executes the given block in the context of this +RunContext+.
|
836
|
+
# For internal use only. Do not execute within an +it+ block.
|
837
|
+
#
|
838
|
+
def execute(data, &)
|
839
|
+
@data = data
|
840
|
+
instance_exec(data.is_a?(Hash) ? OpenStruct.new(data) : data, &)
|
841
|
+
rescue AbortException
|
842
|
+
# Do nothing. The run will be ended here
|
843
|
+
rescue Interrupt
|
844
|
+
@skipped = true
|
845
|
+
@engine.formatter.log(:debug, nil, :skipped, 'canceled by user')
|
846
|
+
@engine.logger.info("#{@parent.desc} - canceled by user")
|
847
|
+
raise Interrupt if (@@skip_count += 1) > 2
|
848
|
+
rescue StandardError => e
|
849
|
+
@error = e
|
850
|
+
@engine.formatter.log(:fatal, e.message, :error, e.class.name)
|
851
|
+
@engine.logger.fatal("#{e.message}\n#{e.backtrace.join("\n")}")
|
852
|
+
end
|
853
|
+
|
854
|
+
##
|
855
|
+
# The status of the current run.
|
856
|
+
# One of +:error+, +:failed+, +:skipped+ or +:success+
|
857
|
+
#
|
858
|
+
def status
|
859
|
+
return :error if @error
|
860
|
+
return :failed if @evaluations.any? { |x| x.failures.any? }
|
861
|
+
return :skipped if @skipped
|
862
|
+
|
863
|
+
:success
|
254
864
|
end
|
255
865
|
|
256
|
-
|
257
|
-
|
866
|
+
# :startdoc:
|
867
|
+
|
868
|
+
##
|
869
|
+
# :method: debug
|
870
|
+
# :args: message
|
871
|
+
#
|
872
|
+
# Logs a debug message. Only when +debug+ config option is set to +true+.
|
873
|
+
#
|
874
|
+
|
875
|
+
##
|
876
|
+
# :method: info
|
877
|
+
# :args: message
|
878
|
+
#
|
879
|
+
# Logs a info message.
|
880
|
+
#
|
881
|
+
|
882
|
+
##
|
883
|
+
# :method: warn
|
884
|
+
# :args: message
|
885
|
+
#
|
886
|
+
# Logs a warn message.
|
887
|
+
#
|
888
|
+
|
889
|
+
%i[debug info warn].each do |method|
|
890
|
+
define_method(method) do |message|
|
891
|
+
message = message.to_s
|
892
|
+
@engine.logger.send(method, message)
|
893
|
+
@engine.formatter.log(method, message)
|
894
|
+
end
|
895
|
+
end
|
258
896
|
|
259
|
-
|
897
|
+
alias log info
|
260
898
|
|
261
|
-
|
899
|
+
##
|
900
|
+
# Access the loaded resources (files). The path parameter
|
901
|
+
# is relative to the resource directory.
|
902
|
+
#
|
903
|
+
# resources['path/to/some.txt']
|
904
|
+
#
|
905
|
+
def resources
|
906
|
+
@engine.resources
|
907
|
+
end
|
262
908
|
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
909
|
+
##
|
910
|
+
# deprecated:: use +report+ instead.
|
911
|
+
#
|
912
|
+
# Raise a failure error.
|
913
|
+
# Using this method within an +assert+ or +expect+ block
|
914
|
+
# will report an error and aborts the run *immediately*.
|
915
|
+
#
|
916
|
+
# assert 'something' do
|
917
|
+
# fail_with 'a detailed message'
|
918
|
+
# end
|
919
|
+
#
|
920
|
+
def fail_with message
|
921
|
+
raise Failure, message
|
922
|
+
end
|
923
|
+
|
924
|
+
##
|
925
|
+
# :method: assert
|
926
|
+
# :args: desc, &
|
927
|
+
#
|
928
|
+
# Assert a specific condition. If a block is given it creates an
|
929
|
+
# +EvaluationContext+ and its methods are available. If a failure is reported
|
930
|
+
# within this block, the run will be *aborted* at the end of the block.
|
931
|
+
#
|
932
|
+
# foo = 'bar'
|
933
|
+
#
|
934
|
+
# assert foo.to be 'bar'
|
935
|
+
#
|
936
|
+
# assert 'a certain condition to be true' do
|
937
|
+
# report 'it was not' if foo == 'bar'
|
938
|
+
# end
|
939
|
+
#
|
940
|
+
|
941
|
+
##
|
942
|
+
# :method: expect
|
943
|
+
# :args: desc, &
|
944
|
+
#
|
945
|
+
# Expect a specific condition. If a block is given it creates an
|
946
|
+
# +EvaluationContext+ and its methods are available. If a failure is reported
|
947
|
+
# within this block, the run will *continue*.
|
948
|
+
#
|
949
|
+
# foo = 'bar'
|
950
|
+
#
|
951
|
+
# expect foo.to be 'bar'
|
952
|
+
#
|
953
|
+
# expect 'a certain condition to be true' do
|
954
|
+
# report 'it was not' if foo == 'bar'
|
955
|
+
# end
|
956
|
+
#
|
957
|
+
|
958
|
+
%i[assert expect].each do |method|
|
959
|
+
define_method(method) do |evaluation, &block|
|
960
|
+
desc = "#{method} #{evaluation}"
|
961
|
+
|
962
|
+
@evaluations << if block
|
963
|
+
EvaluationContext.new(@engine, desc, &block)
|
964
|
+
else
|
965
|
+
EvaluationContext.new(@engine, desc) do
|
966
|
+
unless evaluation.failure.nil?
|
967
|
+
@failures << Failure.new(
|
968
|
+
evaluation.failure,
|
969
|
+
evaluation.call_location
|
970
|
+
)
|
971
|
+
end
|
972
|
+
end
|
973
|
+
end
|
974
|
+
|
975
|
+
raise AbortException if method == :assert and @evaluations.any? { |x| x.failures.any? }
|
274
976
|
end
|
977
|
+
end
|
275
978
|
|
276
|
-
|
979
|
+
##
|
980
|
+
# Adds the given key-value arguments to the run report.
|
981
|
+
# Use this to add generated values during the run to the test report.
|
982
|
+
#
|
983
|
+
# property key: 'value'
|
984
|
+
#
|
985
|
+
def property **kwargs
|
986
|
+
@properties.merge!(kwargs)
|
987
|
+
end
|
988
|
+
|
989
|
+
##
|
990
|
+
# Takes a block and measures the time of the execution.
|
991
|
+
# The duration in seconds is available through +duration+.
|
992
|
+
#
|
993
|
+
# measure do
|
994
|
+
# info 'do some long running stuff'
|
995
|
+
# sleep 1
|
996
|
+
# end
|
997
|
+
#
|
998
|
+
# info "run duration was #{duration}"
|
999
|
+
#
|
1000
|
+
def measure
|
1001
|
+
start_time = Time.now
|
1002
|
+
yield
|
1003
|
+
end_time = Time.now
|
1004
|
+
|
1005
|
+
@measured_duration = end_time - start_time
|
1006
|
+
end
|
277
1007
|
|
278
|
-
|
1008
|
+
##
|
1009
|
+
# returns::
|
1010
|
+
# The duration of the previously executed +measure+ block.
|
1011
|
+
#
|
1012
|
+
# The duration optained by +measure+.
|
1013
|
+
#
|
1014
|
+
def duration
|
1015
|
+
@measured_duration
|
1016
|
+
end
|
279
1017
|
|
280
|
-
|
1018
|
+
##
|
1019
|
+
# Executes the given block asyncronously in a new thread.
|
1020
|
+
# This thread can be awaited with +await+ and the given name.
|
1021
|
+
# You can start multiple threads with the same name.
|
1022
|
+
# Using this name with +await+ will wait for all threads to finish.
|
1023
|
+
#
|
1024
|
+
# The last line in the +async+ block will be returned as a result.
|
1025
|
+
# and is available when +await+ ing the threads.
|
1026
|
+
#
|
1027
|
+
# async do
|
1028
|
+
# info 'do some async stuff'
|
1029
|
+
#
|
1030
|
+
# 'a result'
|
1031
|
+
# end
|
1032
|
+
#
|
1033
|
+
# result = await
|
1034
|
+
#
|
1035
|
+
def async(name = DEFAULT_ASYNC_NAME, &)
|
1036
|
+
@threads[name] ||= []
|
1037
|
+
@threads[name] << Thread.new(&)
|
281
1038
|
end
|
282
1039
|
|
283
|
-
|
284
|
-
|
1040
|
+
##
|
1041
|
+
# returns:: An +Array+ of previously executed +async+ blocks
|
1042
|
+
# with the same name.
|
1043
|
+
#
|
1044
|
+
# Waits for the threads created with +async+ to finish.
|
1045
|
+
# Awaits all threads with the given name.
|
1046
|
+
#
|
1047
|
+
def await name = DEFAULT_ASYNC_NAME
|
1048
|
+
return unless @threads.key? name
|
285
1049
|
|
286
|
-
|
1050
|
+
threads = @threads[name].map(&:join)
|
287
1051
|
|
288
|
-
|
1052
|
+
@threads.delete(name)
|
289
1053
|
|
290
|
-
|
291
|
-
|
292
|
-
before_ctx = SpecContext.new(spec.subject, 'before', spec.context)
|
1054
|
+
threads.map(&:join)
|
1055
|
+
end
|
293
1056
|
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
1057
|
+
##
|
1058
|
+
# Groups code in a block.
|
1059
|
+
# This is solely used for structuring tests
|
1060
|
+
# and will not have effect on test reports.
|
1061
|
+
#
|
1062
|
+
def group(desc, &)
|
1063
|
+
@engine.logger.correlate do
|
1064
|
+
@engine.logger.debug("group \"#{desc}\"")
|
1065
|
+
@engine.formatter.scope(desc, :group, &)
|
1066
|
+
end
|
1067
|
+
end
|
1068
|
+
|
1069
|
+
##
|
1070
|
+
# Observes the given block and catches all errors
|
1071
|
+
# within this block. If errors or failures occur
|
1072
|
+
# the test will not fail or aborted.
|
1073
|
+
#
|
1074
|
+
# The result of the observation can be retreived
|
1075
|
+
# through +success?+
|
1076
|
+
#
|
1077
|
+
# observe do
|
1078
|
+
# info 'do some stuff'
|
1079
|
+
# end
|
1080
|
+
#
|
1081
|
+
# assert success?.to be true
|
1082
|
+
#
|
1083
|
+
# observe do
|
1084
|
+
# raise StandardError, 'Oops!'
|
1085
|
+
# end
|
1086
|
+
#
|
1087
|
+
# assert success?.to be false
|
1088
|
+
#
|
1089
|
+
def observe desc
|
1090
|
+
@engine.formatter.log(:info, "observe #{desc}") do
|
1091
|
+
yield
|
1092
|
+
@success = true
|
1093
|
+
[:info, :ok, nil]
|
1094
|
+
rescue StandardError => e
|
1095
|
+
@success = false
|
1096
|
+
@engine.logger.warn("#{e.message}\n#{e.backtrace.join("\n")}")
|
1097
|
+
[:info, :warn, e.message]
|
1098
|
+
end
|
1099
|
+
end
|
1100
|
+
|
1101
|
+
##
|
1102
|
+
# Returns the status of the pervious +observe+ block
|
1103
|
+
#
|
1104
|
+
def success?
|
1105
|
+
@success.nil? || @success
|
1106
|
+
end
|
1107
|
+
|
1108
|
+
##
|
1109
|
+
# Run the mixin with the given name and parameters
|
1110
|
+
#
|
1111
|
+
# run 'additional actions' do
|
1112
|
+
# with some_param: 42,
|
1113
|
+
# another_param: 'foo'
|
1114
|
+
# end
|
1115
|
+
#
|
1116
|
+
def run(desc, with: nil, &)
|
1117
|
+
@engine.formatter.scope(desc, :mixin) do
|
1118
|
+
raise "mixin \"#{desc}\" not found" unless @engine.mixins.key? desc
|
1119
|
+
|
1120
|
+
mixin = @engine.mixins[desc]
|
1121
|
+
mixin.instance_eval(&) if block_given?
|
1122
|
+
|
1123
|
+
@engine.logger.correlate do
|
1124
|
+
@engine.logger.debug("execute mixin \"#{desc}\"")
|
1125
|
+
result = mixin.run(self, with)
|
1126
|
+
return result.is_a?(Hash) ? OpenStruct.new(result) : result
|
299
1127
|
end
|
1128
|
+
end
|
1129
|
+
end
|
300
1130
|
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
1131
|
+
##
|
1132
|
+
# Add this alias to construct prettier and more readable mixin execution calls.
|
1133
|
+
#
|
1134
|
+
alias also run
|
1135
|
+
|
1136
|
+
##
|
1137
|
+
# Skip the run for this spec. This can be used to skip spec runs when a certain
|
1138
|
+
# condition occurs.
|
1139
|
+
#
|
1140
|
+
# skip 'subject is not yet ready to be tests' unless service_is_ready()
|
1141
|
+
#
|
1142
|
+
def skip message
|
1143
|
+
@skipped = true
|
1144
|
+
@engine.logger.info("#{message} - canceled by user")
|
1145
|
+
raise AbortException
|
1146
|
+
end
|
1147
|
+
end
|
1148
|
+
|
1149
|
+
class Specification
|
1150
|
+
attr_reader :id, :name, :desc, :full_desc, :parent, :root, :tags, :data, :file
|
1151
|
+
|
1152
|
+
def initialize parent, name, desc, tags, data, file, block
|
1153
|
+
@parent = parent
|
1154
|
+
@root = parent.root
|
1155
|
+
@name = name
|
1156
|
+
@desc = desc
|
1157
|
+
@tags = tags
|
1158
|
+
@data = data
|
1159
|
+
@file = file
|
1160
|
+
@block = block
|
1161
|
+
@full_desc = "#{@parent.full_desc} #{@desc}"
|
1162
|
+
end
|
316
1163
|
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
1164
|
+
##
|
1165
|
+
# Creates a new +RunContext+ and executes the spec,
|
1166
|
+
# +before+ and +after+ blocks
|
1167
|
+
#
|
1168
|
+
def run engine, befores, afters, bag
|
1169
|
+
RunContext.new(engine, self, :spec, bag) do |run_context|
|
1170
|
+
engine.formatter.scope(@desc, self) do
|
1171
|
+
befores.each do |block|
|
1172
|
+
engine.formatter.scope('before', :before) do
|
1173
|
+
engine.logger.correlate do
|
1174
|
+
run_context.execute(@data, &block)
|
321
1175
|
end
|
1176
|
+
end
|
1177
|
+
end
|
322
1178
|
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
1179
|
+
run_context.execute(@data, &@block) if run_context.status == :success
|
1180
|
+
ensure
|
1181
|
+
afters.each do |block|
|
1182
|
+
engine.formatter.scope('after', :after) do
|
1183
|
+
engine.logger.correlate do
|
1184
|
+
run_context.execute(@data, &block)
|
1185
|
+
end
|
329
1186
|
end
|
330
1187
|
end
|
331
1188
|
end
|
332
1189
|
end
|
1190
|
+
end
|
1191
|
+
end
|
333
1192
|
|
334
|
-
|
1193
|
+
class DefinitionContext
|
1194
|
+
include Delegate
|
335
1195
|
|
336
|
-
|
1196
|
+
attr_reader :id, :name, :desc, :parent, :full_desc, :children, :specs, :file
|
337
1197
|
|
338
|
-
|
339
|
-
|
340
|
-
|
1198
|
+
def initialize desc, file, parent = nil
|
1199
|
+
@parent = parent
|
1200
|
+
@desc = desc
|
1201
|
+
@file = file
|
1202
|
+
@children = []
|
1203
|
+
@specs = []
|
341
1204
|
|
1205
|
+
@setups = []
|
1206
|
+
@teardowns = []
|
342
1207
|
|
343
|
-
|
344
|
-
|
345
|
-
###########################################
|
1208
|
+
@befores = []
|
1209
|
+
@afters = []
|
346
1210
|
|
1211
|
+
@name = @desc.downcase.gsub(/[^a-z0-9]+/, '_')
|
1212
|
+
@name = @parent.name + '-' + @name unless @parent.nil?
|
347
1213
|
|
348
|
-
|
349
|
-
|
1214
|
+
@full_desc = @parent.nil? ? @desc : "#{@parent.full_desc} #{@desc}"
|
1215
|
+
end
|
350
1216
|
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
1217
|
+
##
|
1218
|
+
# The root context aka test subject.
|
1219
|
+
#
|
1220
|
+
def root
|
1221
|
+
@parent ? @parent.root : self
|
1222
|
+
end
|
355
1223
|
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
1224
|
+
##
|
1225
|
+
# A flattened list of all specs including those of child contexts.
|
1226
|
+
#
|
1227
|
+
def all_specs
|
1228
|
+
@specs + @children.map(&:all_specs).flatten
|
360
1229
|
end
|
361
1230
|
|
362
|
-
|
363
|
-
|
1231
|
+
##
|
1232
|
+
# Creates a new sub context with the given block.
|
1233
|
+
#
|
1234
|
+
def context(desc, &)
|
1235
|
+
file = caller
|
1236
|
+
.first
|
1237
|
+
.gsub(/:in .*/, '')
|
1238
|
+
.gsub(Dir.pwd, '.')
|
1239
|
+
|
1240
|
+
context = DefinitionContext.new(desc, file, self)
|
1241
|
+
@children << context
|
1242
|
+
context.instance_eval(&)
|
1243
|
+
end
|
364
1244
|
|
365
|
-
|
1245
|
+
##
|
1246
|
+
# Adds a setup block which will be executed
|
1247
|
+
# once at the beginning of a context.
|
1248
|
+
# Multiple setups are allowed.
|
1249
|
+
#
|
1250
|
+
def setup &block
|
1251
|
+
@setups << block
|
366
1252
|
end
|
367
1253
|
|
1254
|
+
##
|
1255
|
+
# Adds a teardown block which will be executed
|
1256
|
+
# once at the end of a context.
|
1257
|
+
# Multiple teardowns are allowed.
|
1258
|
+
#
|
1259
|
+
def teardown &block
|
1260
|
+
@teardowns << block
|
1261
|
+
end
|
1262
|
+
|
1263
|
+
##
|
1264
|
+
# Creates a new block which will be executed *before*
|
1265
|
+
# each +it+ block.
|
1266
|
+
#
|
368
1267
|
def before &block
|
369
|
-
@
|
1268
|
+
@befores << block
|
370
1269
|
end
|
371
1270
|
|
1271
|
+
##
|
1272
|
+
# Creates a new block which will be executed *after*
|
1273
|
+
# each +it+ block. These blocks are ensured to be executed,
|
1274
|
+
# even on failure or error.
|
1275
|
+
#
|
372
1276
|
def after &block
|
373
|
-
@
|
1277
|
+
@afters << block
|
374
1278
|
end
|
375
1279
|
|
376
|
-
|
377
|
-
|
378
|
-
|
1280
|
+
##
|
1281
|
+
# Creates a new specifiction (test). This is the main
|
1282
|
+
# building block of a spectre tests and contains all test logic.
|
1283
|
+
#
|
1284
|
+
def it desc, tags: [], with: nil, &block
|
1285
|
+
file = caller
|
1286
|
+
.first
|
1287
|
+
.gsub(/:in .*/, '')
|
1288
|
+
.gsub(Dir.pwd, '.')
|
379
1289
|
|
380
|
-
|
381
|
-
@__setup_blocks << Spec.new(name, @__subject, 'setup', [], nil, block, setup_ctx, spec_file, line)
|
382
|
-
end
|
1290
|
+
with ||= [nil]
|
383
1291
|
|
384
|
-
|
385
|
-
|
386
|
-
|
1292
|
+
with.each_with_index do |data, _index|
|
1293
|
+
spec_index = root.all_specs.count + 1
|
1294
|
+
name = "#{root.name}-#{spec_index}"
|
387
1295
|
|
388
|
-
|
389
|
-
@__teardown_blocks << Spec.new(name, @__subject, 'teardown', [], nil, block, teardown_ctx, spec_file, line)
|
390
|
-
end
|
1296
|
+
spec = Specification.new(self, name, desc, tags, data, file, block)
|
391
1297
|
|
392
|
-
|
393
|
-
|
394
|
-
ctx._evaluate &block
|
1298
|
+
@specs << spec
|
1299
|
+
end
|
395
1300
|
end
|
396
1301
|
|
397
|
-
|
1302
|
+
# :nodoc:
|
1303
|
+
def run engine, specs
|
1304
|
+
runs = []
|
1305
|
+
|
1306
|
+
return runs unless all_specs.any? { |x| specs.include? x }
|
1307
|
+
|
1308
|
+
selected = @specs.select { |x| specs.include? x }
|
398
1309
|
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
1310
|
+
engine.formatter.scope(@desc, self) do
|
1311
|
+
if selected.any?
|
1312
|
+
setup_bag = nil
|
1313
|
+
|
1314
|
+
if @setups.any?
|
1315
|
+
setup_run = RunContext.new(engine, self, :setup) do |run_context|
|
1316
|
+
@setups.each do |block|
|
1317
|
+
engine.formatter.scope('setup', :setup) do
|
1318
|
+
engine.logger.correlate do
|
1319
|
+
engine.logger.debug("setup \"#{@desc}\"")
|
1320
|
+
run_context.execute(nil, &block)
|
1321
|
+
end
|
1322
|
+
end
|
1323
|
+
end
|
1324
|
+
end
|
1325
|
+
|
1326
|
+
setup_bag = setup_run.bag
|
1327
|
+
|
1328
|
+
runs << setup_run
|
1329
|
+
end
|
1330
|
+
|
1331
|
+
# Only run specs if setup was successful
|
1332
|
+
if runs.all? { |x| x.status == :success }
|
1333
|
+
runs += selected.map do |spec|
|
1334
|
+
engine.logger.correlate do
|
1335
|
+
spec.run(engine, @befores, @afters, setup_bag)
|
1336
|
+
end
|
1337
|
+
end
|
1338
|
+
end
|
1339
|
+
|
1340
|
+
if @teardowns.any?
|
1341
|
+
runs << RunContext.new(engine, self, :teardown, setup_bag) do |run_context|
|
1342
|
+
@teardowns.each do |block|
|
1343
|
+
engine.formatter.scope('teardown', :teardown) do
|
1344
|
+
engine.logger.correlate do
|
1345
|
+
engine.logger.debug("teardown \"#{@desc}\"")
|
1346
|
+
run_context.execute(nil, &block)
|
1347
|
+
end
|
1348
|
+
end
|
1349
|
+
end
|
1350
|
+
end
|
1351
|
+
end
|
1352
|
+
end
|
1353
|
+
|
1354
|
+
@children.each do |context|
|
1355
|
+
engine.logger.correlate do
|
1356
|
+
runs += context.run(engine, specs)
|
1357
|
+
end
|
1358
|
+
end
|
1359
|
+
end
|
1360
|
+
|
1361
|
+
runs
|
404
1362
|
end
|
405
1363
|
end
|
406
1364
|
|
1365
|
+
##
|
1366
|
+
# Defines the default config.
|
1367
|
+
#
|
1368
|
+
CONFIG = {
|
1369
|
+
# Directory used as the working directory. All paths
|
1370
|
+
# in the config are relative to this directory.
|
1371
|
+
'work_dir' => '.',
|
1372
|
+
# Path to the global spectre config, which will *always* be loaded.
|
1373
|
+
'global_config_file' => '~/.config/spectre.yml',
|
1374
|
+
# The name of the project config file
|
1375
|
+
'config_file' => 'spectre.yml',
|
1376
|
+
# The log file name. Use <date> placeholder to add the run date.
|
1377
|
+
'log_file' => nil, # Deactivate logging by default
|
1378
|
+
# The log date format
|
1379
|
+
'log_date_format' => '%F %T.%L%:z',
|
1380
|
+
# Format: [timestamp] LEVEL -- module_name: [spec-id] correlation_id log_message
|
1381
|
+
'log_message_format' => "[%s] %5s -- %s: [%s] [%s] %s\n",
|
1382
|
+
# The output formatter to use when running specs
|
1383
|
+
'formatter' => 'Spectre::SimpleFormatter',
|
1384
|
+
# The reporters to use for generating tests results.
|
1385
|
+
'reporters' => ['Spectre::SimpleReporter'],
|
1386
|
+
# The path where reports are placed.
|
1387
|
+
'out_path' => 'reports',
|
1388
|
+
# A list of spec names to run. This will usually be set by command line option.
|
1389
|
+
'specs' => [],
|
1390
|
+
# A list of tags to run. This will usually be set by command line option.
|
1391
|
+
'tags' => [],
|
1392
|
+
# Debug mode. Outputs +debug+ log and more detailed error messages.
|
1393
|
+
'debug' => false,
|
1394
|
+
'env_patterns' => ['environments/**/*.env.yml'],
|
1395
|
+
# The patterns to use when loading environment files.
|
1396
|
+
'env_partial_patterns' => ['environments/**/*.env.secret.yml'],
|
1397
|
+
# The patterns to use when loading spec files.
|
1398
|
+
'spec_patterns' => ['specs/**/*.spec.rb'],
|
1399
|
+
# The patterns to use when loading mixin files.
|
1400
|
+
'mixin_patterns' => ['mixins/**/*.mixin.rb'],
|
1401
|
+
# The patterns to use when loading collection files.
|
1402
|
+
'collections_patterns' => ['**/*.collections.yml'],
|
1403
|
+
# Paths to folders containing resource files
|
1404
|
+
'resource_paths' => ['../common/resources', './resources'],
|
1405
|
+
# A list of modules and/or gems to load when running tests
|
1406
|
+
'modules' => [],
|
1407
|
+
}
|
1408
|
+
|
1409
|
+
##
|
1410
|
+
# The default environment name, when not set in `env` file
|
1411
|
+
# or as selected `env`
|
1412
|
+
#
|
1413
|
+
DEFAULT_ENV_NAME = 'default'
|
1414
|
+
|
1415
|
+
class Engine
|
1416
|
+
attr_reader :env, :formatter, :config, :contexts, :mixins, :collections, :resources
|
1417
|
+
|
1418
|
+
@@current = nil
|
1419
|
+
@@modules = []
|
1420
|
+
|
1421
|
+
##
|
1422
|
+
# The current used engine
|
1423
|
+
#
|
1424
|
+
def self.current
|
1425
|
+
@@current
|
1426
|
+
end
|
1427
|
+
|
1428
|
+
##
|
1429
|
+
# Register a class and methods, which should be
|
1430
|
+
# available in all spectre scopes
|
1431
|
+
#
|
1432
|
+
def self.register cls, *methods
|
1433
|
+
@@modules << [cls, methods]
|
1434
|
+
end
|
1435
|
+
|
1436
|
+
def initialize config
|
1437
|
+
@environments = {}
|
1438
|
+
@collections = {}
|
1439
|
+
@contexts = []
|
1440
|
+
@mixins = {}
|
1441
|
+
@resources = {}
|
1442
|
+
@delegates = {}
|
1443
|
+
|
1444
|
+
@config = Marshal.load(Marshal.dump(CONFIG))
|
1445
|
+
|
1446
|
+
# Load global config file
|
1447
|
+
global_config_file = config['global_config_file'] || File.expand_path('~/.config/spectre.yml')
|
1448
|
+
|
1449
|
+
if File.exist? global_config_file
|
1450
|
+
global_config = load_yaml(global_config_file)
|
1451
|
+
@config.deep_merge!(global_config)
|
1452
|
+
end
|
407
1453
|
|
408
|
-
|
409
|
-
|
410
|
-
|
1454
|
+
# Set working directory so all paths in config
|
1455
|
+
# are relative to this directory
|
1456
|
+
Dir.chdir(config['work_dir'] || @config['work_dir'] || '.')
|
411
1457
|
|
1458
|
+
# Load main spectre config
|
1459
|
+
main_config_file = config['config_file'] || @config['config_file']
|
412
1460
|
|
413
|
-
|
414
|
-
|
1461
|
+
unless main_config_file.nil? or !File.exist? main_config_file
|
1462
|
+
main_config = load_yaml(main_config_file)
|
1463
|
+
@config.deep_merge!(main_config)
|
1464
|
+
Dir.chdir(File.dirname(main_config_file))
|
1465
|
+
end
|
415
1466
|
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
1467
|
+
# Load environments
|
1468
|
+
@config['env_patterns'].each do |pattern|
|
1469
|
+
Dir.glob(pattern).each do |file_path|
|
1470
|
+
loaded_env = load_yaml(file_path)
|
1471
|
+
env_name = loaded_env['name'] || DEFAULT_ENV_NAME
|
1472
|
+
@environments[env_name] = loaded_env
|
1473
|
+
end
|
1474
|
+
end
|
420
1475
|
|
421
|
-
|
1476
|
+
# Load and merge partial environment files
|
1477
|
+
@config['env_partial_patterns'].each do |pattern|
|
1478
|
+
Dir.glob(pattern).each do |file_path|
|
1479
|
+
loaded_env = load_yaml(file_path)
|
1480
|
+
env_name = loaded_env['name'] || DEFAULT_ENV_NAME
|
1481
|
+
@environments[env_name].deep_merge!(loaded_env) if @environments.key?(env_name)
|
422
1482
|
end
|
1483
|
+
end
|
423
1484
|
|
424
|
-
|
1485
|
+
# Select environment and merge it
|
1486
|
+
@config.deep_merge!(@environments[config.delete('selected_env') || DEFAULT_ENV_NAME])
|
425
1487
|
|
426
|
-
|
1488
|
+
# Load collections
|
1489
|
+
@config['collections_patterns'].each do |pattern|
|
1490
|
+
Dir.glob(pattern).each do |file_path|
|
1491
|
+
@collections.merge! load_yaml(file_path)
|
1492
|
+
end
|
1493
|
+
end
|
1494
|
+
|
1495
|
+
# Use collection if given
|
1496
|
+
if config.key? 'collection'
|
1497
|
+
collection = @collections[config['collection']]
|
1498
|
+
|
1499
|
+
raise "collection #{config['collection']} not found" unless collection
|
1500
|
+
|
1501
|
+
@config.deep_merge!(collection)
|
427
1502
|
end
|
428
|
-
end
|
429
1503
|
|
430
|
-
|
431
|
-
|
432
|
-
raise SpectreError.new("no variable or method '#{method_name}' found") unless target.respond_to? method_name
|
1504
|
+
# Merge property overrides
|
1505
|
+
@config.deep_merge!(config)
|
433
1506
|
|
434
|
-
|
1507
|
+
# Set env before loading specs in order to make it available in spec definitions
|
1508
|
+
@env = @config.to_recursive_struct
|
1509
|
+
|
1510
|
+
# Load specs
|
1511
|
+
# Note that spec files are only loaded once, because of the relative require,
|
1512
|
+
# even if the setup function is called multiple times
|
1513
|
+
load_files(@config['spec_patterns'])
|
1514
|
+
|
1515
|
+
# Load mixins
|
1516
|
+
# Mixins are also only loaded once
|
1517
|
+
load_files(@config['mixin_patterns'])
|
1518
|
+
|
1519
|
+
# Load resources
|
1520
|
+
@config['resource_paths'].each do |resource_path|
|
1521
|
+
resource_files = Dir.glob File.join(resource_path, '**/*')
|
1522
|
+
|
1523
|
+
resource_files.each do |file|
|
1524
|
+
file.slice! resource_path
|
1525
|
+
file = file[1..]
|
1526
|
+
@resources[file] = File.expand_path File.join(resource_path, file)
|
1527
|
+
end
|
1528
|
+
end
|
1529
|
+
|
1530
|
+
@formatter = Object
|
1531
|
+
.const_get(@config['formatter'])
|
1532
|
+
.new(@config)
|
1533
|
+
|
1534
|
+
# Load modules
|
1535
|
+
return unless @config['modules'].is_a? Array
|
1536
|
+
|
1537
|
+
@config['modules'].each do |module_name|
|
1538
|
+
module_path = File.join(Dir.pwd, module_name)
|
1539
|
+
|
1540
|
+
if File.exist? module_path
|
1541
|
+
require_relative module_path
|
1542
|
+
else
|
1543
|
+
require module_name
|
1544
|
+
end
|
1545
|
+
end
|
435
1546
|
end
|
436
|
-
end
|
437
1547
|
|
1548
|
+
# :nodoc:
|
1549
|
+
def respond_to_missing?(method, *)
|
1550
|
+
@delegates.key? method
|
1551
|
+
end
|
438
1552
|
|
439
|
-
|
440
|
-
|
441
|
-
|
1553
|
+
# :nodoc:
|
1554
|
+
def method_missing(method, *, **, &)
|
1555
|
+
@delegates[method]&.send(method, *, **, &)
|
1556
|
+
end
|
442
1557
|
|
443
|
-
|
444
|
-
|
1558
|
+
# :nodoc:
|
1559
|
+
def logger
|
1560
|
+
@logger ||= Logger.new(@config, progname: 'spectre')
|
445
1561
|
end
|
446
1562
|
|
447
|
-
|
448
|
-
|
449
|
-
|
1563
|
+
##
|
1564
|
+
# Get a list of specs with the configured filter
|
1565
|
+
#
|
1566
|
+
def list config = @config
|
1567
|
+
spec_filter = config['specs'] || []
|
1568
|
+
tag_filter = config['tags'] || []
|
1569
|
+
|
1570
|
+
@contexts
|
1571
|
+
.map(&:all_specs)
|
450
1572
|
.flatten
|
451
1573
|
.select do |spec|
|
452
|
-
(spec_filter.empty?
|
1574
|
+
(spec_filter.empty? and tag_filter.empty?) or
|
1575
|
+
spec_filter.any? { |x| spec.name.match?("^#{x.gsub('*', '.*')}$") } or
|
1576
|
+
tag_filter.any? { |x| tag?(spec.tags, x) }
|
453
1577
|
end
|
454
1578
|
end
|
455
1579
|
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
end
|
1580
|
+
##
|
1581
|
+
# Runs specs with the current config
|
1582
|
+
#
|
1583
|
+
def run
|
1584
|
+
@@modules.each do |mod, methods|
|
1585
|
+
target = mod.respond_to?(:new) ? mod.new(@config, logger) : mod
|
463
1586
|
|
464
|
-
|
465
|
-
|
466
|
-
|
1587
|
+
methods.each do |method|
|
1588
|
+
@delegates[method] = target
|
1589
|
+
end
|
1590
|
+
end
|
467
1591
|
|
468
|
-
|
469
|
-
|
1592
|
+
list
|
1593
|
+
.group_by { |x| x.parent.root }
|
1594
|
+
.map do |context, specs|
|
1595
|
+
context.run(self, specs)
|
1596
|
+
end
|
1597
|
+
.flatten
|
1598
|
+
rescue Interrupt
|
1599
|
+
# Do nothing here
|
470
1600
|
end
|
471
1601
|
|
472
|
-
|
473
|
-
|
474
|
-
|
1602
|
+
##
|
1603
|
+
# Create a report with the given runs and configured reporter.
|
1604
|
+
#
|
1605
|
+
def report runs
|
1606
|
+
@config['reporters'].each do |reporter|
|
1607
|
+
Object.const_get(reporter)
|
1608
|
+
.new(@config)
|
1609
|
+
.report(runs)
|
475
1610
|
end
|
476
1611
|
end
|
477
1612
|
|
478
|
-
|
479
|
-
|
480
|
-
|
1613
|
+
##
|
1614
|
+
# Cleanup temporary files like logs, etc.
|
1615
|
+
#
|
1616
|
+
def cleanup
|
1617
|
+
Dir.chdir(@config['work_dir'])
|
1618
|
+
log_file_pattern = @config['log_file'].gsub('<date>', '*')
|
1619
|
+
FileUtils.rm_rf(Dir.glob(log_file_pattern), secure: true)
|
481
1620
|
end
|
482
1621
|
|
1622
|
+
##
|
1623
|
+
# Describe a test subject
|
1624
|
+
#
|
1625
|
+
def describe(name, &)
|
1626
|
+
file = caller
|
1627
|
+
.first
|
1628
|
+
.gsub(/:in .*/, '')
|
1629
|
+
.gsub(Dir.pwd, '.')
|
1630
|
+
|
1631
|
+
main_context = @contexts.find { |x| x.desc == name }
|
483
1632
|
|
484
|
-
|
485
|
-
|
486
|
-
|
1633
|
+
if main_context.nil?
|
1634
|
+
main_context = DefinitionContext.new(name, file)
|
1635
|
+
@contexts << main_context
|
1636
|
+
end
|
487
1637
|
|
1638
|
+
main_context.instance_eval(&)
|
1639
|
+
end
|
1640
|
+
|
1641
|
+
##
|
1642
|
+
# Registers a mixin
|
1643
|
+
#
|
1644
|
+
def mixin desc, params: [], &block
|
1645
|
+
file, line = get_call_location(caller_locations)
|
1646
|
+
@mixins[desc] = Mixin.new(desc, params, block, file, line)
|
1647
|
+
end
|
1648
|
+
|
1649
|
+
private
|
488
1650
|
|
489
|
-
def
|
490
|
-
|
1651
|
+
def load_files patterns
|
1652
|
+
@@current = self
|
491
1653
|
|
492
|
-
|
493
|
-
|
494
|
-
|
1654
|
+
patterns.each do |pattern|
|
1655
|
+
Dir.glob(pattern).each do |file|
|
1656
|
+
content = File.read File.absolute_path(file)
|
1657
|
+
instance_eval(content, file, 1)
|
1658
|
+
end
|
495
1659
|
end
|
496
1660
|
|
497
|
-
|
498
|
-
ctx._evaluate &block
|
1661
|
+
@@current = nil
|
499
1662
|
end
|
500
1663
|
|
501
|
-
def
|
502
|
-
|
1664
|
+
def load_yaml file_path
|
1665
|
+
YAML.safe_load_file(file_path, aliases: true) || {}
|
503
1666
|
end
|
504
1667
|
|
505
|
-
def
|
506
|
-
|
1668
|
+
def tag? tags, tag_exp
|
1669
|
+
tags = tags.map(&:to_s)
|
1670
|
+
all_tags = tag_exp.split('+')
|
1671
|
+
|
1672
|
+
included_tags = all_tags.reject { |x| x.start_with? '!' }
|
1673
|
+
|
1674
|
+
excluded_tags = all_tags
|
1675
|
+
.select { |x| x.start_with? '!' }
|
1676
|
+
.map { |x| x[1..] }
|
1677
|
+
|
1678
|
+
included_tags & tags == included_tags and excluded_tags & tags == []
|
507
1679
|
end
|
508
1680
|
end
|
509
1681
|
|
510
|
-
|
1682
|
+
# Delegate methods to specific classes or instances
|
1683
|
+
# to be available in descending block
|
1684
|
+
[
|
1685
|
+
[Assertion, :be, :be_empty, :contain, :match],
|
1686
|
+
[Helpers, :uuid, :now],
|
1687
|
+
].each do |args|
|
1688
|
+
Engine.register(*args)
|
1689
|
+
end
|
511
1690
|
end
|
512
|
-
|
513
|
-
|
514
|
-
extend Spectre::Delegator
|