lopata 0.1.2 → 0.1.7

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,43 +1,65 @@
1
1
  require 'rspec/expectations'
2
2
 
3
+ # Scenario runtime class.
4
+ #
5
+ # All the scenarios are running in context of separate Lopata::Scenario object.
6
+ #
3
7
  class Lopata::Scenario
4
8
  include RSpec::Matchers
5
9
 
10
+ # @private
6
11
  attr_reader :execution
7
12
 
13
+ # @private
8
14
  def initialize(execution)
9
15
  @execution = execution
10
16
  end
11
17
 
12
18
  # Marks current step as pending
19
+ # @example
20
+ # it 'pending step' do
21
+ # pending
22
+ # expect(1).to eq 2
23
+ # end
24
+ #
25
+ # Pending steps wont be failed
13
26
  def pending(message = nil)
14
27
  execution.current_step.pending!(message)
15
28
  end
16
29
 
30
+ # @return [Hash] metadata available for current step
31
+ # @note The metadata keys also availalbe as methods (via method_missing)
17
32
  def metadata
18
33
  execution.metadata
19
34
  end
20
35
 
21
36
  private
22
37
 
38
+ # @private
23
39
  def method_missing(method, *args, &block)
24
- if metadata.keys.include?(method)
40
+ if execution.let_methods.include?(method)
41
+ instance_exec(*args, &execution.let_methods[method])
42
+ elsif metadata.keys.include?(method)
25
43
  metadata[method]
26
44
  else
27
45
  super
28
46
  end
29
47
  end
30
48
 
49
+ # @private
31
50
  def respond_to_missing?(method, *)
32
- metadata.keys.include?(method) or super
51
+ execution.let_methods.include?(method) or metadata.keys.include?(method) or super
33
52
  end
34
53
 
54
+ # @private
55
+ # Scenario execution and live-cycle information
35
56
  class Execution
36
57
  attr_reader :scenario, :status, :steps, :title, :current_step
37
58
 
38
59
  def initialize(title, options_title, metadata = {})
39
60
  @title = [title, options_title].compact.reject(&:empty?).join(' ')
40
61
  @metadata = metadata
62
+ @let_methods = {}
41
63
  @status = :not_runned
42
64
  @steps = []
43
65
  @scenario = Lopata::Scenario.new(self)
@@ -45,10 +67,12 @@ class Lopata::Scenario
45
67
 
46
68
  def run
47
69
  @status = :running
70
+ sort_steps
48
71
  world.notify_observers(:scenario_started, self)
49
- steps_in_running_order.each(&method(:run_step))
72
+ steps.each(&method(:run_step))
50
73
  @status = steps.any?(&:failed?) ? :failed : :passed
51
74
  world.notify_observers(:scenario_finished, self)
75
+ cleanup
52
76
  end
53
77
 
54
78
  def run_step(step)
@@ -56,18 +80,19 @@ class Lopata::Scenario
56
80
  @current_step = step
57
81
  step.run(scenario)
58
82
  skip_rest if step.failed? && step.skip_rest_on_failure?
83
+ @current_step = nil
59
84
  end
60
85
 
61
86
  def world
62
- @world ||= Lopata::Config.world
87
+ Lopata.world
63
88
  end
64
89
 
65
90
  def failed?
66
91
  status == :failed
67
92
  end
68
93
 
69
- def steps_in_running_order
70
- steps.reject(&:teardown_group?) + steps.select(&:teardown_group?)
94
+ def sort_steps
95
+ @steps = steps.reject(&:teardown_group?) + steps.select(&:teardown_group?)
71
96
  end
72
97
 
73
98
  def skip_rest
@@ -81,5 +106,31 @@ class Lopata::Scenario
81
106
  @metadata
82
107
  end
83
108
  end
109
+
110
+ def let_methods
111
+ if current_step
112
+ @let_methods.merge(current_step.let_methods)
113
+ else
114
+ @let_methods
115
+ end
116
+ end
117
+
118
+ def let(method_name, &block)
119
+ # define_singleton_method method_name, &block
120
+ base =
121
+ if current_step && !current_step.groups.empty?
122
+ current_step.groups.last.let_methods
123
+ else
124
+ @let_methods
125
+ end
126
+ base[method_name] = block
127
+ end
128
+
129
+ def cleanup
130
+ @title = nil
131
+ @metadata = nil
132
+ @steps = nil
133
+ @scenario = nil
134
+ end
84
135
  end
85
136
  end
@@ -1,19 +1,41 @@
1
+ # Context for scenario creation.
1
2
  class Lopata::ScenarioBuilder
2
- attr_reader :title, :common_metadata
3
+ # @private
4
+ attr_reader :title, :common_metadata, :options, :diagonals
5
+ # @private
3
6
  attr_accessor :shared_step, :group
4
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
5
23
  def self.define(title, metadata = {}, &block)
6
24
  builder = new(title, metadata)
7
25
  builder.instance_exec &block
8
26
  builder.build
9
27
  end
10
28
 
29
+ # @private
11
30
  def initialize(title, metadata = {})
12
31
  @title, @common_metadata = title, metadata
32
+ @diagonals = []
33
+ @options = []
13
34
  end
14
35
 
36
+ # @private
15
37
  def build
16
- filters = Lopata::Config.filters
38
+ filters = Lopata.configuration.filters
17
39
  option_combinations.each do |option_set|
18
40
  metadata = common_metadata.merge(option_set.metadata)
19
41
  scenario = Lopata::Scenario::Execution.new(title, option_set.title, metadata)
@@ -31,35 +53,104 @@ class Lopata::ScenarioBuilder
31
53
  end
32
54
  end
33
55
 
34
- def as(*args, &block)
35
- @roles = args.flatten
36
- @roles << CalculatedValue.new(&block) if block_given?
37
- @role_options = nil
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)
38
81
  end
39
82
 
40
- def role_options
41
- @role_options ||= build_role_options
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)
42
107
  end
43
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
44
118
  def metadata(hash)
45
119
  raise 'metadata expected to be a Hash' unless hash.is_a?(Hash)
46
120
  @common_metadata ||= {}
47
121
  @common_metadata.merge! hash
48
122
  end
49
123
 
50
- def without_user
51
- @without_user = true
52
- end
53
-
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
+ #
54
136
  def skip_when(&block)
55
137
  @skip_when = block
56
138
  end
57
139
 
140
+ # @private
58
141
  def skip?(option_set)
59
142
  @skip_when && @skip_when.call(option_set)
60
143
  end
61
144
 
62
- %i{ setup action it teardown verify context }.each do |name|
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)
63
154
  name_if = "%s_if" % name
64
155
  name_unless = "%s_unless" % name
65
156
  define_method name, ->(*args, **metadata, &block) { add_step(name, *args, metadata: metadata, &block) }
@@ -71,6 +162,124 @@ class Lopata::ScenarioBuilder
71
162
  }
72
163
  end
73
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
74
283
  def add_step(method_name, *args, condition: nil, metadata: {}, &block)
75
284
  step_class =
76
285
  case method_name
@@ -78,84 +287,43 @@ class Lopata::ScenarioBuilder
78
287
  when /^(context)/ then Lopata::GroupStep
79
288
  else Lopata::Step
80
289
  end
81
- step = step_class.new(method_name, *args, condition: condition, shared_step: shared_step, group: group, &block)
290
+ step = step_class.new(method_name, *args, condition: condition, shared_step: shared_step, &block)
82
291
  step.metadata = metadata
83
292
  steps << step
84
293
  end
85
294
 
295
+ # @private
86
296
  def steps
87
297
  @steps ||= []
88
298
  end
89
299
 
300
+ # @private
90
301
  def steps_with_hooks
91
302
  s = []
92
- unless Lopata::Config.before_scenario_steps.empty?
93
- s << Lopata::ActionStep.new(:setup, *Lopata::Config.before_scenario_steps)
303
+ unless Lopata.configuration.before_scenario_steps.empty?
304
+ s << Lopata::ActionStep.new(:setup, *Lopata.configuration.before_scenario_steps)
94
305
  end
95
306
 
96
307
  s += steps
97
308
 
98
- unless Lopata::Config.after_scenario_steps.empty?
99
- s << Lopata::ActionStep.new(:teardown, *Lopata::Config.after_scenario_steps)
309
+ unless Lopata.configuration.after_scenario_steps.empty?
310
+ s << Lopata::ActionStep.new(:teardown, *Lopata.configuration.after_scenario_steps)
100
311
  end
101
312
 
102
313
  s
103
314
  end
104
315
 
105
- def cleanup(*args, &block)
106
- add_step_as_is(:cleanup, *args, &block)
107
- end
108
-
109
- def add_step_as_is(method_name, *args, &block)
110
- steps << Lopata::Step.new(method_name, *args) do
111
- # do not convert args - symbols mean name of instance variable
112
- # run_step method_name, *args, &block
113
- instance_exec(&block) if block
114
- end
115
- end
116
-
117
- def let(method_name, &block)
118
- steps << Lopata::Step.new(nil) do
119
- define_singleton_method method_name, &block
120
- end
121
- end
122
-
123
- def build_role_options
124
- return [] unless roles
125
- [Diagonal.new(:as, roles.map { |r| [nil, r] })]
126
- end
127
-
128
- def roles
129
- return false if @without_user
130
- @roles ||= [Lopata::Config.default_role].compact
131
- end
132
-
133
- def option(metadata_key, variants)
134
- options << Option.new(metadata_key, variants)
135
- end
136
-
137
- def diagonal(metadata_key, variants)
138
- diagonals << Diagonal.new(metadata_key, variants)
139
- end
140
-
141
- def options
142
- @options ||= []
143
- end
144
-
145
- def diagonals
146
- @diagonals ||= []
147
- end
148
-
316
+ # @private
149
317
  def option_combinations
150
- combinations = combine([OptionSet.new], options + diagonals + role_options)
151
- while !(diagonals + role_options).all?(&:complete?)
152
- combinations << OptionSet.new(*(options + diagonals + role_options).map(&:next_variant))
318
+ combinations = combine([OptionSet.new], options + diagonals)
319
+ while !diagonals.all?(&:complete?)
320
+ combinations << OptionSet.new(*(options + diagonals).map(&:next_variant))
153
321
  end
154
322
  combinations.reject { |option_set| skip?(option_set) }
155
323
  end
156
324
 
325
+ # @private
157
326
  def combine(source_combinations, rest_options)
158
- # raise 'source_combinations cannot be empty' if source_combinations.blank?
159
327
  return source_combinations if rest_options.empty?
160
328
  combinations = []
161
329
  current_option = rest_options.shift
@@ -167,40 +335,50 @@ class Lopata::ScenarioBuilder
167
335
  combine(combinations, rest_options)
168
336
  end
169
337
 
338
+ # @private
170
339
  def world
171
- @world ||= Lopata::Config.world
340
+ Lopata.world
172
341
  end
173
342
 
174
- # Набор вариантов, собранный для одного теста
343
+ # Set of options for scenario
175
344
  class OptionSet
345
+ # @private
176
346
  attr_reader :variants
347
+
348
+ # @private
177
349
  def initialize(*variants)
178
350
  @variants = {}
179
351
  variants.compact.each { |v| self << v }
180
352
  end
181
353
 
354
+ # @private
182
355
  def +(other_set)
183
356
  self.class.new(*@variants.values).tap do |sum|
184
357
  other_set.each { |v| sum << v }
185
358
  end
186
359
  end
187
360
 
361
+ # @private
188
362
  def <<(variant)
189
363
  @variants[variant.key] = variant
190
364
  end
191
365
 
366
+ # @private
192
367
  def [](key)
193
368
  @variants[key]
194
369
  end
195
370
 
371
+ # @private
196
372
  def each(&block)
197
373
  @variants.values.each(&block)
198
374
  end
199
375
 
376
+ # @private
200
377
  def title
201
378
  @variants.values.map(&:title).compact.join(' ')
202
379
  end
203
380
 
381
+ # @return [Hash{Symbol => Object}] metadata for this option set
204
382
  def metadata
205
383
  @variants.values.inject({}) do |metadata, variant|
206
384
  metadata.merge(variant.metadata(self))
@@ -208,6 +386,7 @@ class Lopata::ScenarioBuilder
208
386
  end
209
387
  end
210
388
 
389
+ # @private
211
390
  class Variant
212
391
  attr_reader :key, :title, :value, :option
213
392
 
@@ -256,6 +435,7 @@ class Lopata::ScenarioBuilder
256
435
  end
257
436
  end
258
437
 
438
+ # @private
259
439
  class CalculatedValue
260
440
  def initialize(&block)
261
441
  @proc = block
@@ -266,6 +446,7 @@ class Lopata::ScenarioBuilder
266
446
  end
267
447
  end
268
448
 
449
+ # @private
269
450
  class Option
270
451
  attr_reader :variants, :key
271
452
  def initialize(key, variants)
@@ -301,6 +482,7 @@ class Lopata::ScenarioBuilder
301
482
  end
302
483
  end
303
484
 
485
+ # @private
304
486
  class Diagonal < Option
305
487
  def level_variants
306
488
  [next_variant]