lopata 0.1.4 → 0.1.9

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,8 +67,9 @@ 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)
52
75
  cleanup
@@ -57,18 +80,19 @@ class Lopata::Scenario
57
80
  @current_step = step
58
81
  step.run(scenario)
59
82
  skip_rest if step.failed? && step.skip_rest_on_failure?
83
+ @current_step = nil
60
84
  end
61
85
 
62
86
  def world
63
- @world ||= Lopata::Config.world
87
+ Lopata.world
64
88
  end
65
89
 
66
90
  def failed?
67
91
  status == :failed
68
92
  end
69
93
 
70
- def steps_in_running_order
71
- steps.reject(&:teardown_group?) + steps.select(&:teardown_group?)
94
+ def sort_steps
95
+ @steps = steps.reject(&:teardown_group?) + steps.select(&:teardown_group?)
72
96
  end
73
97
 
74
98
  def skip_rest
@@ -83,6 +107,25 @@ class Lopata::Scenario
83
107
  end
84
108
  end
85
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
+
86
129
  def cleanup
87
130
  @title = nil
88
131
  @metadata = nil
@@ -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]