lopata 0.1.3 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.yardopts +1 -0
- data/README.md +6 -2
- data/exe/lopata +1 -1
- data/lib/lopata.rb +52 -2
- data/lib/lopata/active_record.rb +105 -5
- data/lib/lopata/condition.rb +2 -1
- data/lib/lopata/configuration.rb +126 -0
- data/lib/lopata/environment.rb +36 -0
- data/lib/lopata/factory_bot.rb +52 -16
- data/lib/lopata/generators/app.rb +2 -2
- data/lib/lopata/generators/templates/Gemfile +0 -2
- data/lib/lopata/generators/templates/config/environments/qa.yml +0 -1
- data/lib/lopata/id.rb +1 -0
- data/lib/lopata/loader.rb +2 -0
- data/lib/lopata/observers.rb +1 -0
- data/lib/lopata/observers/backtrace_formatter.rb +16 -2
- data/lib/lopata/observers/base_observer.rb +17 -6
- data/lib/lopata/observers/console_output_observer.rb +31 -8
- data/lib/lopata/observers/web_logger.rb +30 -33
- data/lib/lopata/role.rb +91 -0
- data/lib/lopata/runner.rb +18 -21
- data/lib/lopata/scenario.rb +57 -7
- data/lib/lopata/scenario_builder.rb +250 -68
- data/lib/lopata/shared_step.rb +2 -0
- data/lib/lopata/step.rb +27 -23
- data/lib/lopata/version.rb +2 -1
- data/lib/lopata/world.rb +8 -12
- metadata +7 -8
- data/lib/lopata/config.rb +0 -97
- data/lib/lopata/generators/templates/.rspec +0 -3
- data/lib/lopata/generators/templates/spec/spec_helper.rb +0 -2
- data/lib/lopata/rspec/dsl.rb +0 -39
- data/lib/lopata/rspec/role.rb +0 -74
data/lib/lopata/scenario.rb
CHANGED
@@ -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
|
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,11 +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
|
-
|
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
|
53
76
|
end
|
54
77
|
|
55
78
|
def run_step(step)
|
@@ -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
|
-
|
87
|
+
Lopata.world
|
64
88
|
end
|
65
89
|
|
66
90
|
def failed?
|
67
91
|
status == :failed
|
68
92
|
end
|
69
93
|
|
70
|
-
def
|
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
|
@@ -82,5 +106,31 @@ class Lopata::Scenario
|
|
82
106
|
@metadata
|
83
107
|
end
|
84
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
|
85
135
|
end
|
86
136
|
end
|
@@ -1,19 +1,41 @@
|
|
1
|
+
# Context for scenario creation.
|
1
2
|
class Lopata::ScenarioBuilder
|
2
|
-
|
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
|
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
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
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
|
-
|
41
|
-
|
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
|
-
|
51
|
-
|
52
|
-
|
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
|
-
|
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,
|
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
|
93
|
-
s << Lopata::ActionStep.new(:setup, *Lopata
|
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
|
99
|
-
s << Lopata::ActionStep.new(:teardown, *Lopata
|
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
|
-
|
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
|
151
|
-
while !
|
152
|
-
combinations << OptionSet.new(*(options + diagonals
|
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
|
-
|
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]
|