lopata 0.1.13 → 0.1.14

Sign up to get free protection for your applications and to get access to all the features.
@@ -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