lopata 0.1.13 → 0.1.14
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/README.md +25 -25
- data/exe/lopata +11 -11
- data/lib/lopata.rb +74 -74
- data/lib/lopata/active_record.rb +135 -135
- data/lib/lopata/condition.rb +30 -30
- data/lib/lopata/configuration.rb +125 -125
- data/lib/lopata/environment.rb +35 -35
- data/lib/lopata/factory_bot.rb +72 -72
- data/lib/lopata/generators/app.rb +42 -42
- data/lib/lopata/generators/templates/Gemfile +7 -7
- data/lib/lopata/generators/templates/Lopatafile +20 -20
- data/lib/lopata/generators/templates/config/environments/qa.yml +7 -7
- data/lib/lopata/generators/templates/config/initializers/capybara.rb +1 -1
- data/lib/lopata/id.rb +22 -22
- data/lib/lopata/loader.rb +31 -31
- data/lib/lopata/observers.rb +4 -4
- data/lib/lopata/observers/backtrace_formatter.rb +103 -103
- data/lib/lopata/observers/base_observer.rb +33 -33
- data/lib/lopata/observers/console_output_observer.rb +100 -100
- data/lib/lopata/observers/web_logger.rb +130 -130
- data/lib/lopata/role.rb +109 -109
- data/lib/lopata/runner.rb +67 -67
- data/lib/lopata/scenario.rb +136 -136
- data/lib/lopata/scenario_builder.rb +497 -497
- data/lib/lopata/shared_step.rb +38 -38
- data/lib/lopata/step.rb +191 -191
- data/lib/lopata/version.rb +6 -6
- data/lib/lopata/world.rb +24 -24
- metadata +4 -4
@@ -1,497 +1,497 @@
|
|
1
|
-
# Context for scenario creation.
|
2
|
-
class Lopata::ScenarioBuilder
|
3
|
-
# @private
|
4
|
-
attr_reader :title, :common_metadata, :options, :diagonals
|
5
|
-
# @private
|
6
|
-
attr_accessor :shared_step, :group
|
7
|
-
|
8
|
-
# Defines one or more scenarios.
|
9
|
-
#
|
10
|
-
# @example
|
11
|
-
# Lopata.define 'scenario' do
|
12
|
-
# setup 'test user'
|
13
|
-
# action 'login'
|
14
|
-
# verify 'home page displayed'
|
15
|
-
# end
|
16
|
-
#
|
17
|
-
# Given block will be calculated in context of the ScenarioBuilder
|
18
|
-
#
|
19
|
-
# @param title [String] scenario unique title
|
20
|
-
# @param metadata [Hash] metadata to be used within the scenario
|
21
|
-
# @param block [Block] the scenario definition
|
22
|
-
# @see Lopata.define
|
23
|
-
def self.define(title, metadata = {}, &block)
|
24
|
-
builder = new(title, metadata)
|
25
|
-
builder.instance_exec &block
|
26
|
-
builder.build
|
27
|
-
end
|
28
|
-
|
29
|
-
# @private
|
30
|
-
def initialize(title, metadata = {})
|
31
|
-
@title, @common_metadata = title, metadata
|
32
|
-
@diagonals = []
|
33
|
-
@options = []
|
34
|
-
end
|
35
|
-
|
36
|
-
# @private
|
37
|
-
def build
|
38
|
-
filters = Lopata.configuration.filters
|
39
|
-
option_combinations.each do |option_set|
|
40
|
-
metadata = common_metadata.merge(option_set.metadata)
|
41
|
-
scenario = Lopata::Scenario::Execution.new(title, option_set.title, metadata)
|
42
|
-
|
43
|
-
unless filters.empty?
|
44
|
-
next unless filters.all? { |f| f[scenario] }
|
45
|
-
end
|
46
|
-
|
47
|
-
steps_with_hooks.each do |step|
|
48
|
-
next if step.condition && !step.condition.match?(scenario)
|
49
|
-
step.execution_steps(scenario).each { |s| scenario.steps << s }
|
50
|
-
end
|
51
|
-
|
52
|
-
world.scenarios << scenario
|
53
|
-
end
|
54
|
-
end
|
55
|
-
|
56
|
-
# @!group Defining variants
|
57
|
-
|
58
|
-
# Define option for the scenario.
|
59
|
-
#
|
60
|
-
# The scenario will be generated for all the options.
|
61
|
-
# If more then one option given, the scenarios for all options combinations will be generated.
|
62
|
-
#
|
63
|
-
# @param metadata_key [Symbol] the key to access option data via metadata.
|
64
|
-
# @param variants [Hash{String => Object}] variants for the option
|
65
|
-
# Keys are titles of the variant, values are metadata values.
|
66
|
-
#
|
67
|
-
# @example
|
68
|
-
# Lopata.define 'scenario' do
|
69
|
-
# option :one, 'one' => 1, 'two' => 2
|
70
|
-
# option :two, 'two' => 2, 'three' => 3
|
71
|
-
# # will generate 4 scenarios:
|
72
|
-
# # - 'scenario one two'
|
73
|
-
# # - 'scenario one three'
|
74
|
-
# # - 'scenario two two'
|
75
|
-
# # - 'scenario two three'
|
76
|
-
# end
|
77
|
-
#
|
78
|
-
# @see #diagonal
|
79
|
-
def option(metadata_key, variants)
|
80
|
-
@options << Option.new(metadata_key, variants)
|
81
|
-
end
|
82
|
-
|
83
|
-
# Define diagonal for the scenario.
|
84
|
-
#
|
85
|
-
# The scenario will be generated for all the variants of the diagonal.
|
86
|
-
# Each variant of diagonal will be selected for at least one scenario.
|
87
|
-
# It may be included in more then one scenario when other diagonal or option has more variants.
|
88
|
-
#
|
89
|
-
# @param metadata_key [Symbol] the key to access diagonal data via metadata.
|
90
|
-
# @param variants [Hash{String => Object}] variants for the diagonal.
|
91
|
-
# Keys are titles of the variant, values are metadata values.
|
92
|
-
#
|
93
|
-
# @example
|
94
|
-
# Lopata.define 'scenario' do
|
95
|
-
# option :one, 'one' => 1, 'two' => 2
|
96
|
-
# diagonal :two, 'two' => 2, 'three' => 3
|
97
|
-
# diagonal :three, 'three' => 3, 'four' => 4, 'five' => 5
|
98
|
-
# # will generate 3 scenarios:
|
99
|
-
# # - 'scenario one two three'
|
100
|
-
# # - 'scenario two three four'
|
101
|
-
# # - 'scenario one two five'
|
102
|
-
# end
|
103
|
-
#
|
104
|
-
# @see #option
|
105
|
-
def diagonal(metadata_key, variants)
|
106
|
-
@diagonals << Diagonal.new(metadata_key, variants)
|
107
|
-
end
|
108
|
-
|
109
|
-
# Define additional metadata for the scenario
|
110
|
-
#
|
111
|
-
# @example
|
112
|
-
# Lopata.define 'scenario' do
|
113
|
-
# metadata key: 'value'
|
114
|
-
# it 'metadata available' do
|
115
|
-
# expect(metadata[:key]).to eq 'value'
|
116
|
-
# end
|
117
|
-
# end
|
118
|
-
def metadata(hash)
|
119
|
-
raise 'metadata expected to be a Hash' unless hash.is_a?(Hash)
|
120
|
-
@common_metadata ||= {}
|
121
|
-
@common_metadata.merge! hash
|
122
|
-
end
|
123
|
-
|
124
|
-
# Skip scenario for given variants combination
|
125
|
-
#
|
126
|
-
# @example
|
127
|
-
# Lopata.define 'multiple options' do
|
128
|
-
# option :one, 'one' => 1, 'two' => 2
|
129
|
-
# option :two, 'two' => 2, 'three' => 3
|
130
|
-
# skip_when { |opt| opt.metadata[:one] == opt.metadata[:two] }
|
131
|
-
# it 'not equal' do
|
132
|
-
# expect(one).to_not eq two
|
133
|
-
# end
|
134
|
-
# end
|
135
|
-
#
|
136
|
-
def skip_when(&block)
|
137
|
-
@skip_when = block
|
138
|
-
end
|
139
|
-
|
140
|
-
# @private
|
141
|
-
def skip?(option_set)
|
142
|
-
@skip_when && @skip_when.call(option_set)
|
143
|
-
end
|
144
|
-
|
145
|
-
# @!endgroup
|
146
|
-
|
147
|
-
# @!group Defining Steps
|
148
|
-
|
149
|
-
# @private
|
150
|
-
# @macro [attach] define_step_method
|
151
|
-
# @!scope instance
|
152
|
-
# @method $1
|
153
|
-
def self.define_step_method(name)
|
154
|
-
name_if = "%s_if" % name
|
155
|
-
name_unless = "%s_unless" % name
|
156
|
-
define_method name, ->(*args, **metadata, &block) { add_step(name, *args, metadata: metadata, &block) }
|
157
|
-
define_method name_if, ->(condition, *args, **metadata, &block) {
|
158
|
-
add_step(name, *args, metadata: metadata, condition: Lopata::Condition.new(condition), &block)
|
159
|
-
}
|
160
|
-
define_method name_unless, ->(condition, *args, **metadata, &block) {
|
161
|
-
add_step(name, *args, condition: Lopata::Condition.new(condition, positive: false), metadata: metadata, &block)
|
162
|
-
}
|
163
|
-
end
|
164
|
-
|
165
|
-
# Define setup step.
|
166
|
-
# @example
|
167
|
-
# setup do
|
168
|
-
# end
|
169
|
-
#
|
170
|
-
# # setup from named shared step
|
171
|
-
# setup 'create user'
|
172
|
-
#
|
173
|
-
# # setup with both shared step and code block
|
174
|
-
# setup 'create user' do
|
175
|
-
# @user.update(admin: true)
|
176
|
-
# end
|
177
|
-
#
|
178
|
-
# Setup step used for set test data.
|
179
|
-
# @overload setup(*steps, &block)
|
180
|
-
# @param steps [Array<String, Symbol, Proc>] the steps to be runned as a part of setup.
|
181
|
-
# String - name of shared step to be called.
|
182
|
-
# Symbol - metadata key, referenced to shared step name.
|
183
|
-
# Proc - in-place step implementation.
|
184
|
-
# @param block [Block] The implementation of the step.
|
185
|
-
define_step_method :setup
|
186
|
-
|
187
|
-
# Define action step.
|
188
|
-
# @example
|
189
|
-
# action do
|
190
|
-
# end
|
191
|
-
#
|
192
|
-
# # action from named shared step
|
193
|
-
# action 'login'
|
194
|
-
#
|
195
|
-
# # setup with both shared step and code block
|
196
|
-
# action 'login', 'go dashboard' do
|
197
|
-
# @user.update(admin: true)
|
198
|
-
# end
|
199
|
-
#
|
200
|
-
# Action step is used for emulate user or external system action
|
201
|
-
#
|
202
|
-
# @overload action(*steps, &block)
|
203
|
-
# @param steps [Array<String, Symbol, Proc>] the steps to be runned as a part of action.
|
204
|
-
# String - name of shared step to be called.
|
205
|
-
# Symbol - metadata key, referenced to shared step name.
|
206
|
-
# Proc - in-place step implementation.
|
207
|
-
# @param block [Block] The implementation of the step.
|
208
|
-
define_step_method :action
|
209
|
-
|
210
|
-
# Define teardown step.
|
211
|
-
# @example
|
212
|
-
# setup { @user = User.create! }
|
213
|
-
# teardown { @user.destroy }
|
214
|
-
# Teardown step will be called at the end of scenario running.
|
215
|
-
# But it suggested to be decared right after setup or action step which require teardown.
|
216
|
-
#
|
217
|
-
# @overload teardown(*steps, &block)
|
218
|
-
# @param steps [Array<String, Symbol, Proc>] the steps to be runned as a part of teardown.
|
219
|
-
# String - name of shared step to be called.
|
220
|
-
# Symbol - metadata key, referenced to shared step name.
|
221
|
-
# Proc - in-place step implementation.
|
222
|
-
# @param block [Block] The implementation of the step.
|
223
|
-
define_step_method :teardown
|
224
|
-
|
225
|
-
# Define verify steps.
|
226
|
-
# @example
|
227
|
-
# verify 'home page displayed' # call shared step.
|
228
|
-
# Usually for validation shared steps inclusion
|
229
|
-
#
|
230
|
-
# @overload verify(*steps, &block)
|
231
|
-
# @param steps [Array<String, Symbol, Proc>] the steps to be runned as a part of verification.
|
232
|
-
# String - name of shared step to be called.
|
233
|
-
# Symbol - metadata key, referenced to shared step name.
|
234
|
-
# Proc - in-place step implementation.
|
235
|
-
# @param block [Block] The implementation of the step.
|
236
|
-
define_step_method :verify
|
237
|
-
|
238
|
-
# Define group of steps.
|
239
|
-
# The metadata for the group may be overriden
|
240
|
-
# @example
|
241
|
-
# context 'the task', task: :created do
|
242
|
-
# verify 'task setup'
|
243
|
-
# it 'created' do
|
244
|
-
# expect(metadata[:task]).to eq :created
|
245
|
-
# end
|
246
|
-
# end
|
247
|
-
# Teardown steps within group will be called at the end of the group, not scenario
|
248
|
-
# @overload context(title, **metadata, &block)
|
249
|
-
# @param title [String] context title
|
250
|
-
# @param metadata [Hash] the step additional metadata
|
251
|
-
# @param block [Block] The implementation of the step.
|
252
|
-
define_step_method :context
|
253
|
-
|
254
|
-
# Define single validation step.
|
255
|
-
# @example
|
256
|
-
# it 'works' do
|
257
|
-
# expect(1).to eq 1
|
258
|
-
# end
|
259
|
-
# @overload it(title, &block)
|
260
|
-
# @param title [String] validation title
|
261
|
-
# @param block [Block] The implementation of the step.
|
262
|
-
define_step_method :it
|
263
|
-
|
264
|
-
# Define runtime method for the scenario.
|
265
|
-
#
|
266
|
-
# @note
|
267
|
-
# The method to be called via #method_missing, so it wont override already defined methods.
|
268
|
-
#
|
269
|
-
# @example
|
270
|
-
# let(:square) { |num| num * num }
|
271
|
-
# it 'calculated' do
|
272
|
-
# expect(square(4)).to eq 16
|
273
|
-
# end
|
274
|
-
def let(method_name, &block)
|
275
|
-
steps << Lopata::Step.new(:let) do
|
276
|
-
execution.let(method_name, &block)
|
277
|
-
end
|
278
|
-
end
|
279
|
-
|
280
|
-
# @!endgroup
|
281
|
-
|
282
|
-
# @private
|
283
|
-
def add_step(method_name, *args, condition: nil, metadata: {}, &block)
|
284
|
-
step_class =
|
285
|
-
case method_name
|
286
|
-
when /^(setup|action|teardown|verify)/ then Lopata::ActionStep
|
287
|
-
when /^(context)/ then Lopata::GroupStep
|
288
|
-
else Lopata::Step
|
289
|
-
end
|
290
|
-
step = step_class.new(method_name, *args, condition: condition, shared_step: shared_step, &block)
|
291
|
-
step.metadata = metadata
|
292
|
-
steps << step
|
293
|
-
end
|
294
|
-
|
295
|
-
# @private
|
296
|
-
def steps
|
297
|
-
@steps ||= []
|
298
|
-
end
|
299
|
-
|
300
|
-
# @private
|
301
|
-
def steps_with_hooks
|
302
|
-
s = []
|
303
|
-
unless Lopata.configuration.before_scenario_steps.empty?
|
304
|
-
s << Lopata::ActionStep.new(:setup, *Lopata.configuration.before_scenario_steps)
|
305
|
-
end
|
306
|
-
|
307
|
-
s += steps
|
308
|
-
|
309
|
-
unless Lopata.configuration.after_scenario_steps.empty?
|
310
|
-
s << Lopata::ActionStep.new(:teardown, *Lopata.configuration.after_scenario_steps)
|
311
|
-
end
|
312
|
-
|
313
|
-
s
|
314
|
-
end
|
315
|
-
|
316
|
-
# @private
|
317
|
-
def option_combinations
|
318
|
-
combinations = combine([OptionSet.new], options + diagonals)
|
319
|
-
while !diagonals.all?(&:complete?)
|
320
|
-
combinations << OptionSet.new(*(options + diagonals).map(&:next_variant))
|
321
|
-
end
|
322
|
-
combinations.reject { |option_set| skip?(option_set) }
|
323
|
-
end
|
324
|
-
|
325
|
-
# @private
|
326
|
-
def combine(source_combinations, rest_options)
|
327
|
-
return source_combinations if rest_options.empty?
|
328
|
-
combinations = []
|
329
|
-
current_option = rest_options.shift
|
330
|
-
source_combinations.each do |source_variants|
|
331
|
-
current_option.level_variants.each do |v|
|
332
|
-
combinations << (source_variants + OptionSet.new(v))
|
333
|
-
end
|
334
|
-
end
|
335
|
-
combine(combinations, rest_options)
|
336
|
-
end
|
337
|
-
|
338
|
-
# @private
|
339
|
-
def world
|
340
|
-
Lopata.world
|
341
|
-
end
|
342
|
-
|
343
|
-
# Set of options for scenario
|
344
|
-
class OptionSet
|
345
|
-
# @private
|
346
|
-
attr_reader :variants
|
347
|
-
|
348
|
-
# @private
|
349
|
-
def initialize(*variants)
|
350
|
-
@variants = {}
|
351
|
-
variants.compact.each { |v| self << v }
|
352
|
-
end
|
353
|
-
|
354
|
-
# @private
|
355
|
-
def +(other_set)
|
356
|
-
self.class.new(*@variants.values).tap do |sum|
|
357
|
-
other_set.each { |v| sum << v }
|
358
|
-
end
|
359
|
-
end
|
360
|
-
|
361
|
-
# @private
|
362
|
-
def <<(variant)
|
363
|
-
@variants[variant.key] = variant
|
364
|
-
end
|
365
|
-
|
366
|
-
# @private
|
367
|
-
def [](key)
|
368
|
-
@variants[key]
|
369
|
-
end
|
370
|
-
|
371
|
-
# @private
|
372
|
-
def each(&block)
|
373
|
-
@variants.values.each(&block)
|
374
|
-
end
|
375
|
-
|
376
|
-
# @private
|
377
|
-
def title
|
378
|
-
@variants.values.map(&:title).compact.join(' ')
|
379
|
-
end
|
380
|
-
|
381
|
-
# @return [Hash{Symbol => Object}] metadata for this option set
|
382
|
-
def metadata
|
383
|
-
@variants.values.inject({}) do |metadata, variant|
|
384
|
-
metadata.merge(variant.metadata(self))
|
385
|
-
end
|
386
|
-
end
|
387
|
-
end
|
388
|
-
|
389
|
-
# @private
|
390
|
-
class Variant
|
391
|
-
attr_reader :key, :title, :value, :option
|
392
|
-
|
393
|
-
def initialize(option, key, title, value)
|
394
|
-
@option, @key, @title, @value = option, key, title, check_lambda_arity(value)
|
395
|
-
end
|
396
|
-
|
397
|
-
def metadata(option_set)
|
398
|
-
data = { key => value }
|
399
|
-
if value.is_a? Hash
|
400
|
-
value.each do |k, v|
|
401
|
-
sub_key = "%s_%s" % [key, k]
|
402
|
-
data[sub_key.to_sym] = v
|
403
|
-
end
|
404
|
-
end
|
405
|
-
|
406
|
-
option.available_metadata_keys.each do |key|
|
407
|
-
data[key] = nil unless data.has_key?(key)
|
408
|
-
end
|
409
|
-
|
410
|
-
data.each do |key, v|
|
411
|
-
data[key] = v.calculate(option_set) if v.is_a? CalculatedValue
|
412
|
-
end
|
413
|
-
data
|
414
|
-
end
|
415
|
-
|
416
|
-
def self.join(variants)
|
417
|
-
title, metadata = nil, {}
|
418
|
-
variants.each do |v|
|
419
|
-
title = [title, v.title].compact.join(' ')
|
420
|
-
metadata.merge!(v.metadata)
|
421
|
-
end
|
422
|
-
[title, metadata]
|
423
|
-
end
|
424
|
-
|
425
|
-
private
|
426
|
-
|
427
|
-
# Лямдда будет передаваться как блок в instance_eval, которому плохеет, если пришло что-то с нулевой
|
428
|
-
# arity. Поэтому для лямбд с нулевой arity делаем arity == 1
|
429
|
-
def check_lambda_arity(v)
|
430
|
-
if v.is_a?(Proc) && v.arity == 0
|
431
|
-
->(_) { instance_exec(&v) }
|
432
|
-
else
|
433
|
-
v
|
434
|
-
end
|
435
|
-
end
|
436
|
-
end
|
437
|
-
|
438
|
-
# @private
|
439
|
-
class CalculatedValue
|
440
|
-
def initialize(&block)
|
441
|
-
@proc = block
|
442
|
-
end
|
443
|
-
|
444
|
-
def calculate(option_set)
|
445
|
-
@proc.call(option_set)
|
446
|
-
end
|
447
|
-
end
|
448
|
-
|
449
|
-
# @private
|
450
|
-
class Option
|
451
|
-
attr_reader :variants, :key, :use_all_variants
|
452
|
-
def initialize(key, variants, use_all_variants = true)
|
453
|
-
@key = key
|
454
|
-
@variants =
|
455
|
-
if variants.is_a? Hash
|
456
|
-
variants.map { |title, value| Variant.new(self, key, title, value) }
|
457
|
-
else
|
458
|
-
# Array of arrays of two elements
|
459
|
-
variants.map { |v| Variant.new(self, key, *v) }
|
460
|
-
end
|
461
|
-
@use_all_variants = use_all_variants
|
462
|
-
end
|
463
|
-
|
464
|
-
# Variants to apply at one level
|
465
|
-
def level_variants
|
466
|
-
variants
|
467
|
-
end
|
468
|
-
|
469
|
-
def next_variant
|
470
|
-
@current ||= 0
|
471
|
-
selected_variant = variants[@current]
|
472
|
-
@current += 1
|
473
|
-
@complete = true unless use_all_variants # not need to verify all variants, just use first ones.
|
474
|
-
if @current >= variants.length
|
475
|
-
@current = 0
|
476
|
-
@complete = true # all variants have been selected
|
477
|
-
end
|
478
|
-
selected_variant
|
479
|
-
end
|
480
|
-
|
481
|
-
def available_metadata_keys
|
482
|
-
@available_metadata_keys ||= variants
|
483
|
-
.map(&:value).select { |v| v.is_a?(Hash) }.flat_map(&:keys).map { |k| "#{key}_#{k}".to_sym }.uniq
|
484
|
-
end
|
485
|
-
end
|
486
|
-
|
487
|
-
# @private
|
488
|
-
class Diagonal < Option
|
489
|
-
def level_variants
|
490
|
-
[next_variant]
|
491
|
-
end
|
492
|
-
|
493
|
-
def complete?
|
494
|
-
@complete
|
495
|
-
end
|
496
|
-
end
|
497
|
-
end
|
1
|
+
# Context for scenario creation.
|
2
|
+
class Lopata::ScenarioBuilder
|
3
|
+
# @private
|
4
|
+
attr_reader :title, :common_metadata, :options, :diagonals
|
5
|
+
# @private
|
6
|
+
attr_accessor :shared_step, :group
|
7
|
+
|
8
|
+
# Defines one or more scenarios.
|
9
|
+
#
|
10
|
+
# @example
|
11
|
+
# Lopata.define 'scenario' do
|
12
|
+
# setup 'test user'
|
13
|
+
# action 'login'
|
14
|
+
# verify 'home page displayed'
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# Given block will be calculated in context of the ScenarioBuilder
|
18
|
+
#
|
19
|
+
# @param title [String] scenario unique title
|
20
|
+
# @param metadata [Hash] metadata to be used within the scenario
|
21
|
+
# @param block [Block] the scenario definition
|
22
|
+
# @see Lopata.define
|
23
|
+
def self.define(title, metadata = {}, &block)
|
24
|
+
builder = new(title, metadata)
|
25
|
+
builder.instance_exec &block
|
26
|
+
builder.build
|
27
|
+
end
|
28
|
+
|
29
|
+
# @private
|
30
|
+
def initialize(title, metadata = {})
|
31
|
+
@title, @common_metadata = title, metadata
|
32
|
+
@diagonals = []
|
33
|
+
@options = []
|
34
|
+
end
|
35
|
+
|
36
|
+
# @private
|
37
|
+
def build
|
38
|
+
filters = Lopata.configuration.filters
|
39
|
+
option_combinations.each do |option_set|
|
40
|
+
metadata = common_metadata.merge(option_set.metadata)
|
41
|
+
scenario = Lopata::Scenario::Execution.new(title, option_set.title, metadata)
|
42
|
+
|
43
|
+
unless filters.empty?
|
44
|
+
next unless filters.all? { |f| f[scenario] }
|
45
|
+
end
|
46
|
+
|
47
|
+
steps_with_hooks.each do |step|
|
48
|
+
next if step.condition && !step.condition.match?(scenario)
|
49
|
+
step.execution_steps(scenario).each { |s| scenario.steps << s }
|
50
|
+
end
|
51
|
+
|
52
|
+
world.scenarios << scenario
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# @!group Defining variants
|
57
|
+
|
58
|
+
# Define option for the scenario.
|
59
|
+
#
|
60
|
+
# The scenario will be generated for all the options.
|
61
|
+
# If more then one option given, the scenarios for all options combinations will be generated.
|
62
|
+
#
|
63
|
+
# @param metadata_key [Symbol] the key to access option data via metadata.
|
64
|
+
# @param variants [Hash{String => Object}] variants for the option
|
65
|
+
# Keys are titles of the variant, values are metadata values.
|
66
|
+
#
|
67
|
+
# @example
|
68
|
+
# Lopata.define 'scenario' do
|
69
|
+
# option :one, 'one' => 1, 'two' => 2
|
70
|
+
# option :two, 'two' => 2, 'three' => 3
|
71
|
+
# # will generate 4 scenarios:
|
72
|
+
# # - 'scenario one two'
|
73
|
+
# # - 'scenario one three'
|
74
|
+
# # - 'scenario two two'
|
75
|
+
# # - 'scenario two three'
|
76
|
+
# end
|
77
|
+
#
|
78
|
+
# @see #diagonal
|
79
|
+
def option(metadata_key, variants)
|
80
|
+
@options << Option.new(metadata_key, variants)
|
81
|
+
end
|
82
|
+
|
83
|
+
# Define diagonal for the scenario.
|
84
|
+
#
|
85
|
+
# The scenario will be generated for all the variants of the diagonal.
|
86
|
+
# Each variant of diagonal will be selected for at least one scenario.
|
87
|
+
# It may be included in more then one scenario when other diagonal or option has more variants.
|
88
|
+
#
|
89
|
+
# @param metadata_key [Symbol] the key to access diagonal data via metadata.
|
90
|
+
# @param variants [Hash{String => Object}] variants for the diagonal.
|
91
|
+
# Keys are titles of the variant, values are metadata values.
|
92
|
+
#
|
93
|
+
# @example
|
94
|
+
# Lopata.define 'scenario' do
|
95
|
+
# option :one, 'one' => 1, 'two' => 2
|
96
|
+
# diagonal :two, 'two' => 2, 'three' => 3
|
97
|
+
# diagonal :three, 'three' => 3, 'four' => 4, 'five' => 5
|
98
|
+
# # will generate 3 scenarios:
|
99
|
+
# # - 'scenario one two three'
|
100
|
+
# # - 'scenario two three four'
|
101
|
+
# # - 'scenario one two five'
|
102
|
+
# end
|
103
|
+
#
|
104
|
+
# @see #option
|
105
|
+
def diagonal(metadata_key, variants)
|
106
|
+
@diagonals << Diagonal.new(metadata_key, variants)
|
107
|
+
end
|
108
|
+
|
109
|
+
# Define additional metadata for the scenario
|
110
|
+
#
|
111
|
+
# @example
|
112
|
+
# Lopata.define 'scenario' do
|
113
|
+
# metadata key: 'value'
|
114
|
+
# it 'metadata available' do
|
115
|
+
# expect(metadata[:key]).to eq 'value'
|
116
|
+
# end
|
117
|
+
# end
|
118
|
+
def metadata(hash)
|
119
|
+
raise 'metadata expected to be a Hash' unless hash.is_a?(Hash)
|
120
|
+
@common_metadata ||= {}
|
121
|
+
@common_metadata.merge! hash
|
122
|
+
end
|
123
|
+
|
124
|
+
# Skip scenario for given variants combination
|
125
|
+
#
|
126
|
+
# @example
|
127
|
+
# Lopata.define 'multiple options' do
|
128
|
+
# option :one, 'one' => 1, 'two' => 2
|
129
|
+
# option :two, 'two' => 2, 'three' => 3
|
130
|
+
# skip_when { |opt| opt.metadata[:one] == opt.metadata[:two] }
|
131
|
+
# it 'not equal' do
|
132
|
+
# expect(one).to_not eq two
|
133
|
+
# end
|
134
|
+
# end
|
135
|
+
#
|
136
|
+
def skip_when(&block)
|
137
|
+
@skip_when = block
|
138
|
+
end
|
139
|
+
|
140
|
+
# @private
|
141
|
+
def skip?(option_set)
|
142
|
+
@skip_when && @skip_when.call(option_set)
|
143
|
+
end
|
144
|
+
|
145
|
+
# @!endgroup
|
146
|
+
|
147
|
+
# @!group Defining Steps
|
148
|
+
|
149
|
+
# @private
|
150
|
+
# @macro [attach] define_step_method
|
151
|
+
# @!scope instance
|
152
|
+
# @method $1
|
153
|
+
def self.define_step_method(name)
|
154
|
+
name_if = "%s_if" % name
|
155
|
+
name_unless = "%s_unless" % name
|
156
|
+
define_method name, ->(*args, **metadata, &block) { add_step(name, *args, metadata: metadata, &block) }
|
157
|
+
define_method name_if, ->(condition, *args, **metadata, &block) {
|
158
|
+
add_step(name, *args, metadata: metadata, condition: Lopata::Condition.new(condition), &block)
|
159
|
+
}
|
160
|
+
define_method name_unless, ->(condition, *args, **metadata, &block) {
|
161
|
+
add_step(name, *args, condition: Lopata::Condition.new(condition, positive: false), metadata: metadata, &block)
|
162
|
+
}
|
163
|
+
end
|
164
|
+
|
165
|
+
# Define setup step.
|
166
|
+
# @example
|
167
|
+
# setup do
|
168
|
+
# end
|
169
|
+
#
|
170
|
+
# # setup from named shared step
|
171
|
+
# setup 'create user'
|
172
|
+
#
|
173
|
+
# # setup with both shared step and code block
|
174
|
+
# setup 'create user' do
|
175
|
+
# @user.update(admin: true)
|
176
|
+
# end
|
177
|
+
#
|
178
|
+
# Setup step used for set test data.
|
179
|
+
# @overload setup(*steps, &block)
|
180
|
+
# @param steps [Array<String, Symbol, Proc>] the steps to be runned as a part of setup.
|
181
|
+
# String - name of shared step to be called.
|
182
|
+
# Symbol - metadata key, referenced to shared step name.
|
183
|
+
# Proc - in-place step implementation.
|
184
|
+
# @param block [Block] The implementation of the step.
|
185
|
+
define_step_method :setup
|
186
|
+
|
187
|
+
# Define action step.
|
188
|
+
# @example
|
189
|
+
# action do
|
190
|
+
# end
|
191
|
+
#
|
192
|
+
# # action from named shared step
|
193
|
+
# action 'login'
|
194
|
+
#
|
195
|
+
# # setup with both shared step and code block
|
196
|
+
# action 'login', 'go dashboard' do
|
197
|
+
# @user.update(admin: true)
|
198
|
+
# end
|
199
|
+
#
|
200
|
+
# Action step is used for emulate user or external system action
|
201
|
+
#
|
202
|
+
# @overload action(*steps, &block)
|
203
|
+
# @param steps [Array<String, Symbol, Proc>] the steps to be runned as a part of action.
|
204
|
+
# String - name of shared step to be called.
|
205
|
+
# Symbol - metadata key, referenced to shared step name.
|
206
|
+
# Proc - in-place step implementation.
|
207
|
+
# @param block [Block] The implementation of the step.
|
208
|
+
define_step_method :action
|
209
|
+
|
210
|
+
# Define teardown step.
|
211
|
+
# @example
|
212
|
+
# setup { @user = User.create! }
|
213
|
+
# teardown { @user.destroy }
|
214
|
+
# Teardown step will be called at the end of scenario running.
|
215
|
+
# But it suggested to be decared right after setup or action step which require teardown.
|
216
|
+
#
|
217
|
+
# @overload teardown(*steps, &block)
|
218
|
+
# @param steps [Array<String, Symbol, Proc>] the steps to be runned as a part of teardown.
|
219
|
+
# String - name of shared step to be called.
|
220
|
+
# Symbol - metadata key, referenced to shared step name.
|
221
|
+
# Proc - in-place step implementation.
|
222
|
+
# @param block [Block] The implementation of the step.
|
223
|
+
define_step_method :teardown
|
224
|
+
|
225
|
+
# Define verify steps.
|
226
|
+
# @example
|
227
|
+
# verify 'home page displayed' # call shared step.
|
228
|
+
# Usually for validation shared steps inclusion
|
229
|
+
#
|
230
|
+
# @overload verify(*steps, &block)
|
231
|
+
# @param steps [Array<String, Symbol, Proc>] the steps to be runned as a part of verification.
|
232
|
+
# String - name of shared step to be called.
|
233
|
+
# Symbol - metadata key, referenced to shared step name.
|
234
|
+
# Proc - in-place step implementation.
|
235
|
+
# @param block [Block] The implementation of the step.
|
236
|
+
define_step_method :verify
|
237
|
+
|
238
|
+
# Define group of steps.
|
239
|
+
# The metadata for the group may be overriden
|
240
|
+
# @example
|
241
|
+
# context 'the task', task: :created do
|
242
|
+
# verify 'task setup'
|
243
|
+
# it 'created' do
|
244
|
+
# expect(metadata[:task]).to eq :created
|
245
|
+
# end
|
246
|
+
# end
|
247
|
+
# Teardown steps within group will be called at the end of the group, not scenario
|
248
|
+
# @overload context(title, **metadata, &block)
|
249
|
+
# @param title [String] context title
|
250
|
+
# @param metadata [Hash] the step additional metadata
|
251
|
+
# @param block [Block] The implementation of the step.
|
252
|
+
define_step_method :context
|
253
|
+
|
254
|
+
# Define single validation step.
|
255
|
+
# @example
|
256
|
+
# it 'works' do
|
257
|
+
# expect(1).to eq 1
|
258
|
+
# end
|
259
|
+
# @overload it(title, &block)
|
260
|
+
# @param title [String] validation title
|
261
|
+
# @param block [Block] The implementation of the step.
|
262
|
+
define_step_method :it
|
263
|
+
|
264
|
+
# Define runtime method for the scenario.
|
265
|
+
#
|
266
|
+
# @note
|
267
|
+
# The method to be called via #method_missing, so it wont override already defined methods.
|
268
|
+
#
|
269
|
+
# @example
|
270
|
+
# let(:square) { |num| num * num }
|
271
|
+
# it 'calculated' do
|
272
|
+
# expect(square(4)).to eq 16
|
273
|
+
# end
|
274
|
+
def let(method_name, &block)
|
275
|
+
steps << Lopata::Step.new(:let) do
|
276
|
+
execution.let(method_name, &block)
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
# @!endgroup
|
281
|
+
|
282
|
+
# @private
|
283
|
+
def add_step(method_name, *args, condition: nil, metadata: {}, &block)
|
284
|
+
step_class =
|
285
|
+
case method_name
|
286
|
+
when /^(setup|action|teardown|verify)/ then Lopata::ActionStep
|
287
|
+
when /^(context)/ then Lopata::GroupStep
|
288
|
+
else Lopata::Step
|
289
|
+
end
|
290
|
+
step = step_class.new(method_name, *args, condition: condition, shared_step: shared_step, &block)
|
291
|
+
step.metadata = metadata
|
292
|
+
steps << step
|
293
|
+
end
|
294
|
+
|
295
|
+
# @private
|
296
|
+
def steps
|
297
|
+
@steps ||= []
|
298
|
+
end
|
299
|
+
|
300
|
+
# @private
|
301
|
+
def steps_with_hooks
|
302
|
+
s = []
|
303
|
+
unless Lopata.configuration.before_scenario_steps.empty?
|
304
|
+
s << Lopata::ActionStep.new(:setup, *Lopata.configuration.before_scenario_steps)
|
305
|
+
end
|
306
|
+
|
307
|
+
s += steps
|
308
|
+
|
309
|
+
unless Lopata.configuration.after_scenario_steps.empty?
|
310
|
+
s << Lopata::ActionStep.new(:teardown, *Lopata.configuration.after_scenario_steps)
|
311
|
+
end
|
312
|
+
|
313
|
+
s
|
314
|
+
end
|
315
|
+
|
316
|
+
# @private
|
317
|
+
def option_combinations
|
318
|
+
combinations = combine([OptionSet.new], options + diagonals)
|
319
|
+
while !diagonals.all?(&:complete?)
|
320
|
+
combinations << OptionSet.new(*(options + diagonals).map(&:next_variant))
|
321
|
+
end
|
322
|
+
combinations.reject { |option_set| skip?(option_set) }
|
323
|
+
end
|
324
|
+
|
325
|
+
# @private
|
326
|
+
def combine(source_combinations, rest_options)
|
327
|
+
return source_combinations if rest_options.empty?
|
328
|
+
combinations = []
|
329
|
+
current_option = rest_options.shift
|
330
|
+
source_combinations.each do |source_variants|
|
331
|
+
current_option.level_variants.each do |v|
|
332
|
+
combinations << (source_variants + OptionSet.new(v))
|
333
|
+
end
|
334
|
+
end
|
335
|
+
combine(combinations, rest_options)
|
336
|
+
end
|
337
|
+
|
338
|
+
# @private
|
339
|
+
def world
|
340
|
+
Lopata.world
|
341
|
+
end
|
342
|
+
|
343
|
+
# Set of options for scenario
|
344
|
+
class OptionSet
|
345
|
+
# @private
|
346
|
+
attr_reader :variants
|
347
|
+
|
348
|
+
# @private
|
349
|
+
def initialize(*variants)
|
350
|
+
@variants = {}
|
351
|
+
variants.compact.each { |v| self << v }
|
352
|
+
end
|
353
|
+
|
354
|
+
# @private
|
355
|
+
def +(other_set)
|
356
|
+
self.class.new(*@variants.values).tap do |sum|
|
357
|
+
other_set.each { |v| sum << v }
|
358
|
+
end
|
359
|
+
end
|
360
|
+
|
361
|
+
# @private
|
362
|
+
def <<(variant)
|
363
|
+
@variants[variant.key] = variant
|
364
|
+
end
|
365
|
+
|
366
|
+
# @private
|
367
|
+
def [](key)
|
368
|
+
@variants[key]
|
369
|
+
end
|
370
|
+
|
371
|
+
# @private
|
372
|
+
def each(&block)
|
373
|
+
@variants.values.each(&block)
|
374
|
+
end
|
375
|
+
|
376
|
+
# @private
|
377
|
+
def title
|
378
|
+
@variants.values.map(&:title).compact.join(' ')
|
379
|
+
end
|
380
|
+
|
381
|
+
# @return [Hash{Symbol => Object}] metadata for this option set
|
382
|
+
def metadata
|
383
|
+
@variants.values.inject({}) do |metadata, variant|
|
384
|
+
metadata.merge(variant.metadata(self))
|
385
|
+
end
|
386
|
+
end
|
387
|
+
end
|
388
|
+
|
389
|
+
# @private
|
390
|
+
class Variant
|
391
|
+
attr_reader :key, :title, :value, :option
|
392
|
+
|
393
|
+
def initialize(option, key, title, value)
|
394
|
+
@option, @key, @title, @value = option, key, title, check_lambda_arity(value)
|
395
|
+
end
|
396
|
+
|
397
|
+
def metadata(option_set)
|
398
|
+
data = { key => value }
|
399
|
+
if value.is_a? Hash
|
400
|
+
value.each do |k, v|
|
401
|
+
sub_key = "%s_%s" % [key, k]
|
402
|
+
data[sub_key.to_sym] = v
|
403
|
+
end
|
404
|
+
end
|
405
|
+
|
406
|
+
option.available_metadata_keys.each do |key|
|
407
|
+
data[key] = nil unless data.has_key?(key)
|
408
|
+
end
|
409
|
+
|
410
|
+
data.each do |key, v|
|
411
|
+
data[key] = v.calculate(option_set) if v.is_a? CalculatedValue
|
412
|
+
end
|
413
|
+
data
|
414
|
+
end
|
415
|
+
|
416
|
+
def self.join(variants)
|
417
|
+
title, metadata = nil, {}
|
418
|
+
variants.each do |v|
|
419
|
+
title = [title, v.title].compact.join(' ')
|
420
|
+
metadata.merge!(v.metadata)
|
421
|
+
end
|
422
|
+
[title, metadata]
|
423
|
+
end
|
424
|
+
|
425
|
+
private
|
426
|
+
|
427
|
+
# Лямдда будет передаваться как блок в instance_eval, которому плохеет, если пришло что-то с нулевой
|
428
|
+
# arity. Поэтому для лямбд с нулевой arity делаем arity == 1
|
429
|
+
def check_lambda_arity(v)
|
430
|
+
if v.is_a?(Proc) && v.arity == 0
|
431
|
+
->(_) { instance_exec(&v) }
|
432
|
+
else
|
433
|
+
v
|
434
|
+
end
|
435
|
+
end
|
436
|
+
end
|
437
|
+
|
438
|
+
# @private
|
439
|
+
class CalculatedValue
|
440
|
+
def initialize(&block)
|
441
|
+
@proc = block
|
442
|
+
end
|
443
|
+
|
444
|
+
def calculate(option_set)
|
445
|
+
@proc.call(option_set)
|
446
|
+
end
|
447
|
+
end
|
448
|
+
|
449
|
+
# @private
|
450
|
+
class Option
|
451
|
+
attr_reader :variants, :key, :use_all_variants
|
452
|
+
def initialize(key, variants, use_all_variants = true)
|
453
|
+
@key = key
|
454
|
+
@variants =
|
455
|
+
if variants.is_a? Hash
|
456
|
+
variants.map { |title, value| Variant.new(self, key, title, value) }
|
457
|
+
else
|
458
|
+
# Array of arrays of two elements
|
459
|
+
variants.map { |v| Variant.new(self, key, *v) }
|
460
|
+
end
|
461
|
+
@use_all_variants = use_all_variants
|
462
|
+
end
|
463
|
+
|
464
|
+
# Variants to apply at one level
|
465
|
+
def level_variants
|
466
|
+
variants
|
467
|
+
end
|
468
|
+
|
469
|
+
def next_variant
|
470
|
+
@current ||= 0
|
471
|
+
selected_variant = variants[@current]
|
472
|
+
@current += 1
|
473
|
+
@complete = true unless use_all_variants # not need to verify all variants, just use first ones.
|
474
|
+
if @current >= variants.length
|
475
|
+
@current = 0
|
476
|
+
@complete = true # all variants have been selected
|
477
|
+
end
|
478
|
+
selected_variant
|
479
|
+
end
|
480
|
+
|
481
|
+
def available_metadata_keys
|
482
|
+
@available_metadata_keys ||= variants
|
483
|
+
.map(&:value).select { |v| v.is_a?(Hash) }.flat_map(&:keys).map { |k| "#{key}_#{k}".to_sym }.uniq
|
484
|
+
end
|
485
|
+
end
|
486
|
+
|
487
|
+
# @private
|
488
|
+
class Diagonal < Option
|
489
|
+
def level_variants
|
490
|
+
[next_variant]
|
491
|
+
end
|
492
|
+
|
493
|
+
def complete?
|
494
|
+
@complete
|
495
|
+
end
|
496
|
+
end
|
497
|
+
end
|