rpruby 1.2 → 1.2.1
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/.DS_Store +0 -0
- data/.github/workflows/main.yml +16 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.rubocop.yml +13 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/Gemfile +21 -0
- data/LICENSE.txt +21 -0
- data/README.md +43 -0
- data/Rakefile +16 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/config/report_portal.yaml.example +12 -0
- data/lib/rpruby/cucumber/formatter.rb +87 -0
- data/lib/rpruby/cucumber/messagereport.rb +224 -0
- data/lib/rpruby/cucumber/parallel_formatter.rb +14 -0
- data/lib/rpruby/cucumber/parallel_report.rb +54 -0
- data/lib/rpruby/cucumber/report.rb +220 -0
- data/lib/rpruby/event_bus.rb +30 -0
- data/lib/rpruby/events/prepare_start_item_request.rb +13 -0
- data/lib/rpruby/http_client.rb +64 -0
- data/lib/rpruby/logging/log4r_outputter.rb +16 -0
- data/lib/rpruby/logging/logger.rb +36 -0
- data/lib/rpruby/logging/logging_appender.rb +18 -0
- data/lib/rpruby/models/item_search_options.rb +26 -0
- data/lib/rpruby/models/test_item.rb +22 -0
- data/lib/rpruby/rspec/formatter.rb +112 -0
- data/lib/rpruby/settings.rb +65 -0
- data/lib/rpruby/tasks.rb +27 -0
- data/lib/rpruby/version.rb +5 -0
- data/lib/rpruby.rb +223 -0
- data/rpruby.gemspec +44 -0
- metadata +38 -3
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'parallel_tests'
|
2
|
+
|
3
|
+
require_relative 'report'
|
4
|
+
|
5
|
+
module Rpruby
|
6
|
+
module Cucumber
|
7
|
+
class ParallelReport < Report
|
8
|
+
FILE_WITH_LAUNCH_ID = Pathname(Dir.tmpdir) + "parallel_launch_id_for_#{Process.ppid}.lck"
|
9
|
+
|
10
|
+
def parallel?
|
11
|
+
true
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize
|
15
|
+
@root_node = Tree::TreeNode.new('')
|
16
|
+
@parent_item_node = @root_node
|
17
|
+
@last_used_time ||= 0
|
18
|
+
|
19
|
+
if ParallelTests.first_process?
|
20
|
+
File.open(FILE_WITH_LAUNCH_ID, 'w') do |f|
|
21
|
+
f.flock(File::LOCK_EX)
|
22
|
+
start_launch
|
23
|
+
f.write(Rpruby.launch_id)
|
24
|
+
f.flush
|
25
|
+
f.flock(File::LOCK_UN)
|
26
|
+
end
|
27
|
+
else
|
28
|
+
File.open(FILE_WITH_LAUNCH_ID, 'r') do |f|
|
29
|
+
f.flock(File::LOCK_SH)
|
30
|
+
ReportPortal.launch_id = f.read
|
31
|
+
f.flock(File::LOCK_UN)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def test_run_finished(_event, desired_time = Rpruby.now)
|
37
|
+
end_feature(desired_time) unless @parent_item_node.is_root?
|
38
|
+
|
39
|
+
if ParallelTests.first_process?
|
40
|
+
ParallelTests.wait_for_other_processes_to_finish
|
41
|
+
|
42
|
+
File.delete(FILE_WITH_LAUNCH_ID)
|
43
|
+
|
44
|
+
unless attach_to_launch?
|
45
|
+
$stdout.puts "Finishing launch #{Rpruby.launch_id}"
|
46
|
+
Rpruby.close_child_items(nil)
|
47
|
+
time_to_send = time_to_send(desired_time)
|
48
|
+
Rpruby.finish_launch(time_to_send)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,220 @@
|
|
1
|
+
require 'cucumber/formatter/io'
|
2
|
+
require 'cucumber/formatter/hook_query_visitor'
|
3
|
+
require 'tree'
|
4
|
+
require 'securerandom'
|
5
|
+
|
6
|
+
require_relative '../../rpruby'
|
7
|
+
require_relative '../logging/logger'
|
8
|
+
|
9
|
+
module Rpruby
|
10
|
+
module Cucumber
|
11
|
+
# @api private
|
12
|
+
class Report
|
13
|
+
def parallel?
|
14
|
+
false
|
15
|
+
end
|
16
|
+
|
17
|
+
def attach_to_launch?
|
18
|
+
Rpruby::Settings.instance.formatter_modes.include?('attach_to_launch')
|
19
|
+
end
|
20
|
+
|
21
|
+
def initialize
|
22
|
+
@last_used_time = 0
|
23
|
+
@root_node = Tree::TreeNode.new('')
|
24
|
+
@parent_item_node = @root_node
|
25
|
+
start_launch
|
26
|
+
end
|
27
|
+
|
28
|
+
def start_launch(desired_time = Rpruby.now)
|
29
|
+
if attach_to_launch?
|
30
|
+
Rpruby.launch_id =
|
31
|
+
if Rpruby::Settings.instance.launch_id
|
32
|
+
Rpruby::Settings.instance.launch_id
|
33
|
+
else
|
34
|
+
file_path = Rpruby::Settings.instance.file_with_launch_id || (Pathname(Dir.tmpdir) + 'rp_launch_id.tmp')
|
35
|
+
File.read(file_path)
|
36
|
+
end
|
37
|
+
$stdout.puts "Attaching to launch #{Rpruby.launch_id}"
|
38
|
+
else
|
39
|
+
description = Rpruby::Settings.instance.description
|
40
|
+
description ||= ARGV.map { |arg| arg.gsub(/rp_uuid=.+/, 'rp_uuid=[FILTERED]') }.join(' ')
|
41
|
+
Rpruby.start_launch(description, time_to_send(desired_time))
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# TODO: time should be a required argument
|
46
|
+
def test_case_started(event, desired_time = Rpruby.now)
|
47
|
+
test_case = event.test_case
|
48
|
+
feature = test_case.feature
|
49
|
+
if report_hierarchy? && !same_feature_as_previous_test_case?(feature)
|
50
|
+
end_feature(desired_time) unless @parent_item_node.is_root?
|
51
|
+
start_feature_with_parentage(feature, desired_time)
|
52
|
+
end
|
53
|
+
|
54
|
+
name = "#{test_case.keyword}: #{test_case.name}"
|
55
|
+
description = test_case.location.to_s
|
56
|
+
tags = test_case.tags.map(&:name)
|
57
|
+
type = :STEP
|
58
|
+
|
59
|
+
Rpruby.current_scenario = Rpruby::TestItem.new(name: name, type: type, id: nil, start_time: time_to_send(desired_time), description: description, closed: false, tags: tags)
|
60
|
+
scenario_node = Tree::TreeNode.new(SecureRandom.hex, Rpruby.current_scenario)
|
61
|
+
@parent_item_node << scenario_node
|
62
|
+
Rpruby.current_scenario.id = Rpruby.start_item(scenario_node)
|
63
|
+
end
|
64
|
+
|
65
|
+
def test_case_finished(event, desired_time = Rpruby.now)
|
66
|
+
result = event.result
|
67
|
+
status = result.to_sym
|
68
|
+
issue = nil
|
69
|
+
if %i[undefined pending].include?(status)
|
70
|
+
status = :failed
|
71
|
+
issue = result.message
|
72
|
+
end
|
73
|
+
Rpruby.finish_item(Rpruby.current_scenario, status, time_to_send(desired_time), issue)
|
74
|
+
Rpruby.current_scenario = nil
|
75
|
+
end
|
76
|
+
|
77
|
+
def test_step_started(event, desired_time = Rpruby.now)
|
78
|
+
test_step = event.test_step
|
79
|
+
if step?(test_step) # `after_test_step` is also invoked for hooks
|
80
|
+
step_source = test_step.source.last
|
81
|
+
message = "-- #{step_source.keyword}#{step_source.text} --"
|
82
|
+
if step_source.multiline_arg.doc_string?
|
83
|
+
message << %(\n"""\n#{step_source.multiline_arg.content}\n""")
|
84
|
+
elsif step_source.multiline_arg.data_table?
|
85
|
+
message << step_source.multiline_arg.raw.reduce("\n") { |acc, row| acc << "| #{row.join(' | ')} |\n" }
|
86
|
+
end
|
87
|
+
Rpruby.send_log(:trace, message, time_to_send(desired_time))
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def test_step_finished(event, desired_time = Rpruby.now)
|
92
|
+
test_step = event.test_step
|
93
|
+
result = event.result
|
94
|
+
status = result.to_sym
|
95
|
+
|
96
|
+
if %i[failed pending undefined].include?(status)
|
97
|
+
exception_info = if %i[failed pending].include?(status)
|
98
|
+
ex = result.exception
|
99
|
+
format("%s: %s\n %s", ex.class.name, ex.message, ex.backtrace.join("\n "))
|
100
|
+
else
|
101
|
+
format("Undefined step: %s:\n%s", test_step.text, test_step.source.last.backtrace_line)
|
102
|
+
end
|
103
|
+
Rpruby.send_log(:error, exception_info, time_to_send(desired_time))
|
104
|
+
end
|
105
|
+
|
106
|
+
if status != :passed
|
107
|
+
log_level = status == :skipped ? :warn : :error
|
108
|
+
step_type = if step?(test_step)
|
109
|
+
'Step'
|
110
|
+
else
|
111
|
+
hook_class_name = test_step.source.last.class.name.split('::').last
|
112
|
+
location = test_step.location
|
113
|
+
"#{hook_class_name} at `#{location}`"
|
114
|
+
end
|
115
|
+
Rpruby.send_log(log_level, "#{step_type} #{status}", time_to_send(desired_time))
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def test_run_finished(_event, desired_time = Rpruby.now)
|
120
|
+
end_feature(desired_time) unless @parent_item_node.is_root?
|
121
|
+
|
122
|
+
unless attach_to_launch?
|
123
|
+
close_all_children_of(@root_node) # Folder items are closed here as they can't be closed after finishing a feature
|
124
|
+
time_to_send = time_to_send(desired_time)
|
125
|
+
Rpruby.finish_launch(time_to_send)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def puts(message, desired_time = Rpruby.now)
|
130
|
+
Rpruby.send_log(:info, message, time_to_send(desired_time))
|
131
|
+
end
|
132
|
+
|
133
|
+
def embed(path_or_src, mime_type, label, desired_time = Rpruby.now)
|
134
|
+
Rpruby.send_file(:info, path_or_src, label, time_to_send(desired_time), mime_type)
|
135
|
+
end
|
136
|
+
|
137
|
+
private
|
138
|
+
|
139
|
+
# Report Portal sorts logs by time. However, several logs might have the same time.
|
140
|
+
# So to get Report Portal sort them properly the time should be different in all logs related to the same item.
|
141
|
+
# And thus it should be stored.
|
142
|
+
# Only the last time needs to be stored as:
|
143
|
+
# * only one test framework process/thread may send data for a single Report Portal item
|
144
|
+
# * that process/thread can't start the next test until it's done with the previous one
|
145
|
+
def time_to_send(desired_time)
|
146
|
+
time_to_send = desired_time
|
147
|
+
if time_to_send <= @last_used_time
|
148
|
+
time_to_send = @last_used_time + 1
|
149
|
+
end
|
150
|
+
@last_used_time = time_to_send
|
151
|
+
end
|
152
|
+
|
153
|
+
def same_feature_as_previous_test_case?(feature)
|
154
|
+
@parent_item_node.name == feature.location.file.split(File::SEPARATOR).last
|
155
|
+
end
|
156
|
+
|
157
|
+
def start_feature_with_parentage(feature, desired_time)
|
158
|
+
parent_node = @root_node
|
159
|
+
child_node = nil
|
160
|
+
path_components = feature.location.file.split(File::SEPARATOR)
|
161
|
+
path_components.each_with_index do |path_component, index|
|
162
|
+
child_node = parent_node[path_component]
|
163
|
+
unless child_node # if child node was not created yet
|
164
|
+
if index < path_components.size - 1
|
165
|
+
name = "Folder: #{path_component}"
|
166
|
+
description = nil
|
167
|
+
tags = []
|
168
|
+
type = :SUITE
|
169
|
+
else
|
170
|
+
name = "#{feature.keyword}: #{feature.name}"
|
171
|
+
description = feature.file # TODO: consider adding feature description and comments
|
172
|
+
tags = feature.tags.map(&:name)
|
173
|
+
type = :TEST
|
174
|
+
end
|
175
|
+
# TODO: multithreading # Parallel formatter always executes scenarios inside the same feature in the same process
|
176
|
+
if parallel? &&
|
177
|
+
index < path_components.size - 1 && # is folder?
|
178
|
+
(id_of_created_item = Rpruby.item_id_of(name, parent_node)) # get id for folder from report portal
|
179
|
+
# get child id from other process
|
180
|
+
item = Rpruby::TestItem.new(name: name, type: type, id: id_of_created_item, start_time: time_to_send(desired_time), description: description, closed: false, tags: tags)
|
181
|
+
child_node = Tree::TreeNode.new(path_component, item)
|
182
|
+
parent_node << child_node
|
183
|
+
else
|
184
|
+
item = Rpruby::TestItem.new(name: name, type: type, id: nil, start_time: time_to_send(desired_time), description: description, closed: false, tags: tags)
|
185
|
+
child_node = Tree::TreeNode.new(path_component, item)
|
186
|
+
parent_node << child_node
|
187
|
+
item.id = Rpruby.start_item(child_node) # TODO: multithreading
|
188
|
+
end
|
189
|
+
end
|
190
|
+
parent_node = child_node
|
191
|
+
end
|
192
|
+
@parent_item_node = child_node
|
193
|
+
end
|
194
|
+
|
195
|
+
def end_feature(desired_time)
|
196
|
+
Rpruby.finish_item(@parent_item_node.content, nil, time_to_send(desired_time))
|
197
|
+
# Folder items can't be finished here because when the folder started we didn't track
|
198
|
+
# which features the folder contains.
|
199
|
+
# It's not easy to do it using Cucumber currently:
|
200
|
+
# https://github.com/cucumber/cucumber-ruby/issues/887
|
201
|
+
end
|
202
|
+
|
203
|
+
def close_all_children_of(root_node)
|
204
|
+
root_node.postordered_each do |node|
|
205
|
+
if !node.is_root? && !node.content.closed
|
206
|
+
Rpruby.finish_item(node.content)
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
def step?(test_step)
|
212
|
+
!::Cucumber::Formatter::HookQueryVisitor.new(test_step).hook?
|
213
|
+
end
|
214
|
+
|
215
|
+
def report_hierarchy?
|
216
|
+
!Rpruby::Settings.instance.formatter_modes.include?('skip_reporting_hierarchy')
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require_relative 'events/prepare_start_item_request'
|
2
|
+
|
3
|
+
module Rpruby
|
4
|
+
# @api private
|
5
|
+
class EventBus
|
6
|
+
attr_reader :event_types
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@event_types = {
|
10
|
+
prepare_start_item_request: Events::PrepareStartItemRequest
|
11
|
+
}
|
12
|
+
@handlers = {}
|
13
|
+
end
|
14
|
+
|
15
|
+
def on(event_name, &proc)
|
16
|
+
handlers_for(event_name) << proc
|
17
|
+
end
|
18
|
+
|
19
|
+
def broadcast(event_name, **attributes)
|
20
|
+
event = event_types.fetch(event_name).new(**attributes)
|
21
|
+
handlers_for(event_name).each { |handler| handler.call(event) }
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def handlers_for(event_name)
|
27
|
+
@handlers[event_name] ||= []
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Rpruby
|
2
|
+
module Events
|
3
|
+
# An event executed before sending a StartTestItem request.
|
4
|
+
class PrepareStartItemRequest
|
5
|
+
# A hash that contains keys like item's `start_time`, `name`, `description`.
|
6
|
+
attr_reader :request_data
|
7
|
+
|
8
|
+
def initialize(request_data:)
|
9
|
+
@request_data = request_data
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'http'
|
2
|
+
|
3
|
+
module Rpruby
|
4
|
+
# @api private
|
5
|
+
class HttpClient
|
6
|
+
def initialize
|
7
|
+
create_client
|
8
|
+
end
|
9
|
+
|
10
|
+
def send_request(verb, path, options = {})
|
11
|
+
path.prepend("/api/v1/#{Settings.instance.project}/")
|
12
|
+
path.prepend(origin) unless use_persistent?
|
13
|
+
3.times do
|
14
|
+
begin
|
15
|
+
response = @http.request(verb, path, options)
|
16
|
+
rescue StandardError => e
|
17
|
+
puts "Request #{request_info(verb, path)} produced an exception:"
|
18
|
+
puts e
|
19
|
+
recreate_client
|
20
|
+
else
|
21
|
+
return response.parse(:json) if response.status.success?
|
22
|
+
|
23
|
+
message = "Request #{request_info(verb, path)} returned code #{response.code}."
|
24
|
+
message << " Response:\n#{response}" unless response.to_s.empty?
|
25
|
+
puts message
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def create_client
|
33
|
+
@http = HTTP.auth("Bearer #{Settings.instance.uuid}")
|
34
|
+
@http = @http.persistent(origin) if use_persistent?
|
35
|
+
add_insecure_ssl_options if Settings.instance.disable_ssl_verification
|
36
|
+
end
|
37
|
+
|
38
|
+
def add_insecure_ssl_options
|
39
|
+
ssl_context = OpenSSL::SSL::SSLContext.new
|
40
|
+
ssl_context.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
41
|
+
@http.default_options = { ssl_context: ssl_context }
|
42
|
+
end
|
43
|
+
|
44
|
+
# Response should be consumed before sending next request via the same persistent connection.
|
45
|
+
# If an exception occurred, there may be no response so a connection has to be recreated.
|
46
|
+
def recreate_client
|
47
|
+
@http.close
|
48
|
+
create_client
|
49
|
+
end
|
50
|
+
|
51
|
+
def request_info(verb, path)
|
52
|
+
uri = URI.join(origin, path)
|
53
|
+
"#{verb.upcase} `#{uri}`"
|
54
|
+
end
|
55
|
+
|
56
|
+
def origin
|
57
|
+
Addressable::URI.parse(Settings.instance.endpoint).origin
|
58
|
+
end
|
59
|
+
|
60
|
+
def use_persistent?
|
61
|
+
Rpruby::Settings.instance.formatter_modes.include?('use_persistent_connection')
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'log4r/outputter/outputter'
|
2
|
+
|
3
|
+
require_relative '../../rpruby'
|
4
|
+
|
5
|
+
module Rpruby
|
6
|
+
# Custom ReportPortal outputter for 'log4r' gem
|
7
|
+
class Log4rOutputter < Log4r::Outputter
|
8
|
+
def canonical_log(logevent)
|
9
|
+
synch { write(Log4r::LNAMES[logevent.level], format(logevent)) }
|
10
|
+
end
|
11
|
+
|
12
|
+
def write(level, data)
|
13
|
+
Rpruby.send_log(level, data, Rpruby.now)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
module Rpruby
|
4
|
+
class << self
|
5
|
+
# Monkey-patch for built-in Logger class
|
6
|
+
def patch_logger
|
7
|
+
Logger.class_eval do
|
8
|
+
alias_method :orig_add, :add
|
9
|
+
alias_method :orig_write, :<<
|
10
|
+
def add(severity, message = nil, progname = nil, &block)
|
11
|
+
ret = orig_add(severity, message, progname, &block)
|
12
|
+
|
13
|
+
unless severity < @level
|
14
|
+
progname ||= @progname
|
15
|
+
if message.nil?
|
16
|
+
if block_given?
|
17
|
+
message = yield
|
18
|
+
else
|
19
|
+
message = progname
|
20
|
+
progname = @progname
|
21
|
+
end
|
22
|
+
end
|
23
|
+
Rpruby.send_log(format_severity(severity), format_message(format_severity(severity), Time.now, progname, message.to_s), Rpruby.now)
|
24
|
+
end
|
25
|
+
ret
|
26
|
+
end
|
27
|
+
|
28
|
+
def <<(msg)
|
29
|
+
ret = orig_write(msg)
|
30
|
+
Rpruby.send_log(Rpruby::LOG_LEVELS[:unknown], msg.to_s, Rpruby.now)
|
31
|
+
ret
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'logging'
|
2
|
+
|
3
|
+
require_relative '../../rpruby'
|
4
|
+
|
5
|
+
module Rpruby
|
6
|
+
# Custom ReportPortal appender for 'logging' gem
|
7
|
+
class LoggingAppender < ::Logging::Appender
|
8
|
+
def write(event)
|
9
|
+
(str, lvl) = if event.instance_of?(::Logging::LogEvent)
|
10
|
+
[layout.format(event), event.level]
|
11
|
+
else
|
12
|
+
[event.to_s, Rpruby::LOG_LEVELS[:unknown]]
|
13
|
+
end
|
14
|
+
|
15
|
+
Rpruby.send_log(lvl, str, Rpruby.now)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Rpruby
|
2
|
+
# Options of a request to search items
|
3
|
+
class ItemSearchOptions
|
4
|
+
MAPPING = {
|
5
|
+
launch_id: 'filter.eq.launch',
|
6
|
+
name: 'filter.eq.name',
|
7
|
+
description: 'filter.eq.description',
|
8
|
+
parameter_key: 'filter.eq.parameters$key',
|
9
|
+
parameter_value: 'filter.eq.parameters$value',
|
10
|
+
page_size: 'page.size',
|
11
|
+
page_number: 'page.page'
|
12
|
+
}.freeze
|
13
|
+
|
14
|
+
attr_reader :query_params
|
15
|
+
|
16
|
+
def initialize(params = {})
|
17
|
+
@query_params = params.map { |mapping_key, v| [param_name(mapping_key), v] }.to_h
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def param_name(mapping_key)
|
23
|
+
MAPPING.fetch(mapping_key) { raise KeyError, "key not found: '#{mapping_key.inspect}'. It should be one of: #{MAPPING.keys}" }
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Rpruby
|
2
|
+
# Represents a test item
|
3
|
+
class TestItem
|
4
|
+
attr_reader :launch_id, :unique_id, :name, :description, :type, :parameters, :tags, :status, :start_time
|
5
|
+
attr_accessor :id, :closed
|
6
|
+
|
7
|
+
def initialize(options = {})
|
8
|
+
options = options.map { |k, v| [k.to_sym, v] }.to_h
|
9
|
+
@launch_id = options[:launch_id]
|
10
|
+
@unique_id = options[:unique_id]
|
11
|
+
@name = options[:name]
|
12
|
+
@description = options[:description]
|
13
|
+
@type = options[:type]
|
14
|
+
@parameters = options[:parameters]
|
15
|
+
@tags = options[:tags]
|
16
|
+
@status = options[:status]
|
17
|
+
@start_time = options[:start_time]
|
18
|
+
@id = options[:id]
|
19
|
+
@closed = options[:closed]
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
require 'securerandom'
|
2
|
+
require 'tree'
|
3
|
+
require 'rspec/core'
|
4
|
+
|
5
|
+
require_relative '../../rpruby'
|
6
|
+
|
7
|
+
# TODO: Screenshots
|
8
|
+
# TODO: Logs
|
9
|
+
module Rpruby
|
10
|
+
module RSpec
|
11
|
+
class Formatter
|
12
|
+
MAX_DESCRIPTION_LENGTH = 255
|
13
|
+
MIN_DESCRIPTION_LENGTH = 3
|
14
|
+
|
15
|
+
::RSpec::Core::Formatters.register self, :start, :example_group_started, :example_group_finished,
|
16
|
+
:example_started, :example_passed, :example_failed,
|
17
|
+
:example_pending, :message, :stop
|
18
|
+
|
19
|
+
def initialize(_output)
|
20
|
+
ENV['REPORT_PORTAL_USED'] = 'true'
|
21
|
+
end
|
22
|
+
|
23
|
+
def start(_start_notification)
|
24
|
+
cmd_args = ARGV.map { |arg| arg.include?('rp_uuid=') ? 'rp_uuid=[FILTERED]' : arg }.join(' ')
|
25
|
+
Rpruby.start_launch(cmd_args)
|
26
|
+
@root_node = Tree::TreeNode.new(SecureRandom.hex)
|
27
|
+
@current_group_node = @root_node
|
28
|
+
end
|
29
|
+
|
30
|
+
def example_group_started(group_notification)
|
31
|
+
description = group_notification.group.description
|
32
|
+
if description.size < MIN_DESCRIPTION_LENGTH
|
33
|
+
p "Group description should be at least #{MIN_DESCRIPTION_LENGTH} characters ('group_notification': #{group_notification.inspect})"
|
34
|
+
return
|
35
|
+
end
|
36
|
+
item = Rpruby::TestItem.new(name: description[0..MAX_DESCRIPTION_LENGTH - 1],
|
37
|
+
type: :TEST,
|
38
|
+
id: nil,
|
39
|
+
start_time: Rpruby.now,
|
40
|
+
description: '',
|
41
|
+
closed: false,
|
42
|
+
tags: [])
|
43
|
+
group_node = Tree::TreeNode.new(SecureRandom.hex, item)
|
44
|
+
if group_node.nil?
|
45
|
+
p "Group node is nil for item #{item.inspect}"
|
46
|
+
else
|
47
|
+
@current_group_node << group_node unless @current_group_node.nil? # make @current_group_node parent of group_node
|
48
|
+
@current_group_node = group_node
|
49
|
+
group_node.content.id = ReportPortal.start_item(group_node)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def example_group_finished(_group_notification)
|
54
|
+
unless @current_group_node.nil?
|
55
|
+
Rpruby.finish_item(@current_group_node.content)
|
56
|
+
@current_group_node = @current_group_node.parent
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def example_started(notification)
|
61
|
+
description = notification.example.description
|
62
|
+
if description.size < MIN_DESCRIPTION_LENGTH
|
63
|
+
p "Example description should be at least #{MIN_DESCRIPTION_LENGTH} characters ('notification': #{notification.inspect})"
|
64
|
+
return
|
65
|
+
end
|
66
|
+
Rpruby.current_scenario = Rpruby::TestItem.new(name: description[0..MAX_DESCRIPTION_LENGTH - 1],
|
67
|
+
type: :STEP,
|
68
|
+
id: nil,
|
69
|
+
start_time: Rpruby.now,
|
70
|
+
description: '',
|
71
|
+
closed: false,
|
72
|
+
tags: [])
|
73
|
+
example_node = Tree::TreeNode.new(SecureRandom.hex, Rpruby.current_scenario)
|
74
|
+
if example_node.nil?
|
75
|
+
p "Example node is nil for scenario #{Rpruby.current_scenario.inspect}"
|
76
|
+
else
|
77
|
+
@current_group_node << example_node
|
78
|
+
example_node.content.id = Rpruby.start_item(example_node)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def example_passed(_notification)
|
83
|
+
Rpruby.finish_item(Rpruby.current_scenario, :passed) unless Rpruby.current_scenario.nil?
|
84
|
+
Rpruby.current_scenario = nil
|
85
|
+
end
|
86
|
+
|
87
|
+
def example_failed(notification)
|
88
|
+
exception = notification.exception
|
89
|
+
Rpruby.send_log(:failed, %(#{exception.class}: #{exception.message}\n\nStacktrace: #{notification.formatted_backtrace.join("\n")}), Rpruby.now)
|
90
|
+
Rpruby.finish_item(Rpruby.current_scenario, :failed) unless Rpruby.current_scenario.nil?
|
91
|
+
Rpruby.current_scenario = nil
|
92
|
+
end
|
93
|
+
|
94
|
+
def example_pending(_notification)
|
95
|
+
Rpruby.finish_item(Rpruby.current_scenario, :skipped) unless Rpruby.current_scenario.nil?
|
96
|
+
Rpruby.current_scenario = nil
|
97
|
+
end
|
98
|
+
|
99
|
+
def message(notification)
|
100
|
+
if notification.message.respond_to?(:read)
|
101
|
+
Rpruby.send_file(:passed, notification.message)
|
102
|
+
else
|
103
|
+
Rpruby.send_log(:passed, notification.message, Rpruby.now)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def stop(_notification)
|
108
|
+
Rpruby.finish_launch
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'singleton'
|
3
|
+
|
4
|
+
module Rpruby
|
5
|
+
class Settings
|
6
|
+
include Singleton
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
filename = ENV.fetch('rp_config') do
|
10
|
+
glob = Dir.glob('{,.config/,config/}report{,-,_}portal{.yml,.yaml}')
|
11
|
+
p "Multiple configuration files found for ReportPortal. Using the first one: #{glob.first}" if glob.size > 1
|
12
|
+
glob.first
|
13
|
+
end
|
14
|
+
|
15
|
+
@properties = filename.nil? ? {} : YAML.load_file(filename)
|
16
|
+
keys = {
|
17
|
+
'uuid' => true,
|
18
|
+
'endpoint' => true,
|
19
|
+
'project' => true,
|
20
|
+
'launch' => true,
|
21
|
+
'tags' => false,
|
22
|
+
'description' => false,
|
23
|
+
'attributes' => false,
|
24
|
+
'is_debug' => false,
|
25
|
+
'disable_ssl_verification' => false,
|
26
|
+
# for parallel execution only
|
27
|
+
'use_standard_logger' => false,
|
28
|
+
'launch_id' => false,
|
29
|
+
'file_with_launch_id' => false
|
30
|
+
}
|
31
|
+
|
32
|
+
keys.each do |key, is_required|
|
33
|
+
define_singleton_method(key.to_sym) { setting(key) }
|
34
|
+
next unless is_required && public_send(key).nil?
|
35
|
+
|
36
|
+
env_variable_name = env_variable_name(key)
|
37
|
+
raise "ReportPortal: Define environment variable '#{env_variable_name.upcase}', '#{env_variable_name}' "\
|
38
|
+
"or key #{key} in the configuration YAML file"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def launch_mode
|
43
|
+
is_debug ? 'DEBUG' : 'DEFAULT'
|
44
|
+
end
|
45
|
+
|
46
|
+
def formatter_modes
|
47
|
+
setting('formatter_modes') || []
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def setting(key)
|
53
|
+
env_variable_name = env_variable_name(key)
|
54
|
+
return YAML.safe_load(ENV[env_variable_name.upcase]) if ENV.key?(env_variable_name.upcase)
|
55
|
+
|
56
|
+
return YAML.safe_load(ENV[env_variable_name]) if ENV.key?(env_variable_name)
|
57
|
+
|
58
|
+
@properties[key]
|
59
|
+
end
|
60
|
+
|
61
|
+
def env_variable_name(key)
|
62
|
+
'rp_' + key
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|