lopata 0.0.16 → 0.1.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6cd685207d7486c0cda7d76e5bbb0276b5af0e0ff73446e4d6866b49e5a8dbc5
4
- data.tar.gz: 54350543eddacf904531c311dd301858ae4cb42830edef735daed97417956b42
3
+ metadata.gz: 4ad9bc72f5e0429f18b12bc247e3fbf4346278b5c6b487a02c665e3c020bd5f7
4
+ data.tar.gz: bde38469c56e1ef03b1480fa09d9d879867660983fcd9abe807ee8be553aaaed
5
5
  SHA512:
6
- metadata.gz: 34ac0e832ac235f88371a44773a9f2df2f87b8099d9857300761dcf4ba6fc81da2386988684253d80e2d4d423457cd6b60e59cf152895b44468276ae1c667284
7
- data.tar.gz: e287063637735bf244f351396457f07857426fc568d269e6be6fd1de07f94df18c5bd29cca3aa53c8ffd1c81007f9c0b64c5cd4d96616d441f8f143611e300b7
6
+ metadata.gz: ba6f140682c330ca68eb2a8d3f27aed552bd376c1a1c773ebe1914b4738da39adfe500a4254e06c2ea3865bd5fedc7246e5301eca6e130683bc4119e7db30071
7
+ data.tar.gz: 0d6b7e1f4ae029539a1c65fd0e9ca4546610bbe6e8da8b37250ec71a106cc3cfe0445b4a6b299de9cbd5a2b1a5e4949b7bad6f178af32e702252755553b69474
data/exe/lopata CHANGED
@@ -1,4 +1,11 @@
1
1
  #!/usr/bin/env ruby
2
2
  require 'bundler/setup'
3
3
  require_relative '../lib/lopata/runner'
4
- Lopata::Runner.start ARGV
4
+
5
+ # use default command with arguments if given command is unknown.
6
+ argv = ARGV.dup
7
+ unless Lopata::Runner.all_commands.keys.include? argv
8
+ argv.unshift 'test'
9
+ end
10
+
11
+ Lopata::Runner.start argv
data/lib/lopata.rb CHANGED
@@ -1,13 +1,20 @@
1
1
  require 'lopata/id'
2
2
  require 'lopata/config'
3
+ require 'lopata/scenario_builder'
3
4
  require 'lopata/scenario'
5
+ require 'lopata/step'
6
+ require 'lopata/shared_step'
4
7
 
5
8
  module Lopata
6
9
  def self.define(*args, &block)
7
- Lopata::Scenario.define(*args, &block)
10
+ Lopata::ScenarioBuilder.define(*args, &block)
8
11
  end
9
12
 
10
13
  def self.xdefine(*args, &block)
11
- Lopata::Scenario.xdefine(*args, &block)
14
+ Lopata::ScenarioBuilder.xdefine(*args, &block)
15
+ end
16
+
17
+ def self.shared_step(name, &block)
18
+ Lopata::SharedStep.register(name, &block)
12
19
  end
13
20
  end
data/lib/lopata/config.rb CHANGED
@@ -7,7 +7,9 @@ module Lopata
7
7
 
8
8
  def init(env)
9
9
  require 'yaml'
10
- @config = YAML::load(File.open("./config/environments/#{env}.yml")) || {}
10
+ @config = {}
11
+ config_filename = "./config/environments/#{env}.yml"
12
+ @config = YAML::load(File.open(config_filename)) if File.exists?(config_filename)
11
13
  init_db
12
14
  @role_descriptions ||= {}
13
15
  # init_includes
@@ -89,5 +91,9 @@ module Lopata
89
91
  def initialize_test
90
92
  @before_start.call if @before_start
91
93
  end
94
+
95
+ def world
96
+ @world ||= Lopata::World.new
97
+ end
92
98
  end
93
99
  end
@@ -14,6 +14,7 @@ module Lopata
14
14
  template '.rspec', "#{name}/.rspec"
15
15
  template 'config/environments/qa.yml', "#{name}/config/environments/qa.yml"
16
16
  template 'config/initializers/capybara.rb', "#{name}/config/initializers/capybara.rb"
17
+ template 'spec/spec_helper.rb', "#{name}/spec/spec_helper.rb"
17
18
  end
18
19
 
19
20
  def init_dirs
@@ -21,7 +22,7 @@ module Lopata
21
22
  empty_directory "#{name}/app/#{dir}"
22
23
  end
23
24
 
24
- %w{spec config/initializers}.each do |dir|
25
+ %w{scenarios shared_steps config/initializers}.each do |dir|
25
26
  empty_directory "#{name}/#{dir}"
26
27
  end
27
28
  end
@@ -1,2 +1,3 @@
1
1
  --color
2
2
  --format documentation
3
+ --require spec_helper
@@ -2,8 +2,8 @@
2
2
  gem 'selenium-webdriver'
3
3
  gem 'rspec', :require => 'spec'
4
4
  gem 'rake'
5
- gem 'activerecord', '~> 4.2.0'
6
- gem 'capybara', '~> 2.2.1' # fix capybara version to avoid capybara-angular deprication varnings.
5
+ gem 'activerecord', '~> 6.0.2'
6
+ gem 'capybara', '~> 3.11.1'
7
7
  gem 'thor'
8
- <% require "lopata/rspec/version" %>
9
- gem 'lopata', '<%= Lopata::RSpec::Version::STRING %>'
8
+ <% require "lopata/version" %>
9
+ gem 'lopata', '<%= Lopata::Version::STRING %>'
@@ -6,6 +6,7 @@ require 'rubygems'
6
6
  require 'bundler/setup'
7
7
  require 'active_support/all'
8
8
  require 'capybara'
9
+ require 'capybara/dsl'
9
10
  require 'selenium/webdriver'
10
11
  require 'fileutils'
11
12
  require 'active_support'
@@ -0,0 +1,2 @@
1
+ Lopata::Config.role_descriptions = { user: 'User' }
2
+ Lopata::Config.default_role = :user
@@ -0,0 +1,4 @@
1
+ module Lopata::Observers
2
+ autoload :BaseObserver, 'lopata/observers/base_observer.rb'
3
+ autoload :ConsoleOutputObserver, 'lopata/observers/console_output_observer.rb'
4
+ end
@@ -0,0 +1,23 @@
1
+ module Lopata
2
+ module Observers
3
+ class BaseObserver
4
+ def started(world)
5
+ end
6
+
7
+ def finished(world)
8
+ end
9
+
10
+ def scenario_started(scenario)
11
+ end
12
+
13
+ def scenario_finished(scenario)
14
+ end
15
+
16
+ def step_started(step)
17
+ end
18
+
19
+ def step_finished(step)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,62 @@
1
+ module Lopata
2
+ module Observers
3
+ class ConsoleOutputObserver < BaseObserver
4
+ def finished(world)
5
+ total = world.scenarios.length
6
+ statuses = world.scenarios.map(&:status)
7
+ counts = statuses.uniq.map do |status|
8
+ colored("%d %s", status) % [statuses.count { |s| s == status }, status]
9
+ end
10
+ details = counts.empty? ? "" : "(%s)" % counts.join(', ')
11
+ puts "#{total} scenario%s %s" % [total == 1 ? '' : 's', details]
12
+ end
13
+
14
+ def step_finished(step)
15
+ @failed_steps << step if step.failed?
16
+ end
17
+
18
+ def scenario_started(scenario)
19
+ @failed_steps = []
20
+ end
21
+
22
+ def scenario_finished(scenario)
23
+ message = "#{scenario.title} #{bold(scenario.status.to_s.upcase)}"
24
+ puts colored(message, scenario.status)
25
+
26
+ @failed_steps.each do |step|
27
+ if step.exception
28
+ puts step.exception.message
29
+ puts step.exception.backtrace.join("\n")
30
+ puts
31
+ end
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ def colored(text, status)
38
+ case status
39
+ when :failed then red(text)
40
+ when :passed then green(text)
41
+ else text
42
+ end
43
+ end
44
+
45
+ def red(text)
46
+ wrap(text, 31)
47
+ end
48
+
49
+ def green(text)
50
+ wrap(text, 32)
51
+ end
52
+
53
+ def bold(text)
54
+ wrap(text, 1)
55
+ end
56
+
57
+ def wrap(text, code)
58
+ "\e[#{code}m#{text}\e[0m"
59
+ end
60
+ end
61
+ end
62
+ end
data/lib/lopata/runner.rb CHANGED
@@ -1,6 +1,9 @@
1
1
  require 'thor'
2
2
  require_relative 'generators/app'
3
3
  require_relative 'config'
4
+ require_relative 'world'
5
+ require_relative '../lopata'
6
+ require_relative 'observers'
4
7
 
5
8
  module Lopata
6
9
  class Runner < Thor
@@ -13,35 +16,47 @@ module Lopata
13
16
  option :build, aliases: 'b'
14
17
  option :keep, type: :boolean, aliases: 'k'
15
18
  option :text, aliases: 't'
16
- def test
17
- require 'rspec'
18
-
19
- Dir["./spec/support/**/*.rb"].sort.each { |f| require f}
20
- ENV['HOME'] = File.absolute_path('.') # disable warning on rspec loading on windows
21
- Lopata::Config.ops = {
22
- focus: options[:focus],
23
- rerun: options[:rerun],
24
- users: options[:users],
25
- build: options[:build],
26
- env: options[:env],
27
- keep: options[:keep],
28
- text: options[:text]
29
- }
30
- Lopata::Config.init(options[:env])
31
- Lopata::Config.initialize_test
32
- Lopata::Config.init_rspec
33
-
34
- ::RSpec::Core::Runner.run ['spec']
19
+ def test(*args)
20
+ configure_from_options
21
+
22
+ # Dir["./spec/support/**/*.rb"].sort.each { |f| require f}
23
+ world = Lopata::Config.world
24
+ world.setup_observers
25
+ world.load_shared_steps
26
+ world.load_scenarios(*args)
27
+ world.scenarios.each { |s| s.run }
28
+ world.finish
35
29
  end
36
30
 
37
31
  default_task :test
38
32
 
39
33
  register Generators::App, :new, 'lopata new project-name', 'Init new lopata projects'
34
+
35
+ def self.exit_on_failure?
36
+ true
37
+ end
38
+
39
+ no_commands do
40
+ def configure_from_options
41
+ Lopata::Config.ops = {
42
+ focus: options[:focus],
43
+ rerun: options[:rerun],
44
+ users: options[:users],
45
+ build: options[:build],
46
+ env: options[:env],
47
+ keep: options[:keep],
48
+ text: options[:text]
49
+ }
50
+ Lopata::Config.init(options[:env])
51
+ Lopata::Config.initialize_test
52
+ # ENV['HOME'] = File.absolute_path('.') # disable warning on rspec loading on windows
53
+ # Lopata::Config.init_rspec
54
+ end
55
+ end
40
56
  end
41
57
  end
42
58
 
43
59
  unless ARGV.first == 'new'
44
- raise 'No Lopatafile found in running dir' unless File.exists?('./Lopatafile')
45
- eval File.binread('./Lopatafile')
60
+ eval File.binread('./Lopatafile') if File.exists?('./Lopatafile')
46
61
  end
47
62
 
@@ -1,169 +1,47 @@
1
- class Lopata::Scenario
2
- def self.define(title = nil, metadata = nil, &block)
3
- scenario = new
4
- scenario.title(title) if title
5
- scenario.metadata(metadata) if metadata
6
- scenario.instance_exec &block
7
- scenario.build_rspec
8
- end
9
-
10
- # Do noting. Exclude defined scenario from suite.
11
- def self.xdefine(*attrs)
12
- end
13
-
14
- def build_rspec
15
- option_combinations.each do |option_set|
16
- args = prepare_args(option_set)
17
- raise "scenario required a name in first argument" unless args.first.is_a? String
18
- steps = @steps
19
- spec = RSpec.describe(*args)
20
- spec.send :extend, RSpecInjections
21
- spec.nested_with_as(*args) do
22
- steps.each do |block|
23
- instance_exec &block
24
- end
25
- if Lopata::Config.after_scenario
26
- instance_exec &Lopata::Config.after_scenario
27
- end
28
- end
29
- end
30
- end
31
-
32
- def as(*args, &block)
33
- @roles = args.flatten
34
- @roles << CalculatedValue.new(&block) if block_given?
35
- @role_options = nil
36
- end
37
-
38
- def role_options
39
- @role_options ||= build_role_options
40
- end
41
-
42
- def metadata(hash)
43
- raise 'metadata expected to be a Hash' unless hash.is_a?(Hash)
44
- @common_metadata ||= {}
45
- @common_metadata.merge! hash
46
- end
47
-
48
- def without_user
49
- @without_user = true
50
- end
1
+ require 'rspec/expectations'
51
2
 
52
- def skip_when(&block)
53
- @skip_when = block
54
- end
55
-
56
- def skip?(option_set)
57
- @skip_when && @skip_when.call(option_set)
58
- end
3
+ class Lopata::Scenario
4
+ include RSpec::Matchers
59
5
 
60
- %i{setup action it context teardown include_context include_examples}.each do |name|
61
- name_if = "%s_if" % name
62
- name_unless = "%s_unless" % name
63
- define_method name, ->(*args, &block) { add_step(name, *args, &block) }
64
- define_method name_if, ->(condition, *args, &block) { add_if_step(name, condition, *args, &block) }
65
- define_method name_unless, ->(condition, *args, &block) { add_unless_step(name, condition, *args, &block) }
66
- end
6
+ attr_reader :title, :metadata, :steps, :status
67
7
 
68
- def cleanup(*args, &block)
69
- add_step_as_is(:cleanup, *args, &block)
8
+ def initialize(*args)
9
+ @title = args.first
10
+ @metadata = args.last.is_a?(Hash) ? args.last : {}
11
+ @steps = []
12
+ @status = :not_runned
70
13
  end
71
14
 
72
- def add_step(method_name, *args, &block)
73
- @steps ||= []
74
- @steps << Proc.new do
75
- # will be called in context of rspec group
76
- flat_args = args.flatten
77
- flat_args = Lopata::Scenario.separate_args(flat_args) if method_name =~ /^(setup|action)/
78
- converted_args = Lopata::Scenario.convert_args(metadata, *flat_args)
79
- send method_name, *converted_args, &block
80
- end
15
+ def run
16
+ @status = :running
17
+ world.notify_observers(:scenario_started, self)
18
+ teardown_steps = []
19
+ @steps.reject(&:teardown?).each { |step| step.run(self) }
20
+ @steps.select(&:teardown?).each { |step| step.run(self) }
21
+ @status = @steps.all?(&:passed?) ? :passed : :failed
22
+ world.notify_observers(:scenario_finished, self)
81
23
  end
82
24
 
83
- def add_if_step(method_name, condition, *args, &block)
84
- @steps ||= []
85
- @steps << Proc.new do
86
- # will be called in context of rspec group
87
- if match_metadata?(condition)
88
- flat_args = args.flatten
89
- flat_args = Lopata::Scenario.separate_args(flat_args) if method_name =~ /^(setup|action)/
90
- converted_args = Lopata::Scenario.convert_args(metadata, *flat_args)
91
- send method_name, *converted_args, &block
92
- end
93
- end
94
- end
95
-
96
- def add_unless_step(method_name, condition, *args, &block)
97
- @steps ||= []
98
- @steps << Proc.new do
99
- # will be called in context of rspec group
100
- unless match_metadata?(condition)
101
- flat_args = args.flatten
102
- flat_args = Lopata::Scenario.separate_args(flat_args) if method_name =~ /^(setup|action)/
103
- converted_args = Lopata::Scenario.convert_args(metadata, *flat_args)
104
- send method_name, *converted_args, &block
105
- end
106
- end
107
- end
108
-
109
- def add_step_as_is(method_name, *args, &block)
110
- @steps ||= []
111
- @steps << Proc.new do
112
- # do not convert args - symbols mean name of instance variable
113
- send method_name, *args, &block
114
- end
115
- end
116
-
117
- def let_metadata(*keys)
118
- @steps ||= []
119
- @steps << Proc.new do
120
- m = metadata
121
- keys.each do |key|
122
- define_method key do
123
- m[key]
124
- end
125
-
126
- define_singleton_method key do
127
- m[key]
128
- end
129
- end
25
+ def match_metadata?(metadata_key)
26
+ case metadata_key
27
+ when Hash
28
+ metadata_key.keys.all? { |k| metadata[k] == metadata_key[k] }
29
+ when Array
30
+ metadata_key.map { |key| metadata[key] }.none?(&:nil?)
31
+ else
32
+ metadata[metadata_key]
130
33
  end
131
34
  end
132
35
 
133
- def let_method(method_name, &block)
134
- @steps ||= []
135
- @steps << Proc.new do
136
- define_method method_name, &block
137
- define_singleton_method method_name, &block
138
- end
36
+ def run_step(method_name, *args, &block)
37
+ instance_exec(&block)
139
38
  end
140
39
 
141
- def steps(&block)
142
- @steps ||= []
143
- @steps << block
40
+ def world
41
+ @world ||= Lopata::Config.world
144
42
  end
145
43
 
146
- def steps_if(metadata_key, &block)
147
- @steps ||= []
148
- @steps << Proc.new do
149
- if match_metadata?(metadata_key)
150
- instance_exec &block
151
- end
152
- end
153
- end
154
- alias steps_for steps_if
155
-
156
- def steps_unless(metadata_key, &block)
157
- @steps ||= []
158
- @steps << Proc.new do
159
- unless match_metadata?(metadata_key)
160
- instance_exec &block
161
- end
162
- end
163
- end
164
- alias steps_for_not steps_unless
165
-
166
- def self.convert_args(metadata, *args)
44
+ def convert_args(*args)
167
45
  args.map do |arg|
168
46
  case arg
169
47
  # trait symbols as link to metadata.
@@ -174,220 +52,25 @@ class Lopata::Scenario
174
52
  end.flatten
175
53
  end
176
54
 
177
- def self.separate_args(args)
55
+ def separate_args(args)
178
56
  args.map { |a| a.is_a?(String) && a =~ /,/ ? a.split(',').map(&:strip) : a }.flatten
179
57
  end
180
58
 
181
- def build_role_options
182
- return [] unless roles
183
- [Diagonal.new(:as, roles.map { |r| [nil, r] })]
184
- end
185
-
186
- def roles
187
- return false if @without_user
188
- @roles ||= [Lopata::Config.default_role].compact
189
- end
190
-
191
- def title(value)
192
- @title = value
193
- end
194
-
195
- def option(metadata_key, variants)
196
- options << Option.new(metadata_key, variants)
197
- end
198
-
199
- def diagonal(metadata_key, variants)
200
- diagonals << Diagonal.new(metadata_key, variants)
201
- end
202
-
203
- def options
204
- @options ||= []
205
- end
206
-
207
- def diagonals
208
- @diagonals ||= []
209
- end
210
-
211
- def option_combinations
212
- combinations = combine([OptionSet.new], options + diagonals + role_options)
213
- while !(diagonals + role_options).all?(&:complete?)
214
- combinations << OptionSet.new(*(options + diagonals + role_options).map(&:next_variant))
215
- end
216
- combinations.reject { |option_set| skip?(option_set) }
217
- end
218
-
219
- def combine(source_combinations, rest_options)
220
- # raise 'source_combinations cannot be empty' if source_combinations.blank?
221
- return source_combinations if rest_options.blank?
222
- combinations = []
223
- current_option = rest_options.shift
224
- source_combinations.each do |source_variants|
225
- current_option.level_variants.each do |v|
226
- combinations << (source_variants + OptionSet.new(v))
227
- end
228
- end
229
- combine(combinations, rest_options)
230
- end
231
-
232
- def prepare_args(option_set, *args)
233
- options_title, metadata = option_set.title, option_set.metadata
234
- if args[0].is_a? String
235
- args[0] = [@title, options_title, args[0]].reject(&:blank?).join(' ')
236
- else
237
- args.unshift([@title, options_title].reject(&:blank?).join(' '))
238
- end
239
-
240
- metadata.merge!(@common_metadata) if @common_metadata
241
-
242
- if args.last.is_a? Hash
243
- args.last.merge!(metadata)
244
- else
245
- args << metadata
246
- end
247
- args
59
+ def failed?
60
+ status == :failed
248
61
  end
249
62
 
250
- # Набор вариантов, собранный для одного теста
251
- class OptionSet
252
- attr_reader :variants
253
- def initialize(*variants)
254
- @variants = {}
255
- variants.each { |v| self << v }
256
- end
257
-
258
- def +(other_set)
259
- self.class.new(*@variants.values).tap do |sum|
260
- other_set.each { |v| sum << v }
261
- end
262
- end
63
+ private
263
64
 
264
- def <<(variant)
265
- @variants[variant.key] = variant
266
- end
267
-
268
- def [](key)
269
- @variants[key]
270
- end
271
-
272
- def each(&block)
273
- @variants.values.each(&block)
274
- end
275
-
276
- def title
277
- @variants.values.map(&:title).compact.join(' ')
278
- end
279
-
280
- def metadata
281
- @variants.values.inject({}) do |metadata, variant|
282
- metadata.merge(variant.metadata(self))
283
- end
284
- end
285
- end
286
-
287
- class Variant
288
- attr_reader :key, :title, :value
289
-
290
- def initialize(key, title, value)
291
- @key, @title, @value = key, title, check_lambda_arity(value)
292
- end
293
-
294
- def metadata(option_set)
295
- data = { key => value }
296
- if value.is_a? Hash
297
- value.each do |k, v|
298
- sub_key = "%s_%s" % [key, k]
299
- data[sub_key.to_sym] = v
300
- end
301
- end
302
-
303
- data.each do |key, v|
304
- data[key] = v.calculate(option_set) if v.is_a? CalculatedValue
305
- end
306
- data
307
- end
308
-
309
- def self.join(variants)
310
- title, metadata = nil, {}
311
- variants.each do |v|
312
- title = [title, v.title].compact.join(' ')
313
- metadata.merge!(v.metadata)
314
- end
315
- [title, metadata]
316
- end
317
-
318
- private
319
-
320
- # Лямдда будет передаваться как блок в instance_eval, которому плохеет, если пришло что-то с нулевой
321
- # arity. Поэтому для лямбд с нулевой arity делаем arity == 1
322
- def check_lambda_arity(v)
323
- if v.is_a?(Proc) && v.arity == 0
324
- ->(_) { instance_exec(&v) }
325
- else
326
- v
327
- end
328
- end
329
- end
330
-
331
- class CalculatedValue
332
- def initialize(&block)
333
- @proc = block
334
- end
335
-
336
- def calculate(option_set)
337
- @proc.call(option_set)
338
- end
339
- end
340
-
341
- class Option
342
- attr_reader :variants
343
- def initialize(key, variants)
344
- @variants =
345
- if variants.is_a? Hash
346
- variants.map { |title, value| Variant.new(key, title, value) }
347
- else
348
- # Array of arrays of two elements
349
- variants.map { |v| Variant.new(key, *v) }
350
- end
351
- end
352
-
353
- # Variants to apply at one level
354
- def level_variants
355
- variants
356
- end
357
-
358
- def next_variant
359
- @current ||= 0
360
- selected_variant = variants[@current]
361
- @current += 1
362
- if @current >= variants.length
363
- @current = 0
364
- @complete = true # all variants have been selected
65
+ def method_missing(method, *args, &block)
66
+ if metadata.keys.include?(method)
67
+ metadata[method]
68
+ else
69
+ super
365
70
  end
366
- selected_variant
367
71
  end
368
- end
369
72
 
370
- class Diagonal < Option
371
- def level_variants
372
- [next_variant]
73
+ def respond_to_missing?(method, *)
74
+ metadata.keys.include?(method) or super
373
75
  end
374
-
375
- def complete?
376
- @complete
377
- end
378
- end
379
-
380
- # RSpec helpers for spec builing
381
- module RSpecInjections
382
- def match_metadata?(metadata_key)
383
- case metadata_key
384
- when Hash
385
- metadata_key.keys.all? { |k| metadata[k] == metadata_key[k] }
386
- when Array
387
- metadata_key.map { |key| metadata[key] }.none?(&:nil?)
388
- else
389
- metadata[metadata_key]
390
- end
391
- end
392
- end
393
- end
76
+ end
@@ -0,0 +1,311 @@
1
+ class Lopata::ScenarioBuilder
2
+ def self.define(title, metadata = nil, &block)
3
+ builder = new
4
+ builder.title(title)
5
+ builder.metadata(metadata) if metadata
6
+ builder.instance_exec &block
7
+ builder.build
8
+ end
9
+
10
+ # Do noting. Exclude defined scenario from suite.
11
+ def self.xdefine(*attrs)
12
+ end
13
+
14
+ def build
15
+ world = Lopata::Config.world
16
+ option_combinations.map do |option_set|
17
+ args = prepare_args(option_set)
18
+ raise "scenario required a name in first argument" unless args.first.is_a? String
19
+ scenario = Lopata::Scenario.new(*args)
20
+
21
+ steps.each do |step|
22
+ scenario.steps << step
23
+ end
24
+
25
+ scenario.steps << Lopata::Config.after_scenario if Lopata::Config.after_scenario
26
+
27
+ world.scenarios << scenario
28
+ end
29
+ end
30
+
31
+ def as(*args, &block)
32
+ @roles = args.flatten
33
+ @roles << CalculatedValue.new(&block) if block_given?
34
+ @role_options = nil
35
+ end
36
+
37
+ def role_options
38
+ @role_options ||= build_role_options
39
+ end
40
+
41
+ def metadata(hash)
42
+ raise 'metadata expected to be a Hash' unless hash.is_a?(Hash)
43
+ @common_metadata ||= {}
44
+ @common_metadata.merge! hash
45
+ end
46
+
47
+ def without_user
48
+ @without_user = true
49
+ end
50
+
51
+ def skip_when(&block)
52
+ @skip_when = block
53
+ end
54
+
55
+ def skip?(option_set)
56
+ @skip_when && @skip_when.call(option_set)
57
+ end
58
+
59
+ %i{setup action it teardown}.each do |name|
60
+ name_if = "%s_if" % name
61
+ name_unless = "%s_unless" % name
62
+ define_method name, ->(*args, &block) { add_step(name, *args, &block) }
63
+ define_method name_if, ->(condition, *args, &block) { add_step(name, *args, if_cond: condition, &block) }
64
+ define_method name_unless, ->(condition, *args, &block) { add_step(name, *args, unless_cond: condition, &block) }
65
+ end
66
+
67
+ def add_step(method_name, *args, if_cond: nil, unless_cond: nil, &block)
68
+ steps << Lopata::Step.new(method_name, *args) do
69
+ # will be called in context of scenario
70
+ next if if_cond && !match_metadata?(if_cond)
71
+ next if unless_cond && match_metadata?(unless_cond)
72
+
73
+ flat_args = args.flatten
74
+ flat_args = separate_args(flat_args) if method_name =~ /^(setup|action)/
75
+ converted_args = convert_args(*flat_args)
76
+ converted_args.shift if method_name =~ /^it$/
77
+ converted_args.each do |step|
78
+ if step.is_a?(String)
79
+ Lopata::SharedStep.find(step).steps.each do |shared_step|
80
+ instance_exec(&shared_step.block)
81
+ end
82
+ elsif step.is_a?(Proc)
83
+ instance_exec(&step)
84
+ end
85
+ end
86
+ # run_step method_name, *converted_args, &block
87
+ instance_exec(&block) if block
88
+ end
89
+ end
90
+
91
+ def steps
92
+ @steps ||= []
93
+ end
94
+
95
+ def cleanup(*args, &block)
96
+ add_step_as_is(:cleanup, *args, &block)
97
+ end
98
+
99
+ def add_step_as_is(method_name, *args, &block)
100
+ steps << Lopata::Step.new(method_name, *args) do
101
+ # do not convert args - symbols mean name of instance variable
102
+ # run_step method_name, *args, &block
103
+ instance_exec(&block) if block
104
+ end
105
+ end
106
+
107
+ def let(method_name, &block)
108
+ steps << Lopata::Step.new(nil) do
109
+ define_singleton_method method_name, &block
110
+ end
111
+ end
112
+
113
+ def build_role_options
114
+ return [] unless roles
115
+ [Diagonal.new(:as, roles.map { |r| [nil, r] })]
116
+ end
117
+
118
+ def roles
119
+ return false if @without_user
120
+ @roles ||= [Lopata::Config.default_role].compact
121
+ end
122
+
123
+ def title(value)
124
+ @title = value
125
+ end
126
+
127
+ def option(metadata_key, variants)
128
+ options << Option.new(metadata_key, variants)
129
+ end
130
+
131
+ def diagonal(metadata_key, variants)
132
+ diagonals << Diagonal.new(metadata_key, variants)
133
+ end
134
+
135
+ def options
136
+ @options ||= []
137
+ end
138
+
139
+ def diagonals
140
+ @diagonals ||= []
141
+ end
142
+
143
+ def option_combinations
144
+ combinations = combine([OptionSet.new], options + diagonals + role_options)
145
+ while !(diagonals + role_options).all?(&:complete?)
146
+ combinations << OptionSet.new(*(options + diagonals + role_options).map(&:next_variant))
147
+ end
148
+ combinations.reject { |option_set| skip?(option_set) }
149
+ end
150
+
151
+ def combine(source_combinations, rest_options)
152
+ # raise 'source_combinations cannot be empty' if source_combinations.blank?
153
+ return source_combinations if rest_options.empty?
154
+ combinations = []
155
+ current_option = rest_options.shift
156
+ source_combinations.each do |source_variants|
157
+ current_option.level_variants.each do |v|
158
+ combinations << (source_variants + OptionSet.new(v))
159
+ end
160
+ end
161
+ combine(combinations, rest_options)
162
+ end
163
+
164
+ def prepare_args(option_set, *args)
165
+ options_title, metadata = option_set.title, option_set.metadata
166
+ if args[0].is_a? String
167
+ args[0] = [@title, options_title, args[0]].compact.reject(&:empty?).join(' ')
168
+ else
169
+ args.unshift([@title, options_title].compact.reject(&:empty?).join(' '))
170
+ end
171
+
172
+ metadata.merge!(@common_metadata) if @common_metadata
173
+
174
+ if args.last.is_a? Hash
175
+ args.last.merge!(metadata)
176
+ else
177
+ args << metadata
178
+ end
179
+ args
180
+ end
181
+
182
+ # Набор вариантов, собранный для одного теста
183
+ class OptionSet
184
+ attr_reader :variants
185
+ def initialize(*variants)
186
+ @variants = {}
187
+ variants.compact.each { |v| self << v }
188
+ end
189
+
190
+ def +(other_set)
191
+ self.class.new(*@variants.values).tap do |sum|
192
+ other_set.each { |v| sum << v }
193
+ end
194
+ end
195
+
196
+ def <<(variant)
197
+ @variants[variant.key] = variant
198
+ end
199
+
200
+ def [](key)
201
+ @variants[key]
202
+ end
203
+
204
+ def each(&block)
205
+ @variants.values.each(&block)
206
+ end
207
+
208
+ def title
209
+ @variants.values.map(&:title).compact.join(' ')
210
+ end
211
+
212
+ def metadata
213
+ @variants.values.inject({}) do |metadata, variant|
214
+ metadata.merge(variant.metadata(self))
215
+ end
216
+ end
217
+ end
218
+
219
+ class Variant
220
+ attr_reader :key, :title, :value
221
+
222
+ def initialize(key, title, value)
223
+ @key, @title, @value = key, title, check_lambda_arity(value)
224
+ end
225
+
226
+ def metadata(option_set)
227
+ data = { key => value }
228
+ if value.is_a? Hash
229
+ value.each do |k, v|
230
+ sub_key = "%s_%s" % [key, k]
231
+ data[sub_key.to_sym] = v
232
+ end
233
+ end
234
+
235
+ data.each do |key, v|
236
+ data[key] = v.calculate(option_set) if v.is_a? CalculatedValue
237
+ end
238
+ data
239
+ end
240
+
241
+ def self.join(variants)
242
+ title, metadata = nil, {}
243
+ variants.each do |v|
244
+ title = [title, v.title].compact.join(' ')
245
+ metadata.merge!(v.metadata)
246
+ end
247
+ [title, metadata]
248
+ end
249
+
250
+ private
251
+
252
+ # Лямдда будет передаваться как блок в instance_eval, которому плохеет, если пришло что-то с нулевой
253
+ # arity. Поэтому для лямбд с нулевой arity делаем arity == 1
254
+ def check_lambda_arity(v)
255
+ if v.is_a?(Proc) && v.arity == 0
256
+ ->(_) { instance_exec(&v) }
257
+ else
258
+ v
259
+ end
260
+ end
261
+ end
262
+
263
+ class CalculatedValue
264
+ def initialize(&block)
265
+ @proc = block
266
+ end
267
+
268
+ def calculate(option_set)
269
+ @proc.call(option_set)
270
+ end
271
+ end
272
+
273
+ class Option
274
+ attr_reader :variants
275
+ def initialize(key, variants)
276
+ @variants =
277
+ if variants.is_a? Hash
278
+ variants.map { |title, value| Variant.new(key, title, value) }
279
+ else
280
+ # Array of arrays of two elements
281
+ variants.map { |v| Variant.new(key, *v) }
282
+ end
283
+ end
284
+
285
+ # Variants to apply at one level
286
+ def level_variants
287
+ variants
288
+ end
289
+
290
+ def next_variant
291
+ @current ||= 0
292
+ selected_variant = variants[@current]
293
+ @current += 1
294
+ if @current >= variants.length
295
+ @current = 0
296
+ @complete = true # all variants have been selected
297
+ end
298
+ selected_variant
299
+ end
300
+ end
301
+
302
+ class Diagonal < Option
303
+ def level_variants
304
+ [next_variant]
305
+ end
306
+
307
+ def complete?
308
+ @complete
309
+ end
310
+ end
311
+ end
@@ -0,0 +1,31 @@
1
+ module Lopata
2
+ class SharedStep
3
+ attr_reader :block
4
+
5
+ class SharedStepNotFound < StandardError; end
6
+
7
+ def self.register(name, &block)
8
+ raise ArgumentError, "Comma is not allowed in shared step name: '%s'" % name if name =~ /,/
9
+ @shared_steps ||= {}
10
+ @shared_steps[name] = new(&block)
11
+ end
12
+
13
+ def self.find(name)
14
+ @shared_steps[name] or raise StandardError, "Shared step '%s' not found" % name
15
+ end
16
+
17
+ def initialize(&block)
18
+ @block = block
19
+ end
20
+
21
+ def steps
22
+ @steps ||= build_steps
23
+ end
24
+
25
+ def build_steps
26
+ builder = Lopata::ScenarioBuilder.new
27
+ builder.instance_exec(&block)
28
+ builder.steps
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,42 @@
1
+ module Lopata
2
+ class Step
3
+ attr_reader :block, :status, :exception
4
+
5
+ def initialize(method_name, *args, &block)
6
+ @method_name = method_name
7
+ @args = args
8
+ @status = :not_started
9
+ @block = block
10
+ @exception = nil
11
+ end
12
+
13
+ def run(scenario)
14
+ @status = :running
15
+ world.notify_observers(:step_started, self)
16
+ begin
17
+ scenario.instance_exec(&block)
18
+ @status = :passed
19
+ rescue Exception => e
20
+ @status = :failed
21
+ @exception = e
22
+ end
23
+ world.notify_observers(:step_finished, self)
24
+ end
25
+
26
+ def world
27
+ @world ||= Lopata::Config.world
28
+ end
29
+
30
+ def failed?
31
+ status == :failed
32
+ end
33
+
34
+ def passed?
35
+ status == :passed
36
+ end
37
+
38
+ def teardown?
39
+ %i{ teardown cleanup }.include?(@method_name)
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,5 @@
1
+ module Lopata
2
+ module Version
3
+ STRING = '0.1.0'
4
+ end
5
+ end
@@ -0,0 +1,49 @@
1
+ class Lopata::World
2
+ attr_reader :scenarios, :observers
3
+
4
+ def initialize
5
+ @scenarios = []
6
+ @observers = []
7
+ end
8
+
9
+ # Loads scenarios for running in current session
10
+ #
11
+ # @param args [Array<String>] files to be load.
12
+ # All files from default location to be loaded if empty.
13
+ def load_scenarios(*args)
14
+ if args.empty?
15
+ load_all_scenarios
16
+ else
17
+ args.each do |file|
18
+ load File.expand_path(file)
19
+ end
20
+ end
21
+ end
22
+
23
+ # Loads all scenarios from predefined paths
24
+ def load_all_scenarios
25
+ Dir["scenarios/**/*.rb"].each { |f| load File.expand_path(f) }
26
+ end
27
+
28
+ def load_shared_steps
29
+ Dir["shared_steps/**/*rb"].each { |f| load File.expand_path(f) }
30
+ end
31
+
32
+ # Called at the end of test running.
33
+ #
34
+ # Notifies observers about testing finish
35
+ def finish
36
+ notify_observers(:finished, self)
37
+ end
38
+
39
+ def notify_observers(event, context)
40
+ @observers.each do |observer|
41
+ observer.send event, context
42
+ end
43
+ end
44
+
45
+ # Define observers based on configuration
46
+ def setup_observers
47
+ @observers = [Lopata::Observers::ConsoleOutputObserver.new]
48
+ end
49
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lopata
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.16
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alexey Volochnev
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-05-06 00:00:00.000000000 Z
11
+ date: 2020-05-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: httparty
@@ -30,15 +30,57 @@ dependencies:
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '0'
33
+ version: '1.0'
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: '0'
41
- description: Functional acceptance tesging with rspec
40
+ version: '1.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec-expectations
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.9'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.9'
55
+ - !ruby/object:Gem::Dependency
56
+ name: cucumber
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.1'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.1'
69
+ - !ruby/object:Gem::Dependency
70
+ name: aruba
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '1.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '1.0'
83
+ description: Functional acceptance tesging
42
84
  email: alexey.volochnev@gmail.com
43
85
  executables:
44
86
  - lopata
@@ -56,14 +98,22 @@ files:
56
98
  - lib/lopata/generators/templates/Lopatafile
57
99
  - lib/lopata/generators/templates/config/environments/qa.yml
58
100
  - lib/lopata/generators/templates/config/initializers/capybara.rb
101
+ - lib/lopata/generators/templates/spec/spec_helper.rb
59
102
  - lib/lopata/id.rb
103
+ - lib/lopata/observers.rb
104
+ - lib/lopata/observers/base_observer.rb
105
+ - lib/lopata/observers/console_output_observer.rb
60
106
  - lib/lopata/rspec/ar_dsl.rb
61
107
  - lib/lopata/rspec/dsl.rb
62
108
  - lib/lopata/rspec/formatter.rb
63
109
  - lib/lopata/rspec/role.rb
64
- - lib/lopata/rspec/version.rb
65
110
  - lib/lopata/runner.rb
66
111
  - lib/lopata/scenario.rb
112
+ - lib/lopata/scenario_builder.rb
113
+ - lib/lopata/shared_step.rb
114
+ - lib/lopata/step.rb
115
+ - lib/lopata/version.rb
116
+ - lib/lopata/world.rb
67
117
  homepage: https://github.com/avolochnev/lopata
68
118
  licenses:
69
119
  - MIT
@@ -76,7 +126,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
76
126
  requirements:
77
127
  - - ">="
78
128
  - !ruby/object:Gem::Version
79
- version: 2.1.0
129
+ version: 2.3.0
80
130
  required_rubygems_version: !ruby/object:Gem::Requirement
81
131
  requirements:
82
132
  - - ">="
@@ -86,5 +136,5 @@ requirements: []
86
136
  rubygems_version: 3.0.3
87
137
  signing_key:
88
138
  specification_version: 4
89
- summary: lopata-0.0.16
139
+ summary: lopata-0.1.0
90
140
  test_files: []
@@ -1,7 +0,0 @@
1
- module Lopata
2
- module RSpec
3
- module Version
4
- STRING = '0.0.16'
5
- end
6
- end
7
- end