parallel_report_portal 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.drone.yml +21 -0
- data/.gitignore +18 -0
- data/.rspec +1 -0
- data/Appraisals +15 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +148 -0
- data/LICENSE.txt +22 -0
- data/ParallelReportPortal.drawio +1 -0
- data/README.md +76 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/gemfiles/.bundle/config +2 -0
- data/gemfiles/cucumber_3.2.gemfile +7 -0
- data/gemfiles/cucumber_4.1.gemfile +7 -0
- data/gemfiles/cucumber_5.2.gemfile +7 -0
- data/gemfiles/cucumber_6.0.gemfile +7 -0
- data/lib/parallel_report_portal.rb +41 -0
- data/lib/parallel_report_portal/clock.rb +13 -0
- data/lib/parallel_report_portal/configuration.rb +138 -0
- data/lib/parallel_report_portal/cucumber/formatter.rb +73 -0
- data/lib/parallel_report_portal/cucumber/report.rb +226 -0
- data/lib/parallel_report_portal/file_utils.rb +62 -0
- data/lib/parallel_report_portal/http.rb +228 -0
- data/lib/parallel_report_portal/version.rb +3 -0
- data/parallel_report_portal.gemspec +45 -0
- metadata +241 -0
@@ -0,0 +1,13 @@
|
|
1
|
+
module ParallelReportPortal
|
2
|
+
# This module is responsilbe for the timekeeping for the tests.
|
3
|
+
module Clock
|
4
|
+
# Get the current time.
|
5
|
+
#
|
6
|
+
# This is based on the Unix time stamp and is in milliseconds.
|
7
|
+
#
|
8
|
+
# @return [Integer] the number of milliseconds since the Unix epoc.
|
9
|
+
def clock
|
10
|
+
(Time.now.to_f * 1000).to_i
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,138 @@
|
|
1
|
+
module ParallelReportPortal
|
2
|
+
# The Configuration class holds the connection properties to communicate with
|
3
|
+
# Report Portal and to identify the user and project for reporting.
|
4
|
+
#
|
5
|
+
# It attempts to load a configuration file called +report_portal.yml+ first in a
|
6
|
+
# local directory called +config+ and if that's not found in the current directory.
|
7
|
+
# (Report Portal actually tells you to create a files called +REPORT_PORTAL.YML+ in
|
8
|
+
# uppercase -- for this reason the initializer is case insensitive with regards to
|
9
|
+
# the file name)
|
10
|
+
#
|
11
|
+
# It will then try an apply the following environment variables, if present (these
|
12
|
+
# can be specified in either lowercase for backwards compatibility with the official
|
13
|
+
# gem or in uppercase for reasons of sanity)
|
14
|
+
#
|
15
|
+
# == Environment variables
|
16
|
+
#
|
17
|
+
# RP_UUID:: The UUID of the user associated with this launch
|
18
|
+
# RP_ENDPOINT:: the URL of the Report Portal API endpoint
|
19
|
+
# RP_PROJECT:: the Report Portal project name -- this must already exist within Report Port and this user must be a member of the project
|
20
|
+
# RP_LAUNCH:: The name of this launch
|
21
|
+
# RP_DESCRIPTION:: A textual string describing this launch
|
22
|
+
# RP_TAGS:: A set of tags to pass to Report Portal for this launch. If these are set via an environment variable, provide a comma-separated string of tags
|
23
|
+
# RP_ATTRIBUTES:: A set of attribute tags to pass to Report Portal for this launch. If these are set via an environment variable, provide a comma-separated string of attributes
|
24
|
+
class Configuration
|
25
|
+
ATTRIBUTES = [:uuid, :endpoint, :project, :launch, :debug, :description, :tags, :attributes]
|
26
|
+
|
27
|
+
# @return [String] the Report Portal user UUID
|
28
|
+
attr_accessor :uuid
|
29
|
+
# @return [String] the Report Portal URI - this should include the scheme
|
30
|
+
# e.g. +https://reportportal.local/api/v1+
|
31
|
+
attr_accessor :endpoint
|
32
|
+
# @return [String] the Report Portal project name.
|
33
|
+
# This must exist and match the project name within
|
34
|
+
# Report Portal.
|
35
|
+
attr_accessor :project
|
36
|
+
# @return [String] the launch name for this test run.
|
37
|
+
attr_accessor :launch
|
38
|
+
# @return [Array<String>] an array of tags to attach to this launch.
|
39
|
+
attr_reader :tags
|
40
|
+
# @return [Boolean] true if this is a debug run (this launch will appear
|
41
|
+
# on the debug tab in Report Portal).
|
42
|
+
attr_reader :debug
|
43
|
+
# @return [String] a textual description of this launch.
|
44
|
+
attr_accessor :description
|
45
|
+
# @return [Array<String>] an array of attributes to attach to this launch
|
46
|
+
# (Report Portal 5)
|
47
|
+
attr_reader :attributes
|
48
|
+
|
49
|
+
|
50
|
+
# Create an instance of Configuration.
|
51
|
+
#
|
52
|
+
# The initializer will first attempt to load a configuration files called
|
53
|
+
# +report_portal.yml+ (case insensitive) in the both the +config+ and current
|
54
|
+
# working directory (the former takes precidence). It will then apply
|
55
|
+
# any of the environment variable values.
|
56
|
+
def initialize
|
57
|
+
load_configuration_file
|
58
|
+
ATTRIBUTES.each do |attr|
|
59
|
+
env_value = get_env("rp_#{attr.to_s}")
|
60
|
+
send(:"#{attr}=", env_value) if env_value
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# Sets the tags for the launch. If an array is provided, the array is used,
|
65
|
+
# if a string is provided, the string is broken into components by splitting
|
66
|
+
# on a comma.
|
67
|
+
#
|
68
|
+
# e.g.
|
69
|
+
# configuration.tags="one,two, three"
|
70
|
+
# #=> ["one", "two", "three"]
|
71
|
+
#
|
72
|
+
# param [String | Array<String>] taglist a list of tags to set
|
73
|
+
def tags=(taglist)
|
74
|
+
if taglist.is_a?(String)
|
75
|
+
@tags = taglist.split(',').map(&:strip)
|
76
|
+
elsif taglist.is_a?(Array)
|
77
|
+
@tags = taglist
|
78
|
+
else
|
79
|
+
@tags = []
|
80
|
+
end
|
81
|
+
tags
|
82
|
+
end
|
83
|
+
|
84
|
+
|
85
|
+
# Enables the debug flag which is sent to Report Portal. If this flag is set
|
86
|
+
# Report Portal will include this launch in its 'debug' tab.
|
87
|
+
#
|
88
|
+
# param [Boolean | String] value if the value is a Boolean, it will take that value
|
89
|
+
# if it is a String, it will set values of 'true' to +true+, else all values will be false.
|
90
|
+
def debug=(value)
|
91
|
+
@debug = if [true, false].include?(value)
|
92
|
+
value
|
93
|
+
else
|
94
|
+
value.to_s.downcase.strip == 'true'
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# Sets the attributes for the launch. If an array is provided, the array is used,
|
99
|
+
# if a string is provided, the string is broken into components by splitting
|
100
|
+
# on a comma.
|
101
|
+
#
|
102
|
+
# e.g.
|
103
|
+
# configuration.tags="one,two, three"
|
104
|
+
# #=> ["one", "two", "three"]
|
105
|
+
#
|
106
|
+
# param [String | Array<String>] taglist a list of tags to set
|
107
|
+
def attributes=(attrlist)
|
108
|
+
if attrlist.is_a?(String)
|
109
|
+
@attributes = attrlist.split(',').map(&:strip)
|
110
|
+
elsif attrlist.is_a?(Array)
|
111
|
+
@attributes = attrlist
|
112
|
+
else
|
113
|
+
@attributes = []
|
114
|
+
end
|
115
|
+
attributes
|
116
|
+
end
|
117
|
+
|
118
|
+
private
|
119
|
+
|
120
|
+
def get_env(name)
|
121
|
+
ENV[name.upcase] || ENV[name.downcase]
|
122
|
+
end
|
123
|
+
|
124
|
+
def load_configuration_file
|
125
|
+
files = Dir['./config/*'] + Dir['./*']
|
126
|
+
files
|
127
|
+
.filter { |fn| fn.downcase.end_with?('/report_portal.yml') }
|
128
|
+
.first
|
129
|
+
.then { |fn| fn ? File.read(fn) : '' }
|
130
|
+
.then { |ys| YAML.safe_load(ys, symbolize_names: true) }
|
131
|
+
.then do |yaml|
|
132
|
+
ATTRIBUTES.each do |attr|
|
133
|
+
send(:"#{attr}=", yaml[attr]) if yaml&.fetch(attr, nil)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require_relative 'report'
|
2
|
+
|
3
|
+
module ParallelReportPortal
|
4
|
+
module Cucumber
|
5
|
+
# Formatter supporting the Cucumber formatter API.
|
6
|
+
# This is the class which does the heavy-lifting by
|
7
|
+
# integrating with cucumber.
|
8
|
+
class Formatter
|
9
|
+
|
10
|
+
CucumberMessagesVersion=[4,0,0]
|
11
|
+
|
12
|
+
# Create a new formatter instance
|
13
|
+
#
|
14
|
+
# @param [Cucumber::Configuration] cucumber_config the cucumber configuration environment
|
15
|
+
def initialize(cucumber_config)
|
16
|
+
@ast_lookup = if (::Cucumber::VERSION.split('.').map(&:to_i) <=> CucumberMessagesVersion) > 0
|
17
|
+
require 'cucumber/formatter/ast_lookup'
|
18
|
+
::Cucumber::Formatter::AstLookup.new(cucumber_config)
|
19
|
+
else
|
20
|
+
nil
|
21
|
+
end
|
22
|
+
start_background_thread.priority = Thread.main.priority + 1
|
23
|
+
register_event_handlers(cucumber_config)
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def report
|
29
|
+
@report ||= Report.new(@ast_lookup)
|
30
|
+
end
|
31
|
+
|
32
|
+
def register_event_handlers(config)
|
33
|
+
[:test_case_started,
|
34
|
+
:test_case_finished,
|
35
|
+
:test_step_started,
|
36
|
+
:test_step_finished].each do |event_name|
|
37
|
+
config.on_event(event_name) do |event|
|
38
|
+
background_queue << -> { report.public_send(event_name, event, ParallelReportPortal.clock) }
|
39
|
+
end
|
40
|
+
end
|
41
|
+
config.on_event :test_run_started, &method(:handle_test_run_started )
|
42
|
+
config.on_event :test_run_finished, &method(:handle_test_run_finished)
|
43
|
+
end
|
44
|
+
|
45
|
+
def handle_test_run_started(event)
|
46
|
+
background_queue << proc { report.launch_started(ParallelReportPortal.clock) }
|
47
|
+
end
|
48
|
+
|
49
|
+
def background_queue
|
50
|
+
@background_queue ||= Queue.new
|
51
|
+
end
|
52
|
+
|
53
|
+
def start_background_thread
|
54
|
+
@background_thread ||= Thread.new do
|
55
|
+
loop do
|
56
|
+
code = background_queue.shift
|
57
|
+
code.call
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def handle_test_run_finished(event)
|
63
|
+
background_queue << proc do
|
64
|
+
report.feature_finished(ParallelReportPortal.clock)
|
65
|
+
report.launch_finished(ParallelReportPortal.clock)
|
66
|
+
end
|
67
|
+
sleep 0.01 while !background_queue.empty? || background_queue.num_waiting == 0
|
68
|
+
@background_thread.kill
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,226 @@
|
|
1
|
+
require 'faraday'
|
2
|
+
require 'tree'
|
3
|
+
|
4
|
+
module ParallelReportPortal
|
5
|
+
module Cucumber
|
6
|
+
# Report object. This handles the management of the state heirarchy and
|
7
|
+
# the issuing of the requests to the HTTP module.
|
8
|
+
class Report
|
9
|
+
|
10
|
+
attr_reader :launch_id
|
11
|
+
|
12
|
+
Feature = Struct.new(:feature, :id)
|
13
|
+
|
14
|
+
LOG_LEVELS = {
|
15
|
+
error: 'ERROR',
|
16
|
+
warn: 'WARN',
|
17
|
+
info: 'INFO',
|
18
|
+
debug: 'DEBUG',
|
19
|
+
trace: 'TRACE',
|
20
|
+
fatal: 'FATAL',
|
21
|
+
unknown: 'UNKNOWN'
|
22
|
+
}
|
23
|
+
|
24
|
+
|
25
|
+
# Create a new instance of the report
|
26
|
+
def initialize(ast_lookup = nil)
|
27
|
+
@feature = nil
|
28
|
+
@tree = Tree::TreeNode.new( 'root' )
|
29
|
+
@ast_lookup = ast_lookup
|
30
|
+
end
|
31
|
+
|
32
|
+
# Issued to start a launch. It is possilbe that this method could be called
|
33
|
+
# from multiple processes for the same launch if this is being run with
|
34
|
+
# parallel tests enabled. A temporary launch file will be created (using
|
35
|
+
# exclusive locking). The first time this method is called it will write the
|
36
|
+
# launch id to the launch file, subsequent calls by other processes will read
|
37
|
+
# this launch id and use that.
|
38
|
+
#
|
39
|
+
# @param start_time [Integer] the millis from the epoch
|
40
|
+
# @return [String] the UUID of this launch
|
41
|
+
def launch_started(start_time)
|
42
|
+
ParallelReportPortal.file_open_exlock_and_block(ParallelReportPortal.launch_id_file, 'a+' ) do |file|
|
43
|
+
if file.size == 0
|
44
|
+
@launch_id = ParallelReportPortal.req_launch_started(start_time)
|
45
|
+
file.write(@launch_id)
|
46
|
+
file.flush
|
47
|
+
else
|
48
|
+
@launch_id = file.readline
|
49
|
+
end
|
50
|
+
@launch_id
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Called to finish a launch. Any open children items will be closed in the process.
|
55
|
+
#
|
56
|
+
# @param clock [Integer] the millis from the epoch
|
57
|
+
def launch_finished(clock)
|
58
|
+
@tree.postordered_each do |node|
|
59
|
+
ParallelReportPortal.req_feature_finished(node.content, clock) unless node.is_root?
|
60
|
+
end
|
61
|
+
ParallelReportPortal.req_launch_finished(launch_id, clock)
|
62
|
+
end
|
63
|
+
|
64
|
+
# Called to indicate that a feature has started.
|
65
|
+
#
|
66
|
+
# @param
|
67
|
+
def feature_started(feature, clock)
|
68
|
+
parent_id = hierarchy(feature, clock)
|
69
|
+
feature = feature.feature if using_cucumber_messages?
|
70
|
+
ParallelReportPortal.req_feature_started(launch_id, parent_id, feature, clock)
|
71
|
+
end
|
72
|
+
|
73
|
+
def feature_finished(clock)
|
74
|
+
if @feature
|
75
|
+
resp = ParallelReportPortal.req_feature_finished(@feature.id, clock)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def test_case_started(event, clock)
|
80
|
+
test_case = lookup_test_case(event.test_case)
|
81
|
+
feature = lookup_feature(event.test_case)
|
82
|
+
feature = current_feature(feature, clock)
|
83
|
+
@test_case_id = ParallelReportPortal.req_test_case_started(launch_id, feature.id, test_case, clock)
|
84
|
+
end
|
85
|
+
|
86
|
+
def test_case_finished(event, clock)
|
87
|
+
result = event.result
|
88
|
+
status = result.to_sym
|
89
|
+
failure_message = nil
|
90
|
+
if [:undefined, :pending].include?(status)
|
91
|
+
status = :failed
|
92
|
+
failure_message = result.message
|
93
|
+
end
|
94
|
+
resp = ParallelReportPortal.req_test_case_finished(@test_case_id, status, clock)
|
95
|
+
end
|
96
|
+
|
97
|
+
def test_step_started(event, clock)
|
98
|
+
test_step = event.test_step
|
99
|
+
if !hook?(test_step)
|
100
|
+
step_source = lookup_step_source(test_step)
|
101
|
+
detail = "#{step_source.keyword} #{step_source.text}"
|
102
|
+
if (using_cucumber_messages? ? test_step : step_source).multiline_arg.doc_string?
|
103
|
+
detail << %(\n"""\n#{(using_cucumber_messages? ? test_step : step_source).multiline_arg.content}\n""")
|
104
|
+
elsif (using_cucumber_messages? ? test_step : step_source).multiline_arg.data_table?
|
105
|
+
detail << (using_cucumber_messages? ? test_step : step_source).multiline_arg.raw.reduce("\n") {|acc, row| acc << "| #{row.join(' | ')} |\n"}
|
106
|
+
end
|
107
|
+
|
108
|
+
ParallelReportPortal.req_log(@test_case_id, detail, status_to_level(:trace), clock)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def test_step_finished(event, clock)
|
113
|
+
test_step = event.test_step
|
114
|
+
result = event.result
|
115
|
+
status = result.to_sym
|
116
|
+
if !hook?(test_step)
|
117
|
+
step_source = lookup_step_source(test_step)
|
118
|
+
detail = "#{step_source.text}"
|
119
|
+
|
120
|
+
if [:failed, :pending, :undefined].include?(status)
|
121
|
+
level = :error
|
122
|
+
detail = if [:failed, :pending].include?(status)
|
123
|
+
ex = result.exception
|
124
|
+
sprintf("%s: %s\n %s", ex.class.name, ex.message, ex.backtrace.join("\n "))
|
125
|
+
else
|
126
|
+
sprintf("Undefined step: %s:\n%s", test_step.text, test_step.source.last.backtrace_line)
|
127
|
+
end
|
128
|
+
|
129
|
+
ParallelReportPortal.req_log(@test_case_id, detail, level, clock)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
private
|
135
|
+
|
136
|
+
def using_cucumber_messages?
|
137
|
+
@ast_lookup != nil
|
138
|
+
end
|
139
|
+
|
140
|
+
def hierarchy(feature, clock)
|
141
|
+
node = nil
|
142
|
+
path_components = if using_cucumber_messages?
|
143
|
+
feature.uri.split(File::SEPARATOR)
|
144
|
+
else
|
145
|
+
feature.location.file.split(File::SEPARATOR)
|
146
|
+
end
|
147
|
+
ParallelReportPortal.file_open_exlock_and_block(ParallelReportPortal.hierarchy_file, 'a+b' ) do |file|
|
148
|
+
@tree = Marshal.load(File.read(file)) if file.size > 0
|
149
|
+
node = @tree.root
|
150
|
+
path_components[0..-2].each do |component|
|
151
|
+
next_node = node[component]
|
152
|
+
unless next_node
|
153
|
+
id = ParallelReportPortal.req_hierarchy(launch_id, "Folder: #{component}", node.content, 'SUITE', [], nil, clock )
|
154
|
+
next_node = Tree::TreeNode.new(component, id)
|
155
|
+
node << next_node
|
156
|
+
node = next_node
|
157
|
+
else
|
158
|
+
node = next_node
|
159
|
+
end
|
160
|
+
end
|
161
|
+
file.truncate(0)
|
162
|
+
file.write(Marshal.dump(@tree))
|
163
|
+
file.flush
|
164
|
+
end
|
165
|
+
|
166
|
+
node.content
|
167
|
+
end
|
168
|
+
|
169
|
+
def lookup_feature(test_case)
|
170
|
+
if using_cucumber_messages?
|
171
|
+
@ast_lookup.gherkin_document(test_case.location.file)
|
172
|
+
else
|
173
|
+
test_case.feature
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
def lookup_test_case(test_case)
|
178
|
+
if using_cucumber_messages?
|
179
|
+
@ast_lookup.gherkin_document(test_case.location.file).feature
|
180
|
+
else
|
181
|
+
test_case
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
def lookup_step_source(step)
|
186
|
+
if using_cucumber_messages?
|
187
|
+
@ast_lookup.step_source(step).step
|
188
|
+
else
|
189
|
+
step.source.last
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
def current_feature(feature, clock)
|
194
|
+
if @feature&.feature == feature
|
195
|
+
@feature
|
196
|
+
else
|
197
|
+
feature_finished(clock)
|
198
|
+
@feature = Feature.new(feature, feature_started(feature, clock))
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
def hook?(test_step)
|
203
|
+
if using_cucumber_messages?
|
204
|
+
test_step.hook?
|
205
|
+
else
|
206
|
+
! test_step.source.last.respond_to?(:keyword)
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
def status_to_level(status)
|
211
|
+
case status
|
212
|
+
when :passed
|
213
|
+
LOG_LEVELS[:info]
|
214
|
+
when :failed, :undefined, :pending, :error
|
215
|
+
LOG_LEVELS[:error]
|
216
|
+
when :skipped
|
217
|
+
LOG_LEVELS[:warn]
|
218
|
+
else
|
219
|
+
LOG_LEVELS.fetch(status, LOG_LEVELS[:info])
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end
|