lopata 0.1.1 → 0.1.2

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: 9233d9fba2a55749f02037d5ca106c0cb9249debda448aac58ccc2cc724a378e
4
- data.tar.gz: 65ea743e92f5084b6f04b52595296cb80a3d4540f4cb8ba8bc2c774cbc448aaf
3
+ metadata.gz: 996fe5f1c16b6fed5daadff4ce365a778c2f09d79562c221e497c541541e9012
4
+ data.tar.gz: 7200a98d6bead7fb3baed82bbdffbf1c6aca805d999a563f3ada78026344dfed
5
5
  SHA512:
6
- metadata.gz: a706fde4a5acc83b27400ce50c19137b7fd9f296bfb2ac4baeec55e50e5fe48b4bf6e90110611f4f69cd2dc9571a15d1de34e54e8bec094f0c2c2d5cc5ca1a87
7
- data.tar.gz: 2d0764e801260489815f30b8572ffdff7bdf6572fc64b55d0e53c091f8f125925374ae9cf706b1fe0bc956a6d56ef23b6dd1bd767b7ce13de550b4878dc86f4f
6
+ metadata.gz: 037ccc6d687292186b752de6924a2f3373d0934569f9e2d01567cac1a4607aa33ce70f53473ad0d737d07cdb1da231bb8b526f6c24f8c2e73d494978dfc94179
7
+ data.tar.gz: 7c90f679400d64d7ab676807cea167947cee47114cf9ea8daed84c517888bf5e7e64b475c1841f9e6cb886cf08184ca3acedaf6dc5af68a3518e74e4130f91b7
@@ -0,0 +1,36 @@
1
+ module Lopata
2
+ module ActiveRecord
3
+ # To be included in Lopata::Scenario
4
+ module Methods
5
+ def cleanup(*objects)
6
+ return if Lopata::Config.ops[:keep]
7
+ objects.flatten.compact.each do |o|
8
+ begin
9
+ o.reload.destroy!
10
+ rescue ::ActiveRecord::RecordNotFound
11
+ # Already destroyed
12
+ end
13
+ end
14
+ end
15
+
16
+ def reload(*objects)
17
+ objects.flatten.each(&:reload)
18
+ end
19
+ end
20
+
21
+ # To be included in Lopata::ScenarioBuilder
22
+ module DSL
23
+ def cleanup(*vars, &block)
24
+ unless vars.empty?
25
+ teardown do
26
+ cleanup vars.map { |v| instance_variable_get "@#{v}" }
27
+ end
28
+ end
29
+ teardown &block if block_given?
30
+ end
31
+ end
32
+ end
33
+ end
34
+
35
+ Lopata::Scenario.include Lopata::ActiveRecord::Methods
36
+ Lopata::ScenarioBuilder.include Lopata::ActiveRecord::DSL
@@ -5,8 +5,6 @@ module Lopata
5
5
  @condition, @positive = condition, positive
6
6
  end
7
7
 
8
- EMPTY = new({})
9
-
10
8
  alias positive? positive
11
9
 
12
10
  def match?(scenario)
data/lib/lopata/config.rb CHANGED
@@ -2,8 +2,8 @@ module Lopata
2
2
  module Config
3
3
  extend self
4
4
 
5
- attr_accessor :build_number, :lopata_host, :lopata_code, :only_roles, :role_descriptions, :after_as,
6
- :default_role, :ops, :after_scenario
5
+ attr_accessor :build_number, :lopata_host, :lopata_code, :only_roles, :role_descriptions,
6
+ :default_role, :ops
7
7
 
8
8
  def init(env)
9
9
  require 'yaml'
@@ -40,34 +40,15 @@ module Lopata
40
40
  init_rspec_filters
41
41
  end
42
42
 
43
- def init_active_record
44
- require 'lopata/rspec/ar_dsl'
45
- ::RSpec.configure do |c|
46
- c.include Lopata::RSpec::AR::DSL
47
- end
48
- end
49
-
50
43
  def init_lopata_logging(build)
51
44
  self.build_number = build
52
45
  require 'lopata/observers/web_logger'
53
46
  add_observer Lopata::Observers::WebLogger.new
54
47
  end
55
48
 
56
- def init_rerun
57
- ::RSpec.configure do |c|
58
- c.inclusion_filter = { full_description: build_rerun_filter_proc }
59
- end
60
- end
61
-
62
49
  def init_rspec_filters
63
50
  filters = {}
64
51
  filters[:focus] = true if ops[:focus]
65
- if ops[:rerun]
66
- filters[:full_description] = build_rerun_filter_proc
67
- end
68
- if ops[:text]
69
- filters[:full_description] = ->(desc) { desc.include?(ops[:text]) }
70
- end
71
52
  unless filters.blank?
72
53
  ::RSpec.configure do |c|
73
54
  c.inclusion_filter = filters
@@ -75,17 +56,28 @@ module Lopata
75
56
  end
76
57
  end
77
58
 
78
- def build_rerun_filter_proc
79
- to_rerun = Lopata::Client.new(Lopata::Config.build_number).to_rerun
80
- Proc.new do |desc|
81
- to_rerun.include?(desc)
82
- end
83
- end
84
-
85
59
  def before_start(&block)
86
60
  @before_start = block
87
61
  end
88
62
 
63
+ def before_scenario(*steps, &block)
64
+ before_scenario_steps.append(*steps) unless steps.empty?
65
+ before_scenario_steps.append(block) if block_given?
66
+ end
67
+
68
+ def before_scenario_steps
69
+ @before_scenario_steps ||= []
70
+ end
71
+
72
+ def after_scenario(*steps, &block)
73
+ after_scenario_steps.append(*steps) unless steps.empty?
74
+ after_scenario_steps.append(block) if block_given?
75
+ end
76
+
77
+ def after_scenario_steps
78
+ @after_scenario_steps ||= []
79
+ end
80
+
89
81
  def initialize_test
90
82
  @before_start.call if @before_start
91
83
  end
@@ -94,6 +86,10 @@ module Lopata
94
86
  @world ||= Lopata::World.new
95
87
  end
96
88
 
89
+ def filters
90
+ @filters ||= []
91
+ end
92
+
97
93
  def add_observer(observer)
98
94
  world.observers << observer
99
95
  end
@@ -0,0 +1,36 @@
1
+ require_relative 'active_record'
2
+
3
+ module Lopata
4
+ module FactoryBot
5
+ # To be included in Lopata::Scenario
6
+ module Methods
7
+ def create(*params)
8
+ cleanup_later ::FactoryBot.create(*params)
9
+ end
10
+
11
+ def find_created(cls, params)
12
+ cleanup_later cls.where(params).take
13
+ end
14
+
15
+ def cleanup_later(object)
16
+ return nil unless object
17
+ @created_objects ||= []
18
+ @created_objects << object
19
+ object
20
+ end
21
+ end
22
+
23
+ # To be included in Lopata::ScenarioBuilder
24
+ module DSL
25
+ end
26
+ end
27
+ end
28
+
29
+ Lopata::Scenario.include Lopata::FactoryBot::Methods
30
+ Lopata::ScenarioBuilder.include Lopata::FactoryBot::DSL
31
+
32
+ Lopata.configure do |c|
33
+ c.after_scenario { cleanup @created_objects }
34
+ end
35
+
36
+ ::FactoryBot.find_definitions unless Lopata::Config.readonly
@@ -0,0 +1,89 @@
1
+ module Lopata
2
+ module Observers
3
+ # Based on RSpec::Core::BacktraceFormatter
4
+ class BacktraceFormatter
5
+ attr_accessor :exclusion_patterns, :inclusion_patterns
6
+
7
+ def initialize
8
+ patterns = %w[ /lib\d*/ruby/ bin/ exe/lopata /lib/bundler/ /exe/bundle: ]
9
+ patterns.map! { |s| Regexp.new(s.gsub("/", File::SEPARATOR)) }
10
+
11
+ @exclusion_patterns = [Regexp.union(*patterns)]
12
+ @inclusion_patterns = []
13
+
14
+ inclusion_patterns << Regexp.new(Dir.getwd)
15
+ end
16
+
17
+ def format(backtrace)
18
+ return [] unless backtrace
19
+ return backtrace if backtrace.empty?
20
+
21
+ backtrace.map { |l| backtrace_line(l) }.compact.
22
+ tap do |filtered|
23
+ if filtered.empty?
24
+ filtered.concat backtrace
25
+ filtered << ""
26
+ filtered << " Showing full backtrace because every line was filtered out."
27
+ end
28
+ end
29
+ end
30
+
31
+ def error_message(exception, include_backtrace: false)
32
+ backtrace = format(exception.backtrace)
33
+ source_line = extract_source_line(backtrace.first)
34
+ msg = ''
35
+ msg << "\n#{source_line}\n" if source_line
36
+ msg << "#{exception.class.name}: " unless exception.class.name =~ /RSpec/
37
+ msg << exception.message if exception.message
38
+ msg << "\n#{backtrace.join("\n")}\n" if include_backtrace
39
+ msg
40
+ end
41
+
42
+ def extract_source_line(backtrace_line)
43
+ file_and_line_number = backtrace_line.match(/(.+?):(\d+)(|:\d+)/)
44
+ return nil unless file_and_line_number
45
+ file_path, line_number = file_and_line_number[1..2]
46
+ return nil unless File.exist?(file_path)
47
+ lines = File.read(file_path).split("\n")
48
+ lines[line_number.to_i - 1]
49
+ end
50
+
51
+ def backtrace_line(line)
52
+ relative_path(line) unless exclude?(line)
53
+ end
54
+
55
+ def exclude?(line)
56
+ matches?(exclusion_patterns, line) && !matches?(inclusion_patterns, line)
57
+ end
58
+
59
+ private
60
+
61
+ def matches?(patterns, line)
62
+ patterns.any? { |p| line =~ p }
63
+ end
64
+
65
+ # Matches strings either at the beginning of the input or prefixed with a
66
+ # whitespace, containing the current path, either postfixed with the
67
+ # separator, or at the end of the string. Match groups are the character
68
+ # before and the character after the string if any.
69
+ #
70
+ # http://rubular.com/r/fT0gmX6VJX
71
+ # http://rubular.com/r/duOrD4i3wb
72
+ # http://rubular.com/r/sbAMHFrOx1
73
+ def relative_path_regex
74
+ @relative_path_regex ||= /(\A|\s)#{File.expand_path('.')}(#{File::SEPARATOR}|\s|\Z)/
75
+ end
76
+
77
+ # @param line [String] current code line
78
+ # @return [String] relative path to line
79
+ def relative_path(line)
80
+ line = line.sub(relative_path_regex, "\\1.\\2".freeze)
81
+ line = line.sub(/\A([^:]+:\d+)$/, '\\1'.freeze)
82
+ return nil if line == '-e:1'.freeze
83
+ line
84
+ rescue SecurityError
85
+ nil
86
+ end
87
+ end
88
+ end
89
+ end
@@ -1,3 +1,5 @@
1
+ require_relative 'backtrace_formatter'
2
+
1
3
  module Lopata
2
4
  module Observers
3
5
  class ConsoleOutputObserver < BaseObserver
@@ -11,24 +13,14 @@ module Lopata
11
13
  puts "#{total} scenario%s %s" % [total == 1 ? '' : 's', details]
12
14
  end
13
15
 
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
16
  def scenario_finished(scenario)
23
17
  message = "#{scenario.title} #{bold(scenario.status.to_s.upcase)}"
24
18
  puts colored(message, scenario.status)
19
+ return unless scenario.failed?
25
20
 
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
21
+ scenario.steps_in_running_order.each do |step|
22
+ puts colored(" #{status_marker(step.status)} #{step.title}", step.status)
23
+ puts indent(4, backtrace_formatter.error_message(step.exception, include_backtrace: true)) if step.failed?
32
24
  end
33
25
  end
34
26
 
@@ -38,25 +30,48 @@ module Lopata
38
30
  case status
39
31
  when :failed then red(text)
40
32
  when :passed then green(text)
33
+ when :skipped then cyan(text)
34
+ when :pending then yellow(text)
41
35
  else text
42
36
  end
43
37
  end
44
38
 
45
- def red(text)
46
- wrap(text, 31)
39
+ {
40
+ red: 31,
41
+ green: 32,
42
+ cyan: 36,
43
+ yellow: 33,
44
+ bold: 1,
45
+ }.each do |color, code|
46
+ define_method(color) do |text|
47
+ wrap(text, code)
48
+ end
49
+ end
50
+
51
+ def wrap(text, code)
52
+ "\e[#{code}m#{text}\e[0m"
47
53
  end
48
54
 
49
- def green(text)
50
- wrap(text, 32)
55
+ def backtrace_formatter
56
+ @backtrace_formatter ||= Lopata::Observers::BacktraceFormatter.new
51
57
  end
52
58
 
53
- def bold(text)
54
- wrap(text, 1)
59
+ def status_marker(status)
60
+ case status
61
+ when :failed then "[!]"
62
+ when :skipped then "[-]"
63
+ when :pending then "[?]"
64
+ else "[+]"
65
+ end
55
66
  end
56
67
 
57
- def wrap(text, code)
58
- "\e[#{code}m#{text}\e[0m"
68
+ # Adds indent to text
69
+ # @param cols [Number] number of spaces to be added
70
+ # @param text [String] text to add indent
71
+ # @return [String] text with indent
72
+ def indent(cols, text)
73
+ text.split("\n").map { |line| " " * cols + line }.join("\n")
59
74
  end
60
75
  end
61
76
  end
62
- end
77
+ end
@@ -1,5 +1,6 @@
1
1
  require 'httparty'
2
2
  require 'json'
3
+ require_relative 'backtrace_formatter'
3
4
 
4
5
  module Lopata
5
6
  module Observers
@@ -11,46 +12,20 @@ module Lopata
11
12
  end
12
13
 
13
14
  def scenario_finished(scenario)
14
- if scenario.failed?
15
- backtrace = backtrace_for(scenario)
16
- @client.add_attempt(scenario, Lopata::FAILED, error_message_for(scenario), backtrace)
17
- else
18
- @client.add_attempt(scenario, Lopata::PASSED)
19
- end
15
+ @client.add_attempt(scenario)
20
16
  end
21
17
 
22
18
  # def example_pending(notification)
23
19
  # example = notification.example
24
20
  # @client.add_attempt(example, Lopata::PENDING, example.execution_result.pending_message)
25
21
  # end
26
-
27
- private
28
-
29
- def error_message_for(scenario)
30
- exception = scenario.steps.map(&:exception).compact.last
31
- msg = ''
32
- if exception
33
- msg << "#{exception.class.name}: " unless exception.class.name =~ /RSpec/
34
- msg << "#{exception.message.to_s}" if exception.message
35
- end
36
- (msg.length == 0) ? 'Empty message' : msg
37
- end
38
-
39
- def backtrace_for(scenario)
40
- exception = scenario.steps.map(&:exception).compact.last
41
- msg = ''
42
- if exception
43
- msg = exception.backtrace.join("\n")
44
- msg << "\n"
45
- end
46
- msg
47
- end
48
22
  end
49
23
  end
50
24
 
51
25
  PASSED = 0
52
26
  FAILED = 1
53
27
  PENDING = 2
28
+ SKIPPED = 5
54
29
 
55
30
  class Client
56
31
  include HTTParty
@@ -66,15 +41,24 @@ module Lopata
66
41
  @launch_id = JSON.parse(post("/projects/#{project_code}/builds/#{build_number}/launches.json", body: {total: count}).body)['id']
67
42
  end
68
43
 
69
- def add_attempt(scenario, status, msg = nil, backtrace = nil)
44
+ def add_attempt(scenario)
45
+ status = scenario.failed? ? Lopata::FAILED : Lopata::PASSED
46
+ steps = scenario.steps_in_running_order.map { |s| step_hash(s) }
47
+ request = { status: status, steps: steps }
70
48
  test = test_id(scenario)
71
- request = { status: status}
72
- request[:message] = msg if msg
73
- request[:backtrace] = backtrace if backtrace
74
49
  post("/tests/#{test}/attempts.json", body: request)
75
50
  inc_finished
76
51
  end
77
52
 
53
+ def step_hash(step)
54
+ hash = { status: step.status, title: step.title }
55
+ if step.failed?
56
+ hash[:message] = error_message_for(step)
57
+ hash[:backtrace] = backtrace_for(step)
58
+ end
59
+ hash
60
+ end
61
+
78
62
  def test_id(scenario)
79
63
  request = {
80
64
  test: {
@@ -125,5 +109,26 @@ module Lopata
125
109
  def project_code
126
110
  Lopata::Config.lopata_code
127
111
  end
112
+
113
+ def error_message_for(step)
114
+ if step.exception
115
+ backtrace_formatter.error_message(step.exception)
116
+ else
117
+ 'Empty error message'
118
+ end
119
+ end
120
+
121
+ def backtrace_for(step)
122
+ msg = ''
123
+ if step.exception
124
+ msg = backtrace_formatter.format(step.exception.backtrace).join("\n")
125
+ msg << "\n"
126
+ end
127
+ msg
128
+ end
129
+
130
+ def backtrace_formatter
131
+ @backtrace_formatter ||= Lopata::Observers::BacktraceFormatter.new
132
+ end
128
133
  end
129
134
  end
@@ -11,7 +11,7 @@ module Lopata
11
11
  if context.is_a?(Proc)
12
12
  action(&context)
13
13
  else
14
- include_context context
14
+ verify context
15
15
  end
16
16
  end
17
17
  before(:all, &block) if block_given?
@@ -20,7 +20,6 @@ module Lopata::RSpec::Role
20
20
  else
21
21
  Lopata::RSpec::Role.filter_roles(*names).each do |name|
22
22
  example_group_class = describe role_description(name), :current_role => name do
23
- instance_exec &Lopata::Config.after_as if Lopata::Config.after_as
24
23
  define_method :current_role do
25
24
  name
26
25
  end
data/lib/lopata/runner.rb CHANGED
@@ -40,15 +40,25 @@ module Lopata
40
40
  def configure_from_options
41
41
  Lopata::Config.ops = {
42
42
  focus: options[:focus],
43
- rerun: options[:rerun],
44
43
  users: options[:users],
45
44
  build: options[:build],
46
45
  env: options[:env],
47
46
  keep: options[:keep],
48
- text: options[:text]
49
47
  }
50
48
  Lopata::Config.init(options[:env])
51
49
  Lopata::Config.initialize_test
50
+ add_text_filter(options[:text]) if options[:text]
51
+ add_rerun_filter if options[:rerun]
52
+ end
53
+
54
+ def add_text_filter(text)
55
+ Lopata::Config.filters << -> (scenario) { scenario.title.include?(text) }
56
+ end
57
+
58
+ def add_rerun_filter
59
+ to_rerun = Lopata::Client.new(Lopata::Config.build_number).to_rerun
60
+ puts to_rerun
61
+ Lopata::Config.filters << -> (scenario) { to_rerun.include?(scenario.title) }
52
62
  end
53
63
  end
54
64
  end
@@ -3,55 +3,83 @@ require 'rspec/expectations'
3
3
  class Lopata::Scenario
4
4
  include RSpec::Matchers
5
5
 
6
- attr_reader :title, :metadata, :steps, :status
6
+ attr_reader :execution
7
7
 
8
- def initialize(title, options_title, metadata = {})
9
- @title = [title, options_title].compact.reject(&:empty?).join(' ')
10
- @metadata = metadata
11
- @steps = []
12
- @status = :not_runned
8
+ def initialize(execution)
9
+ @execution = execution
13
10
  end
14
11
 
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)
12
+ # Marks current step as pending
13
+ def pending(message = nil)
14
+ execution.current_step.pending!(message)
23
15
  end
24
16
 
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?)
17
+ def metadata
18
+ execution.metadata
19
+ end
20
+
21
+ private
22
+
23
+ def method_missing(method, *args, &block)
24
+ if metadata.keys.include?(method)
25
+ metadata[method]
31
26
  else
32
- metadata[metadata_key]
27
+ super
33
28
  end
34
29
  end
35
30
 
36
- def world
37
- @world ||= Lopata::Config.world
31
+ def respond_to_missing?(method, *)
32
+ metadata.keys.include?(method) or super
38
33
  end
39
34
 
40
- def failed?
41
- status == :failed
42
- end
35
+ class Execution
36
+ attr_reader :scenario, :status, :steps, :title, :current_step
43
37
 
44
- private
38
+ def initialize(title, options_title, metadata = {})
39
+ @title = [title, options_title].compact.reject(&:empty?).join(' ')
40
+ @metadata = metadata
41
+ @status = :not_runned
42
+ @steps = []
43
+ @scenario = Lopata::Scenario.new(self)
44
+ end
45
45
 
46
- def method_missing(method, *args, &block)
47
- if metadata.keys.include?(method)
48
- metadata[method]
49
- else
50
- super
51
- end
46
+ def run
47
+ @status = :running
48
+ world.notify_observers(:scenario_started, self)
49
+ steps_in_running_order.each(&method(:run_step))
50
+ @status = steps.any?(&:failed?) ? :failed : :passed
51
+ world.notify_observers(:scenario_finished, self)
52
+ end
53
+
54
+ def run_step(step)
55
+ return if step.skipped?
56
+ @current_step = step
57
+ step.run(scenario)
58
+ skip_rest if step.failed? && step.skip_rest_on_failure?
52
59
  end
53
60
 
54
- def respond_to_missing?(method, *)
55
- metadata.keys.include?(method) or super
61
+ def world
62
+ @world ||= Lopata::Config.world
56
63
  end
57
- end
64
+
65
+ def failed?
66
+ status == :failed
67
+ end
68
+
69
+ def steps_in_running_order
70
+ steps.reject(&:teardown_group?) + steps.select(&:teardown_group?)
71
+ end
72
+
73
+ def skip_rest
74
+ steps.select { |s| s.status == :not_runned && !s.teardown? }.each(&:skip!)
75
+ end
76
+
77
+ def metadata
78
+ if current_step
79
+ @metadata.merge(current_step.metadata)
80
+ else
81
+ @metadata
82
+ end
83
+ end
84
+ end
85
+ end
@@ -1,5 +1,6 @@
1
1
  class Lopata::ScenarioBuilder
2
2
  attr_reader :title, :common_metadata
3
+ attr_accessor :shared_step, :group
3
4
 
4
5
  def self.define(title, metadata = {}, &block)
5
6
  builder = new(title, metadata)
@@ -12,18 +13,18 @@ class Lopata::ScenarioBuilder
12
13
  end
13
14
 
14
15
  def build
16
+ filters = Lopata::Config.filters
15
17
  option_combinations.each do |option_set|
16
18
  metadata = common_metadata.merge(option_set.metadata)
17
- scenario = Lopata::Scenario.new(title, option_set.title, metadata)
19
+ scenario = Lopata::Scenario::Execution.new(title, option_set.title, metadata)
18
20
 
19
- steps.each do |step|
20
- next if step.condition && !step.condition.match?(scenario)
21
- step.pre_steps(scenario).each { |s| scenario.steps << s }
22
- scenario.steps << step
21
+ unless filters.empty?
22
+ next unless filters.all? { |f| f[scenario] }
23
23
  end
24
24
 
25
- if Lopata::Config.after_scenario
26
- scenario.steps << Lopata::Step.new(:after_scenario, &Lopata::Config.after_scenario)
25
+ steps_with_hooks.each do |step|
26
+ next if step.condition && !step.condition.match?(scenario)
27
+ step.execution_steps(scenario).each { |s| scenario.steps << s }
27
28
  end
28
29
 
29
30
  world.scenarios << scenario
@@ -58,28 +59,49 @@ class Lopata::ScenarioBuilder
58
59
  @skip_when && @skip_when.call(option_set)
59
60
  end
60
61
 
61
- %i{ setup action it teardown }.each do |name|
62
+ %i{ setup action it teardown verify context }.each do |name|
62
63
  name_if = "%s_if" % name
63
64
  name_unless = "%s_unless" % name
64
- define_method name, ->(*args, &block) { add_step(name, *args, &block) }
65
- define_method name_if, ->(condition, *args, &block) { add_step(name, *args, condition: Lopata::Condition.new(condition), &block) }
66
- define_method name_unless, ->(condition, *args, &block) { add_step(name, *args, condition: Lopata::Condition.new(condition, positive: false), &block) }
65
+ define_method name, ->(*args, **metadata, &block) { add_step(name, *args, metadata: metadata, &block) }
66
+ define_method name_if, ->(condition, *args, **metadata, &block) {
67
+ add_step(name, *args, metadata: metadata, condition: Lopata::Condition.new(condition), &block)
68
+ }
69
+ define_method name_unless, ->(condition, *args, **metadata, &block) {
70
+ add_step(name, *args, condition: Lopata::Condition.new(condition, positive: false), metadata: metadata, &block)
71
+ }
67
72
  end
68
73
 
69
- def add_step(method_name, *args, condition: nil, &block)
74
+ def add_step(method_name, *args, condition: nil, metadata: {}, &block)
70
75
  step_class =
71
- if method_name =~ /^(setup|action|teardown)/
72
- Lopata::ActionStep
73
- else
74
- Lopata::Step
76
+ case method_name
77
+ when /^(setup|action|teardown|verify)/ then Lopata::ActionStep
78
+ when /^(context)/ then Lopata::GroupStep
79
+ else Lopata::Step
75
80
  end
76
- steps << step_class.new(method_name, *args, condition: condition, &block)
81
+ step = step_class.new(method_name, *args, condition: condition, shared_step: shared_step, group: group, &block)
82
+ step.metadata = metadata
83
+ steps << step
77
84
  end
78
85
 
79
86
  def steps
80
87
  @steps ||= []
81
88
  end
82
89
 
90
+ def steps_with_hooks
91
+ s = []
92
+ unless Lopata::Config.before_scenario_steps.empty?
93
+ s << Lopata::ActionStep.new(:setup, *Lopata::Config.before_scenario_steps)
94
+ end
95
+
96
+ s += steps
97
+
98
+ unless Lopata::Config.after_scenario_steps.empty?
99
+ s << Lopata::ActionStep.new(:teardown, *Lopata::Config.after_scenario_steps)
100
+ end
101
+
102
+ s
103
+ end
104
+
83
105
  def cleanup(*args, &block)
84
106
  add_step_as_is(:cleanup, *args, &block)
85
107
  end
@@ -187,10 +209,10 @@ class Lopata::ScenarioBuilder
187
209
  end
188
210
 
189
211
  class Variant
190
- attr_reader :key, :title, :value
212
+ attr_reader :key, :title, :value, :option
191
213
 
192
- def initialize(key, title, value)
193
- @key, @title, @value = key, title, check_lambda_arity(value)
214
+ def initialize(option, key, title, value)
215
+ @option, @key, @title, @value = option, key, title, check_lambda_arity(value)
194
216
  end
195
217
 
196
218
  def metadata(option_set)
@@ -202,6 +224,10 @@ class Lopata::ScenarioBuilder
202
224
  end
203
225
  end
204
226
 
227
+ option.available_metadata_keys.each do |key|
228
+ data[key] = nil unless data.has_key?(key)
229
+ end
230
+
205
231
  data.each do |key, v|
206
232
  data[key] = v.calculate(option_set) if v.is_a? CalculatedValue
207
233
  end
@@ -241,14 +267,15 @@ class Lopata::ScenarioBuilder
241
267
  end
242
268
 
243
269
  class Option
244
- attr_reader :variants
270
+ attr_reader :variants, :key
245
271
  def initialize(key, variants)
272
+ @key = key
246
273
  @variants =
247
274
  if variants.is_a? Hash
248
- variants.map { |title, value| Variant.new(key, title, value) }
275
+ variants.map { |title, value| Variant.new(self, key, title, value) }
249
276
  else
250
277
  # Array of arrays of two elements
251
- variants.map { |v| Variant.new(key, *v) }
278
+ variants.map { |v| Variant.new(self, key, *v) }
252
279
  end
253
280
  end
254
281
 
@@ -267,6 +294,11 @@ class Lopata::ScenarioBuilder
267
294
  end
268
295
  selected_variant
269
296
  end
297
+
298
+ def available_metadata_keys
299
+ @available_metadata_keys ||= variants
300
+ .map(&:value).select { |v| v.is_a?(Hash) }.flat_map(&:keys).map { |k| "#{key}_#{k}".to_sym }.uniq
301
+ end
270
302
  end
271
303
 
272
304
  class Diagonal < Option
@@ -2,16 +2,15 @@ module Lopata
2
2
  class SharedStep
3
3
  attr_reader :name, :block
4
4
 
5
- class SharedStepNotFound < StandardError; end
5
+ class NotFound < StandardError; end
6
6
 
7
7
  def self.register(name, &block)
8
8
  raise ArgumentError, "Comma is not allowed in shared step name: '%s'" % name if name =~ /,/
9
- @shared_steps ||= {}
10
- @shared_steps[name] = new(name, &block)
9
+ registry[name] = new(name, &block)
11
10
  end
12
11
 
13
12
  def self.find(name)
14
- @shared_steps[name] or raise StandardError, "Shared step '%s' not found" % name
13
+ registry[name] or raise NotFound, "Shared step '%s' not found" % name
15
14
  end
16
15
 
17
16
  def initialize(name, &block)
@@ -24,8 +23,15 @@ module Lopata
24
23
 
25
24
  def build_steps
26
25
  builder = Lopata::ScenarioBuilder.new(name)
26
+ builder.shared_step = self
27
27
  builder.instance_exec(&block)
28
28
  builder.steps
29
29
  end
30
+
31
+ private
32
+
33
+ def self.registry
34
+ @shared_steps ||= {}
35
+ end
30
36
  end
31
37
  end
data/lib/lopata/step.rb CHANGED
@@ -1,14 +1,118 @@
1
1
  module Lopata
2
2
  class Step
3
- attr_reader :block, :status, :exception, :args, :condition
3
+ attr_reader :block, :args, :condition, :method_name, :shared_step, :group
4
+ # metadata overrien by the step.
5
+ attr_accessor :metadata
4
6
 
5
- def initialize(method_name, *args, condition: nil, &block)
7
+ def initialize(method_name, *args, condition: nil, shared_step: nil, group: nil, &block)
6
8
  @method_name = method_name
7
9
  @args = args
8
- @status = :not_started
9
10
  @block = block
11
+ @shared_step = shared_step
12
+ @condition = condition
13
+ @group = group
14
+ initialized! if defined? initialized!
15
+ end
16
+
17
+ def title
18
+ base_title = args.first
19
+ base_title ||= shared_step && "#{method_name.capitalize} #{shared_step.name}" || "Untitled #{method_name}"
20
+ if group
21
+ "#{group.title}: #{base_title}"
22
+ else
23
+ base_title
24
+ end
25
+ end
26
+
27
+ def execution_steps(scenario, groups: [])
28
+ return [] if condition && !condition.match?(scenario)
29
+ return [] unless block
30
+ [StepExecution.new(self, groups, &block)]
31
+ end
32
+ end
33
+
34
+ # Used for action, setup, teardown
35
+ class ActionStep < Step
36
+ def execution_steps(scenario, groups: [])
37
+ steps = []
38
+ return steps if condition && !condition.match?(scenario)
39
+ convert_args(scenario).each do |step|
40
+ if step.is_a?(String)
41
+ Lopata::SharedStep.find(step).steps.each do |shared_step|
42
+ next if shared_step.condition && !shared_step.condition.match?(scenario)
43
+ steps += shared_step.execution_steps(scenario, groups: groups)
44
+ end
45
+ elsif step.is_a?(Proc)
46
+ steps << StepExecution.new(self, groups, &step)
47
+ end
48
+ end
49
+ steps << StepExecution.new(self, groups, &block) if block
50
+ steps.reject { |s| !s.block }
51
+ end
52
+
53
+ def separate_args(args)
54
+ args.map { |a| a.is_a?(String) && a =~ /,/ ? a.split(',').map(&:strip) : a }.flatten
55
+ end
56
+
57
+ def convert_args(scenario)
58
+ flat_args = separate_args(args.flatten)
59
+ flat_args.map do |arg|
60
+ case arg
61
+ # trait symbols as link to metadata.
62
+ when Symbol then scenario.metadata[arg]
63
+ else
64
+ arg
65
+ end
66
+ end.flatten
67
+ end
68
+
69
+ def title
70
+ if group
71
+ "%s: %s" % [group.title, method_name]
72
+ else
73
+ shared_step && "#{method_name.capitalize} #{shared_step.name}" || "Untitled #{method_name}"
74
+ end
75
+ end
76
+ end
77
+
78
+ # Used for context
79
+ class GroupStep < Step
80
+
81
+ def execution_steps(scenario, groups: [])
82
+ steps = []
83
+ return steps if condition && !condition.match?(scenario)
84
+ @steps.each do |step|
85
+ steps += step.execution_steps(scenario, groups: groups + [self])
86
+ end
87
+ steps.reject! { |s| !s.block }
88
+ steps.reject { |s| s.teardown_group?(self) } + steps.select { |s| s.teardown_group?(self) }
89
+ end
90
+
91
+ private
92
+
93
+ # Group step's block is a block in context of builder, not scenario. So hide the @block to not be used in scenario.
94
+ def initialized!
95
+ builder = Lopata::ScenarioBuilder.new(title)
96
+ builder.group = self
97
+ builder.instance_exec(&@block)
98
+ @steps = builder.steps
99
+ @block = nil
100
+ end
101
+ end
102
+
103
+ class StepExecution
104
+ attr_reader :step, :status, :exception, :block, :pending_message, :groups
105
+ extend Forwardable
106
+ def_delegators :step, :title, :method_name
107
+
108
+ class PendingStepFixedError < StandardError; end
109
+
110
+ def initialize(step, groups, &block)
111
+ @step = step
112
+ @status = :not_runned
10
113
  @exception = nil
11
- @condition = condition || Lopata::Condition::EMPTY
114
+ @block = block
115
+ @groups = groups
12
116
  end
13
117
 
14
118
  def run(scenario)
@@ -16,16 +120,22 @@ module Lopata
16
120
  world.notify_observers(:step_started, self)
17
121
  begin
18
122
  run_step(scenario)
19
- @status = :passed
123
+ if pending?
124
+ @status = :failed
125
+ raise PendingStepFixedError, 'Expected step to fail since it is pending, but it passed.'
126
+ else
127
+ @status = :passed
128
+ end
20
129
  rescue Exception => e
21
- @status = :failed
130
+ @status = :failed unless pending?
22
131
  @exception = e
23
132
  end
24
133
  world.notify_observers(:step_finished, self)
25
134
  end
26
135
 
27
136
  def run_step(scenario)
28
- scenario.instance_exec(&block) if block
137
+ return unless block
138
+ scenario.instance_exec(&block)
29
139
  end
30
140
 
31
141
  def world
@@ -40,47 +150,38 @@ module Lopata
40
150
  status == :passed
41
151
  end
42
152
 
43
- def teardown?
44
- %i{ teardown cleanup }.include?(@method_name)
153
+ def skipped?
154
+ status == :skipped
45
155
  end
46
156
 
47
- def pre_steps(scenario)
48
- []
157
+ def skip!
158
+ @status = :skipped
49
159
  end
50
- end
51
160
 
52
- # Used for action, setup, teardown
53
- class ActionStep < Step
54
- def pre_steps(scenario)
55
- steps = []
56
- convert_args(scenario).each do |step|
57
- if step.is_a?(String)
58
- Lopata::SharedStep.find(step).steps.each do |shared_step|
59
- steps += shared_step.pre_steps(scenario)
60
- steps << shared_step
61
- end
62
- elsif step.is_a?(Proc)
63
- steps << Lopata::Step.new(method_name, &step)
64
- end
65
- end
66
- steps
161
+ def pending?
162
+ status == :pending
67
163
  end
68
164
 
69
- def separate_args(args)
70
- args.map { |a| a.is_a?(String) && a =~ /,/ ? a.split(',').map(&:strip) : a }.flatten
165
+ def pending!(message = nil)
166
+ @status = :pending
167
+ @pending_message = message
71
168
  end
72
169
 
73
- def convert_args(scenario)
74
- flat_args = separate_args(args.flatten)
75
- flat_args.map do |arg|
76
- case arg
77
- # trait symbols as link to metadata.
78
- when Symbol then scenario.metadata[arg]
79
- else
80
- arg
81
- end
82
- end.flatten
170
+ def teardown?
171
+ %i{ teardown cleanup }.include?(method_name)
83
172
  end
84
173
 
174
+ def teardown_group?(group = nil)
175
+ teardown? && self.groups.last == group
176
+ end
177
+
178
+ def skip_rest_on_failure?
179
+ %i{ setup action }.include?(method_name)
180
+ end
181
+
182
+ # Step metadata is a combination of metadata given for step and all contexts (groups) the step included
183
+ def metadata
184
+ ([step] + groups).compact.inject({}) { |merged, part| merged.merge(part.metadata) }
185
+ end
85
186
  end
86
- end
187
+ end
@@ -1,5 +1,5 @@
1
1
  module Lopata
2
2
  module Version
3
- STRING = '0.1.1'
3
+ STRING = '0.1.2'
4
4
  end
5
5
  end
data/lib/lopata.rb CHANGED
@@ -17,4 +17,8 @@ module Lopata
17
17
  def self.shared_step(name, &block)
18
18
  Lopata::SharedStep.register(name, &block)
19
19
  end
20
+
21
+ def self.configure(&block)
22
+ yield Lopata::Config
23
+ end
20
24
  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.1.1
4
+ version: 0.1.2
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-14 00:00:00.000000000 Z
11
+ date: 2020-05-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: httparty
@@ -80,7 +80,7 @@ dependencies:
80
80
  - - "~>"
81
81
  - !ruby/object:Gem::Version
82
82
  version: '1.0'
83
- description: Functional acceptance tesging
83
+ description: Functional acceptance testing
84
84
  email: alexey.volochnev@gmail.com
85
85
  executables:
86
86
  - lopata
@@ -90,8 +90,10 @@ files:
90
90
  - README.md
91
91
  - exe/lopata
92
92
  - lib/lopata.rb
93
+ - lib/lopata/active_record.rb
93
94
  - lib/lopata/condition.rb
94
95
  - lib/lopata/config.rb
96
+ - lib/lopata/factory_bot.rb
95
97
  - lib/lopata/generators/app.rb
96
98
  - lib/lopata/generators/templates/.rspec
97
99
  - lib/lopata/generators/templates/Gemfile
@@ -102,10 +104,10 @@ files:
102
104
  - lib/lopata/id.rb
103
105
  - lib/lopata/loader.rb
104
106
  - lib/lopata/observers.rb
107
+ - lib/lopata/observers/backtrace_formatter.rb
105
108
  - lib/lopata/observers/base_observer.rb
106
109
  - lib/lopata/observers/console_output_observer.rb
107
110
  - lib/lopata/observers/web_logger.rb
108
- - lib/lopata/rspec/ar_dsl.rb
109
111
  - lib/lopata/rspec/dsl.rb
110
112
  - lib/lopata/rspec/role.rb
111
113
  - lib/lopata/runner.rb
@@ -137,5 +139,5 @@ requirements: []
137
139
  rubygems_version: 3.0.3
138
140
  signing_key:
139
141
  specification_version: 4
140
- summary: lopata-0.1.1
142
+ summary: lopata-0.1.2
141
143
  test_files: []
@@ -1,38 +0,0 @@
1
- module Lopata
2
- module RSpec
3
- module AR
4
- module DSL
5
- def self.included(base)
6
- base.extend(ClassMethods)
7
- end
8
-
9
- def cleanup(*objects)
10
- return if Lopata::Config.ops[:keep]
11
- objects.flatten.compact.each do |o|
12
- begin
13
- o.reload.destroy!
14
- rescue ActiveRecord::RecordNotFound
15
- # Already destroyed
16
- end
17
- end
18
- end
19
-
20
- def reload(*objects)
21
- objects.flatten.each(&:reload)
22
- end
23
-
24
-
25
- module ClassMethods
26
- def cleanup(*vars, &block)
27
- unless vars.empty?
28
- teardown do
29
- cleanup vars.map { |v| instance_variable_get "@#{v}" }
30
- end
31
- end
32
- teardown &block if block_given?
33
- end
34
- end
35
- end
36
- end
37
- end
38
- end