lopata 0.1.1 → 0.1.2

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: 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