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.
@@ -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