lopata 0.0.16 → 0.1.0

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