lopata 0.1.1 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +6 -2
- data/exe/lopata +1 -1
- data/lib/lopata.rb +54 -1
- data/lib/lopata/active_record.rb +99 -0
- data/lib/lopata/condition.rb +2 -3
- data/lib/lopata/configuration.rb +126 -0
- data/lib/lopata/environment.rb +36 -0
- data/lib/lopata/factory_bot.rb +36 -0
- data/lib/lopata/generators/app.rb +0 -2
- data/lib/lopata/generators/templates/Gemfile +0 -2
- data/lib/lopata/generators/templates/config/environments/qa.yml +0 -1
- data/lib/lopata/observers/backtrace_formatter.rb +103 -0
- data/lib/lopata/observers/base_observer.rb +17 -6
- data/lib/lopata/observers/console_output_observer.rb +63 -26
- data/lib/lopata/observers/web_logger.rb +57 -59
- data/lib/lopata/role.rb +46 -0
- data/lib/lopata/runner.rb +18 -17
- data/lib/lopata/scenario.rb +94 -34
- data/lib/lopata/scenario_builder.rb +62 -73
- data/lib/lopata/shared_step.rb +10 -4
- data/lib/lopata/step.rb +143 -44
- data/lib/lopata/version.rb +1 -1
- data/lib/lopata/world.rb +3 -1
- metadata +10 -10
- data/lib/lopata/config.rb +0 -101
- data/lib/lopata/generators/templates/.rspec +0 -3
- data/lib/lopata/generators/templates/spec/spec_helper.rb +0 -2
- data/lib/lopata/rspec/ar_dsl.rb +0 -38
- data/lib/lopata/rspec/dsl.rb +0 -39
- data/lib/lopata/rspec/role.rb +0 -75
@@ -0,0 +1,103 @@
|
|
1
|
+
module Lopata
|
2
|
+
module Observers
|
3
|
+
# @private
|
4
|
+
# Based on RSpec::Core::BacktraceFormatter
|
5
|
+
#
|
6
|
+
# Provides ability to format backtrace and find source code by file and line number.
|
7
|
+
class BacktraceFormatter
|
8
|
+
# @private
|
9
|
+
attr_accessor :exclusion_patterns, :inclusion_patterns
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
patterns = %w[ /lib\d*/ruby/ bin/ exe/lopata /lib/bundler/ /exe/bundle /\.rvm/ ]
|
13
|
+
patterns.map! { |s| Regexp.new(s.gsub("/", File::SEPARATOR)) }
|
14
|
+
|
15
|
+
@exclusion_patterns = [Regexp.union(*patterns)]
|
16
|
+
@inclusion_patterns = []
|
17
|
+
|
18
|
+
inclusion_patterns << Regexp.new(Dir.getwd)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Filter backtrace.
|
22
|
+
#
|
23
|
+
# @param backtrace [Array<String>] exception backtrace
|
24
|
+
# @return [Array<String>] backtrace lines except ruby libraries, gems and executable files.
|
25
|
+
def format(backtrace)
|
26
|
+
return [] unless backtrace
|
27
|
+
return backtrace if backtrace.empty?
|
28
|
+
|
29
|
+
backtrace.map { |l| backtrace_line(l) }.compact.
|
30
|
+
tap do |filtered|
|
31
|
+
if filtered.empty?
|
32
|
+
filtered.concat backtrace
|
33
|
+
filtered << ""
|
34
|
+
filtered << " Showing full backtrace because every line was filtered out."
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
|
40
|
+
# Extracts error message from excetion
|
41
|
+
#
|
42
|
+
# @param exeption [Exception]
|
43
|
+
# @param include_backtrace [Boolean] whether to add formatted backtrace to output
|
44
|
+
# @return [String] error message from excetion, incuding source code line.
|
45
|
+
def error_message(exception, include_backtrace: false)
|
46
|
+
backtrace = format(exception.backtrace)
|
47
|
+
source_line = extract_source_line(backtrace.first)
|
48
|
+
msg = ''
|
49
|
+
msg << "\n#{source_line}\n" if source_line
|
50
|
+
msg << "#{exception.class.name}: " unless exception.class.name =~ /RSpec/
|
51
|
+
msg << exception.message if exception.message
|
52
|
+
msg << "\n#{backtrace.join("\n")}\n" if include_backtrace
|
53
|
+
msg
|
54
|
+
end
|
55
|
+
|
56
|
+
def extract_source_line(backtrace_line)
|
57
|
+
file_and_line_number = backtrace_line.match(/(.+?):(\d+)(|:\d+)/)
|
58
|
+
return nil unless file_and_line_number
|
59
|
+
file_path, line_number = file_and_line_number[1..2]
|
60
|
+
return nil unless File.exist?(file_path)
|
61
|
+
lines = File.read(file_path).split("\n")
|
62
|
+
lines[line_number.to_i - 1]
|
63
|
+
end
|
64
|
+
|
65
|
+
def backtrace_line(line)
|
66
|
+
relative_path(line) unless exclude?(line)
|
67
|
+
end
|
68
|
+
|
69
|
+
def exclude?(line)
|
70
|
+
matches?(exclusion_patterns, line) && !matches?(inclusion_patterns, line)
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
def matches?(patterns, line)
|
76
|
+
patterns.any? { |p| line =~ p }
|
77
|
+
end
|
78
|
+
|
79
|
+
# Matches strings either at the beginning of the input or prefixed with a
|
80
|
+
# whitespace, containing the current path, either postfixed with the
|
81
|
+
# separator, or at the end of the string. Match groups are the character
|
82
|
+
# before and the character after the string if any.
|
83
|
+
#
|
84
|
+
# http://rubular.com/r/fT0gmX6VJX
|
85
|
+
# http://rubular.com/r/duOrD4i3wb
|
86
|
+
# http://rubular.com/r/sbAMHFrOx1
|
87
|
+
def relative_path_regex
|
88
|
+
@relative_path_regex ||= /(\A|\s)#{File.expand_path('.')}(#{File::SEPARATOR}|\s|\Z)/
|
89
|
+
end
|
90
|
+
|
91
|
+
# @param line [String] current code line
|
92
|
+
# @return [String] relative path to line
|
93
|
+
def relative_path(line)
|
94
|
+
line = line.sub(relative_path_regex, "\\1.\\2".freeze)
|
95
|
+
line = line.sub(/\A([^:]+:\d+)$/, '\\1'.freeze)
|
96
|
+
return nil if line == '-e:1'.freeze
|
97
|
+
line
|
98
|
+
rescue SecurityError
|
99
|
+
nil
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -1,23 +1,34 @@
|
|
1
1
|
module Lopata
|
2
2
|
module Observers
|
3
|
+
# Lopata allows observe scenarios execution.
|
4
|
+
# All the observers are subclasses of Lopata::Observers::BaseObserver.
|
5
|
+
#
|
6
|
+
# @see Lopata::Observers::ConsoleOutputObserver for implementation example
|
3
7
|
class BaseObserver
|
8
|
+
# Called before scenarios execution.
|
9
|
+
# All the scenarios are prepared at the moment, so it may be used to get number of scenarios
|
10
|
+
# via world.scenarios.count
|
11
|
+
#
|
12
|
+
# @param world [Lopata::World]
|
4
13
|
def started(world)
|
5
14
|
end
|
6
15
|
|
16
|
+
# Called after all scenarios execution.
|
17
|
+
# All the scenarios are finished at the moment, so it may be used for output statistics.
|
18
|
+
#
|
19
|
+
# @param world [Lopata::World]
|
7
20
|
def finished(world)
|
8
21
|
end
|
9
22
|
|
23
|
+
# Called before single scenario execution.
|
24
|
+
# @param world [Lopata::Scenario::Execution]
|
10
25
|
def scenario_started(scenario)
|
11
26
|
end
|
12
27
|
|
28
|
+
# Called after single scenario execution.
|
29
|
+
# @param world [Lopata::Scenario::Execution]
|
13
30
|
def scenario_finished(scenario)
|
14
31
|
end
|
15
|
-
|
16
|
-
def step_started(step)
|
17
|
-
end
|
18
|
-
|
19
|
-
def step_finished(step)
|
20
|
-
end
|
21
32
|
end
|
22
33
|
end
|
23
34
|
end
|
@@ -1,35 +1,45 @@
|
|
1
|
+
require_relative 'backtrace_formatter'
|
2
|
+
|
1
3
|
module Lopata
|
2
4
|
module Observers
|
5
|
+
# @private
|
3
6
|
class ConsoleOutputObserver < BaseObserver
|
7
|
+
extend Forwardable
|
8
|
+
# @private
|
9
|
+
attr_reader :output
|
10
|
+
# @private
|
11
|
+
def_delegators :output, :puts, :flush
|
12
|
+
|
13
|
+
def initialize
|
14
|
+
@output = $stdout
|
15
|
+
end
|
16
|
+
|
17
|
+
# @see Lopata::Observers::BaseObserver#finished
|
4
18
|
def finished(world)
|
5
|
-
total =
|
6
|
-
|
7
|
-
|
8
|
-
colored("%d %s", status) % [statuses.count { |s| s == status }, status]
|
19
|
+
total = statuses.values.inject(0, &:+)
|
20
|
+
counts = statuses.map do |status, count|
|
21
|
+
colored("%d %s", status) % [count, status]
|
9
22
|
end
|
10
23
|
details = counts.empty? ? "" : "(%s)" % counts.join(', ')
|
11
24
|
puts "#{total} scenario%s %s" % [total == 1 ? '' : 's', details]
|
12
25
|
end
|
13
26
|
|
14
|
-
|
15
|
-
@failed_steps << step if step.failed?
|
16
|
-
end
|
17
|
-
|
18
|
-
def scenario_started(scenario)
|
19
|
-
@failed_steps = []
|
20
|
-
end
|
21
|
-
|
27
|
+
# @see Lopata::Observers::BaseObserver#scenario_finished
|
22
28
|
def scenario_finished(scenario)
|
23
29
|
message = "#{scenario.title} #{bold(scenario.status.to_s.upcase)}"
|
24
30
|
puts colored(message, scenario.status)
|
25
31
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
32
|
+
statuses[scenario.status] ||= 0
|
33
|
+
statuses[scenario.status] += 1
|
34
|
+
|
35
|
+
if scenario.failed?
|
36
|
+
scenario.steps.each do |step|
|
37
|
+
puts colored(" #{status_marker(step.status)} #{step.title}", step.status)
|
38
|
+
puts indent(4, backtrace_formatter.error_message(step.exception, include_backtrace: true)) if step.failed?
|
31
39
|
end
|
32
40
|
end
|
41
|
+
|
42
|
+
flush
|
33
43
|
end
|
34
44
|
|
35
45
|
private
|
@@ -38,25 +48,52 @@ module Lopata
|
|
38
48
|
case status
|
39
49
|
when :failed then red(text)
|
40
50
|
when :passed then green(text)
|
51
|
+
when :skipped then cyan(text)
|
52
|
+
when :pending then yellow(text)
|
41
53
|
else text
|
42
54
|
end
|
43
55
|
end
|
44
56
|
|
45
|
-
|
46
|
-
|
57
|
+
{
|
58
|
+
red: 31,
|
59
|
+
green: 32,
|
60
|
+
cyan: 36,
|
61
|
+
yellow: 33,
|
62
|
+
bold: 1,
|
63
|
+
}.each do |color, code|
|
64
|
+
define_method(color) do |text|
|
65
|
+
wrap(text, code)
|
66
|
+
end
|
47
67
|
end
|
48
68
|
|
49
|
-
def
|
50
|
-
|
69
|
+
def wrap(text, code)
|
70
|
+
"\e[#{code}m#{text}\e[0m"
|
51
71
|
end
|
52
72
|
|
53
|
-
def
|
54
|
-
|
73
|
+
def backtrace_formatter
|
74
|
+
@backtrace_formatter ||= Lopata::Observers::BacktraceFormatter.new
|
55
75
|
end
|
56
76
|
|
57
|
-
def
|
58
|
-
|
77
|
+
def status_marker(status)
|
78
|
+
case status
|
79
|
+
when :failed then "[!]"
|
80
|
+
when :skipped then "[-]"
|
81
|
+
when :pending then "[?]"
|
82
|
+
else "[+]"
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# Adds indent to text
|
87
|
+
# @param cols [Number] number of spaces to be added
|
88
|
+
# @param text [String] text to add indent
|
89
|
+
# @return [String] text with indent
|
90
|
+
def indent(cols, text)
|
91
|
+
text.split("\n").map { |line| " " * cols + line }.join("\n")
|
92
|
+
end
|
93
|
+
|
94
|
+
def statuses
|
95
|
+
@statuses ||= {}
|
59
96
|
end
|
60
97
|
end
|
61
98
|
end
|
62
|
-
end
|
99
|
+
end
|
@@ -1,49 +1,20 @@
|
|
1
1
|
require 'httparty'
|
2
2
|
require 'json'
|
3
|
+
require_relative 'backtrace_formatter'
|
3
4
|
|
4
5
|
module Lopata
|
5
6
|
module Observers
|
7
|
+
# @private
|
6
8
|
class WebLogger < BaseObserver
|
7
9
|
def started(world)
|
8
|
-
|
9
|
-
@client = Lopata::Client.new(Lopata::Config.build_number)
|
10
|
+
@client = Lopata::Client.new
|
10
11
|
@client.start(world.scenarios.count)
|
12
|
+
@finished = 0
|
11
13
|
end
|
12
14
|
|
13
15
|
def scenario_finished(scenario)
|
14
|
-
|
15
|
-
|
16
|
-
@client.add_attempt(scenario, Lopata::FAILED, error_message_for(scenario), backtrace)
|
17
|
-
else
|
18
|
-
@client.add_attempt(scenario, Lopata::PASSED)
|
19
|
-
end
|
20
|
-
end
|
21
|
-
|
22
|
-
# def example_pending(notification)
|
23
|
-
# example = notification.example
|
24
|
-
# @client.add_attempt(example, Lopata::PENDING, example.execution_result.pending_message)
|
25
|
-
# 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
|
16
|
+
@finished += 1
|
17
|
+
@client.add_attempt(scenario, @finished)
|
47
18
|
end
|
48
19
|
end
|
49
20
|
end
|
@@ -51,28 +22,41 @@ module Lopata
|
|
51
22
|
PASSED = 0
|
52
23
|
FAILED = 1
|
53
24
|
PENDING = 2
|
25
|
+
SKIPPED = 5
|
54
26
|
|
27
|
+
# @private
|
55
28
|
class Client
|
56
29
|
include HTTParty
|
57
|
-
base_uri Lopata::Config.lopata_host
|
58
30
|
|
59
|
-
|
31
|
+
attr_reader :url, :project_code, :build_number
|
60
32
|
|
61
|
-
def initialize
|
62
|
-
|
33
|
+
def initialize
|
34
|
+
params = Lopata.configuration.web_logging_params
|
35
|
+
raise "Web logging is not initailzed" unless params
|
36
|
+
@url = HTTParty.normalize_base_uri(params[:url])
|
37
|
+
@project_code = params[:project_code]
|
38
|
+
@build_number = params[:build_number]
|
63
39
|
end
|
64
40
|
|
65
41
|
def start(count)
|
66
42
|
@launch_id = JSON.parse(post("/projects/#{project_code}/builds/#{build_number}/launches.json", body: {total: count}).body)['id']
|
67
43
|
end
|
68
44
|
|
69
|
-
def add_attempt(scenario,
|
45
|
+
def add_attempt(scenario, finished)
|
46
|
+
status = scenario.failed? ? Lopata::FAILED : Lopata::PASSED
|
47
|
+
steps = scenario.steps.map { |s| step_hash(s) }
|
48
|
+
request = { status: status, steps: steps, launch: { id: @launch_id, finished: finished } }
|
70
49
|
test = test_id(scenario)
|
71
|
-
request = { status: status}
|
72
|
-
request[:message] = msg if msg
|
73
|
-
request[:backtrace] = backtrace if backtrace
|
74
50
|
post("/tests/#{test}/attempts.json", body: request)
|
75
|
-
|
51
|
+
end
|
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
|
76
60
|
end
|
77
61
|
|
78
62
|
def test_id(scenario)
|
@@ -98,32 +82,46 @@ module Lopata
|
|
98
82
|
|
99
83
|
private
|
100
84
|
|
101
|
-
def get_json(
|
102
|
-
JSON.parse(self.class.get(url).body)
|
85
|
+
def get_json(path)
|
86
|
+
JSON.parse(self.class.get(path, base_uri: url).body)
|
103
87
|
end
|
104
88
|
|
105
89
|
def post(*args)
|
106
|
-
self.class.post(*args)
|
90
|
+
self.class.post(*with_base_uri(args))
|
107
91
|
end
|
108
92
|
|
109
93
|
def patch(*args)
|
110
|
-
self.class.patch(*args)
|
94
|
+
self.class.patch(*with_base_uri(args))
|
95
|
+
end
|
96
|
+
|
97
|
+
def with_base_uri(args = [])
|
98
|
+
if args.last.is_a? Hash
|
99
|
+
args.last[:base_uri] = url
|
100
|
+
else
|
101
|
+
args << { base_uri: url }
|
102
|
+
end
|
103
|
+
args
|
104
|
+
end
|
105
|
+
|
106
|
+
def error_message_for(step)
|
107
|
+
if step.exception
|
108
|
+
backtrace_formatter.error_message(step.exception)
|
109
|
+
else
|
110
|
+
'Empty error message'
|
111
|
+
end
|
111
112
|
end
|
112
113
|
|
113
|
-
def
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
headers: { 'Content-Type' => 'application/json', 'Accept' => 'application/json' })
|
119
|
-
if response.code == 404
|
120
|
-
puts 'Launch has been cancelled. Exit.'
|
121
|
-
exit!
|
114
|
+
def backtrace_for(step)
|
115
|
+
msg = ''
|
116
|
+
if step.exception
|
117
|
+
msg = backtrace_formatter.format(step.exception.backtrace).join("\n")
|
118
|
+
msg << "\n"
|
122
119
|
end
|
120
|
+
msg
|
123
121
|
end
|
124
122
|
|
125
|
-
def
|
126
|
-
Lopata::
|
123
|
+
def backtrace_formatter
|
124
|
+
@backtrace_formatter ||= Lopata::Observers::BacktraceFormatter.new
|
127
125
|
end
|
128
126
|
end
|
129
127
|
end
|