rpruby 1.2 → 1.2.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|