reportportal_new 0.7
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +104 -0
- data/lib/report_portal/cucumber/formatter.rb +72 -0
- data/lib/report_portal/cucumber/parallel_formatter.rb +14 -0
- data/lib/report_portal/cucumber/parallel_report.rb +54 -0
- data/lib/report_portal/cucumber/report.rb +220 -0
- data/lib/report_portal/event_bus.rb +30 -0
- data/lib/report_portal/events/prepare_start_item_request.rb +13 -0
- data/lib/report_portal/http_client.rb +64 -0
- data/lib/report_portal/logging/log4r_outputter.rb +16 -0
- data/lib/report_portal/logging/logger.rb +36 -0
- data/lib/report_portal/logging/logging_appender.rb +18 -0
- data/lib/report_portal/models/item_search_options.rb +26 -0
- data/lib/report_portal/models/test_item.rb +22 -0
- data/lib/report_portal/rspec/formatter.rb +112 -0
- data/lib/report_portal/settings.rb +65 -0
- data/lib/report_portal/tasks.rb +27 -0
- data/lib/report_portal/version.rb +3 -0
- data/lib/reportportal.rb +215 -0
- metadata +118 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: ffa835c0d8f575802b92617f1ec388e58f352ab57a0c9883c1a599e6f46e7e40
|
4
|
+
data.tar.gz: 2ab5f9ceda441223e866ab17f68e62a5495ba149635c2fed39ff32dc69d61868
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 5bd766e7905f78d31341315a301270c1e6f8e90c7f50f6067cdd8aadd5b57468dec8064319d9e609b769576c91e8fd0b051787d0a885348b69b0c2976d466b4c
|
7
|
+
data.tar.gz: '00284db69276412b208c4fd3614180ea3558525a5d26e9cbc2112ef90e1835867353181b15e5dcafed701e9157722c81e98cdad7f8b59cfc4c72cfbcef25a489'
|
data/README.md
ADDED
@@ -0,0 +1,104 @@
|
|
1
|
+
# Ruby Cucumber and RSpec formatters for ReportPortal
|
2
|
+
|
3
|
+
[Download](https://rubygems.org/gems/reportportal)
|
4
|
+
|
5
|
+
[![Join Slack chat!](https://reportportal-slack-auto.herokuapp.com/badge.svg)](https://reportportal-slack-auto.herokuapp.com)
|
6
|
+
[![stackoverflow](https://img.shields.io/badge/reportportal-stackoverflow-orange.svg?style=flat)](http://stackoverflow.com/questions/tagged/reportportal)
|
7
|
+
[![UserVoice](https://img.shields.io/badge/uservoice-vote%20ideas-orange.svg?style=flat)](https://rpp.uservoice.com/forums/247117-report-portal)
|
8
|
+
[![Build with Love](https://img.shields.io/badge/build%20with-❤%EF%B8%8F%E2%80%8D-lightgrey.svg)](http://reportportal.io?style=flat)
|
9
|
+
|
10
|
+
|
11
|
+
## Installation
|
12
|
+
|
13
|
+
Use Ruby 2.3+
|
14
|
+
|
15
|
+
**Rubygems**
|
16
|
+
|
17
|
+
From https://rubygems.org
|
18
|
+
|
19
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
20
|
+
gem install reportportal
|
21
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
22
|
+
|
23
|
+
or
|
24
|
+
|
25
|
+
Add `gem 'reportportal', git: 'https://github.com/reportportal/agent-ruby.git'` to your `Gemfile`. Run `bundle install`.
|
26
|
+
|
27
|
+
## Usage (examples)
|
28
|
+
|
29
|
+
* With Cucumber:
|
30
|
+
|
31
|
+
```cucumber <other options> -f ReportPortal::Cucumber::Formatter```
|
32
|
+
|
33
|
+
* With Cucumber and parallel_tests gem:
|
34
|
+
|
35
|
+
```parallel_cucumber <some options> -o '<some other options> -f ReportPortal::Cucumber::ParallelFormatter'```
|
36
|
+
|
37
|
+
* With RSpec:
|
38
|
+
|
39
|
+
```rspec <other options> -f ReportPortal::RSpec::Formatter```
|
40
|
+
|
41
|
+
## Configuration
|
42
|
+
Create report_portal.yml configuration file in one of the following folders of your project: '.', './.config', './config' (see report_portal.yaml.example).
|
43
|
+
Alternatively specify path to configuration file via rp_config environment variable.
|
44
|
+
|
45
|
+
Supported settings:
|
46
|
+
|
47
|
+
- uuid - uuid of the ReportPortal user
|
48
|
+
- endpoint - URI of ReportPortal web service where requests should be sent
|
49
|
+
- launch - launch name
|
50
|
+
- description - custom launch description
|
51
|
+
- project - project name
|
52
|
+
- attributes - launch attributes. key value object
|
53
|
+
- formatter_modes - array of modes that modify formatter behavior, see [formatter modes](#formatter_modes)
|
54
|
+
- launch_id - id of previously created launch (to be used if formatter_modes contains attach_to_launch)
|
55
|
+
- file_with_launch_id - path to file with id of launch (to be used if formatter_modes contains attach_to_launch)
|
56
|
+
- disable_ssl_verification - set to true to disable SSL verification on connect to ReportPortal (potential security hole!). Set `disable_ssl_verification` to `true` if you see the following error:
|
57
|
+
```
|
58
|
+
Request to https://rp.epam.com/reportportal-ws/api/v1/pass-team/launch//finish produced an exception: RestClient::SSLCertificateNotVerified: SSL_connect returned=1 errno=0 state=SSLv3 read server certificate B: certificate verify failed
|
59
|
+
```
|
60
|
+
- is_debug - set to true to mark the launch as 'DEBUG' (it will appear in Debug tab in Report Portal)
|
61
|
+
- use_standard_logger - set to true to enable logging via standard Ruby Logger class to ReportPortal. Note that log messages are transformed to strings before sending.
|
62
|
+
|
63
|
+
Each of these settings can be overridden by an environment variable with the same name and 'rp_' prefix (e.g. 'rp_uuid' for 'uuid'). Environment variable can be upper cased (e.g. 'RP_UUID').
|
64
|
+
Environment variables take precedence over YAML configuration.
|
65
|
+
Environment variable values are parsed as YAML values.
|
66
|
+
|
67
|
+
## WebMock configuration
|
68
|
+
If you use WebMock for stubbing and setting expectations on HTTP requests in Ruby,
|
69
|
+
add this to your configuration file (for RSpec it would be `rails_helper.rb` or `spec_helper.rb`)
|
70
|
+
|
71
|
+
```ruby
|
72
|
+
WebMock.disable_net_connect!(:net_http_connect_on_start => true, :allow_localhost => true, :allow => [/rp\.epam\.com/]) # Don't break Net::HTTP
|
73
|
+
```
|
74
|
+
|
75
|
+
<a name="formatter_modes"></a>
|
76
|
+
## Formatter modes
|
77
|
+
|
78
|
+
The following modes are supported:
|
79
|
+
|
80
|
+
| Name | Purpose |
|
81
|
+
| --- | --- |
|
82
|
+
| attach_to_launch | Do not create a new launch but add executing features/scenarios to an existing launch. Use launch_id or file_with_launch_id settings to configure that. If they are not present client will check rp_launch_id.tmp in `Dir.tmpdir`)
|
83
|
+
| use_same_thread_for_reporting | Send reporting commands in the same main thread used for running tests. This mode is useful for debugging this Report Portal client. It changes default behavior to send commands in the separate thread. Default behavior is there not to slow test execution. |
|
84
|
+
| skip_reporting_hierarchy | Do not create items for folders and feature files |
|
85
|
+
| use_persistent_connection | Use persistent connection to connect to the server |
|
86
|
+
|
87
|
+
## Logging
|
88
|
+
Experimental support for three common logging frameworks was added:
|
89
|
+
|
90
|
+
- Logger (part of standard Ruby library)
|
91
|
+
- [Logging](http://rubygems.org/gems/logging)
|
92
|
+
- [Log4r](https://rubygems.org/gems/log4r)
|
93
|
+
|
94
|
+
To use Logger, set use_standard_logger parameter to true (see Configuration chapter). For the other two corresponding appenders/outputters are available under reportportal/logging.
|
95
|
+
|
96
|
+
## Parallel formatter
|
97
|
+
|
98
|
+
ReportPortal::Cucumber::ParallelFormatter can be used for tests started via parallel_tests gem.
|
99
|
+
|
100
|
+
Note: Launch id is shared between independent processes (as is the case with parallel_tests gem) via a file in `Dir.tmpdir`.
|
101
|
+
|
102
|
+
## Links
|
103
|
+
|
104
|
+
- [ReportPortal](https://github.com/reportportal/)
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require_relative 'report'
|
2
|
+
|
3
|
+
module ReportPortal
|
4
|
+
module Cucumber
|
5
|
+
class Formatter
|
6
|
+
# @api private
|
7
|
+
def initialize(config)
|
8
|
+
ENV['REPORT_PORTAL_USED'] = 'true'
|
9
|
+
|
10
|
+
setup_message_processing
|
11
|
+
|
12
|
+
@io = config.out_stream
|
13
|
+
|
14
|
+
%i[test_case_started test_case_finished test_step_started test_step_finished test_run_finished].each do |event_name|
|
15
|
+
config.on_event event_name do |event|
|
16
|
+
process_message(event_name, event)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
config.on_event(:test_run_finished) { finish_message_processing }
|
20
|
+
end
|
21
|
+
|
22
|
+
def puts(message)
|
23
|
+
process_message(:puts, message)
|
24
|
+
@io.puts(message)
|
25
|
+
@io.flush
|
26
|
+
end
|
27
|
+
|
28
|
+
def embed(*args)
|
29
|
+
process_message(:embed, *args)
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def report
|
35
|
+
@report ||= ReportPortal::Cucumber::Report.new
|
36
|
+
end
|
37
|
+
|
38
|
+
def setup_message_processing
|
39
|
+
return if use_same_thread_for_reporting?
|
40
|
+
|
41
|
+
@queue = Queue.new
|
42
|
+
@thread = Thread.new do
|
43
|
+
loop do
|
44
|
+
method_arr = @queue.pop
|
45
|
+
report.public_send(*method_arr)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
@thread.abort_on_exception = true
|
49
|
+
end
|
50
|
+
|
51
|
+
def finish_message_processing
|
52
|
+
return if use_same_thread_for_reporting?
|
53
|
+
|
54
|
+
sleep 0.03 while !@queue.empty? || @queue.num_waiting.zero? # TODO: how to interrupt launch if the user aborted execution
|
55
|
+
@thread.kill
|
56
|
+
end
|
57
|
+
|
58
|
+
def process_message(report_method_name, *method_args)
|
59
|
+
args = [report_method_name, *method_args, ReportPortal.now]
|
60
|
+
if use_same_thread_for_reporting?
|
61
|
+
report.public_send(*args)
|
62
|
+
else
|
63
|
+
@queue.push(args)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def use_same_thread_for_reporting?
|
68
|
+
ReportPortal::Settings.instance.formatter_modes.include?('use_same_thread_for_reporting')
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require_relative 'formatter'
|
2
|
+
require_relative 'parallel_report'
|
3
|
+
|
4
|
+
module ReportPortal
|
5
|
+
module Cucumber
|
6
|
+
class ParallelFormatter < Formatter
|
7
|
+
private
|
8
|
+
|
9
|
+
def report
|
10
|
+
@report ||= ReportPortal::Cucumber::ParallelReport.new
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'parallel_tests'
|
2
|
+
|
3
|
+
require_relative 'report'
|
4
|
+
|
5
|
+
module ReportPortal
|
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(ReportPortal.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 = ReportPortal.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 #{ReportPortal.launch_id}"
|
46
|
+
ReportPortal.close_child_items(nil)
|
47
|
+
time_to_send = time_to_send(desired_time)
|
48
|
+
ReportPortal.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 '../../reportportal'
|
7
|
+
require_relative '../logging/logger'
|
8
|
+
|
9
|
+
module ReportPortal
|
10
|
+
module Cucumber
|
11
|
+
# @api private
|
12
|
+
class Report
|
13
|
+
def parallel?
|
14
|
+
false
|
15
|
+
end
|
16
|
+
|
17
|
+
def attach_to_launch?
|
18
|
+
ReportPortal::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 = ReportPortal.now)
|
29
|
+
if attach_to_launch?
|
30
|
+
ReportPortal.launch_id =
|
31
|
+
if ReportPortal::Settings.instance.launch_id
|
32
|
+
ReportPortal::Settings.instance.launch_id
|
33
|
+
else
|
34
|
+
file_path = ReportPortal::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 #{ReportPortal.launch_id}"
|
38
|
+
else
|
39
|
+
description = ReportPortal::Settings.instance.description
|
40
|
+
description ||= ARGV.map { |arg| arg.gsub(/rp_uuid=.+/, 'rp_uuid=[FILTERED]') }.join(' ')
|
41
|
+
ReportPortal.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 = ReportPortal.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
|
+
ReportPortal.current_scenario = ReportPortal::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, ReportPortal.current_scenario)
|
61
|
+
@parent_item_node << scenario_node
|
62
|
+
ReportPortal.current_scenario.id = ReportPortal.start_item(scenario_node)
|
63
|
+
end
|
64
|
+
|
65
|
+
def test_case_finished(event, desired_time = ReportPortal.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
|
+
ReportPortal.finish_item(ReportPortal.current_scenario, status, time_to_send(desired_time), issue)
|
74
|
+
ReportPortal.current_scenario = nil
|
75
|
+
end
|
76
|
+
|
77
|
+
def test_step_started(event, desired_time = ReportPortal.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
|
+
ReportPortal.send_log(:trace, message, time_to_send(desired_time))
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def test_step_finished(event, desired_time = ReportPortal.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
|
+
ReportPortal.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
|
+
ReportPortal.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 = ReportPortal.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
|
+
ReportPortal.finish_launch(time_to_send)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def puts(message, desired_time = ReportPortal.now)
|
130
|
+
ReportPortal.send_log(:info, message, time_to_send(desired_time))
|
131
|
+
end
|
132
|
+
|
133
|
+
def embed(path_or_src, mime_type, label, desired_time = ReportPortal.now)
|
134
|
+
ReportPortal.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 = ReportPortal.item_id_of(name, parent_node)) # get id for folder from report portal
|
179
|
+
# get child id from other process
|
180
|
+
item = ReportPortal::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 = ReportPortal::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 = ReportPortal.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
|
+
ReportPortal.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
|
+
ReportPortal.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
|
+
!ReportPortal::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 ReportPortal
|
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 ReportPortal
|
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 ReportPortal
|
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
|
+
ReportPortal::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 '../../reportportal'
|
4
|
+
|
5
|
+
module ReportPortal
|
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
|
+
ReportPortal.send_log(level, data, ReportPortal.now)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
module ReportPortal
|
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
|
+
ReportPortal.send_log(format_severity(severity), format_message(format_severity(severity), Time.now, progname, message.to_s), ReportPortal.now)
|
24
|
+
end
|
25
|
+
ret
|
26
|
+
end
|
27
|
+
|
28
|
+
def <<(msg)
|
29
|
+
ret = orig_write(msg)
|
30
|
+
ReportPortal.send_log(ReportPortal::LOG_LEVELS[:unknown], msg.to_s, ReportPortal.now)
|
31
|
+
ret
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'logging'
|
2
|
+
|
3
|
+
require_relative '../../reportportal'
|
4
|
+
|
5
|
+
module ReportPortal
|
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, ReportPortal::LOG_LEVELS[:unknown]]
|
13
|
+
end
|
14
|
+
|
15
|
+
ReportPortal.send_log(lvl, str, ReportPortal.now)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module ReportPortal
|
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 ReportPortal
|
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 '../../reportportal'
|
6
|
+
|
7
|
+
# TODO: Screenshots
|
8
|
+
# TODO: Logs
|
9
|
+
module ReportPortal
|
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
|
+
ReportPortal.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 = ReportPortal::TestItem.new(name: description[0..MAX_DESCRIPTION_LENGTH - 1],
|
37
|
+
type: :TEST,
|
38
|
+
id: nil,
|
39
|
+
start_time: ReportPortal.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
|
+
ReportPortal.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
|
+
ReportPortal.current_scenario = ReportPortal::TestItem.new(name: description[0..MAX_DESCRIPTION_LENGTH - 1],
|
67
|
+
type: :STEP,
|
68
|
+
id: nil,
|
69
|
+
start_time: ReportPortal.now,
|
70
|
+
description: '',
|
71
|
+
closed: false,
|
72
|
+
tags: [])
|
73
|
+
example_node = Tree::TreeNode.new(SecureRandom.hex, ReportPortal.current_scenario)
|
74
|
+
if example_node.nil?
|
75
|
+
p "Example node is nil for scenario #{ReportPortal.current_scenario.inspect}"
|
76
|
+
else
|
77
|
+
@current_group_node << example_node
|
78
|
+
example_node.content.id = ReportPortal.start_item(example_node)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def example_passed(_notification)
|
83
|
+
ReportPortal.finish_item(ReportPortal.current_scenario, :passed) unless ReportPortal.current_scenario.nil?
|
84
|
+
ReportPortal.current_scenario = nil
|
85
|
+
end
|
86
|
+
|
87
|
+
def example_failed(notification)
|
88
|
+
exception = notification.exception
|
89
|
+
ReportPortal.send_log(:failed, %(#{exception.class}: #{exception.message}\n\nStacktrace: #{notification.formatted_backtrace.join("\n")}), ReportPortal.now)
|
90
|
+
ReportPortal.finish_item(ReportPortal.current_scenario, :failed) unless ReportPortal.current_scenario.nil?
|
91
|
+
ReportPortal.current_scenario = nil
|
92
|
+
end
|
93
|
+
|
94
|
+
def example_pending(_notification)
|
95
|
+
ReportPortal.finish_item(ReportPortal.current_scenario, :skipped) unless ReportPortal.current_scenario.nil?
|
96
|
+
ReportPortal.current_scenario = nil
|
97
|
+
end
|
98
|
+
|
99
|
+
def message(notification)
|
100
|
+
if notification.message.respond_to?(:read)
|
101
|
+
ReportPortal.send_file(:passed, notification.message)
|
102
|
+
else
|
103
|
+
ReportPortal.send_log(:passed, notification.message, ReportPortal.now)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def stop(_notification)
|
108
|
+
ReportPortal.finish_launch
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'singleton'
|
3
|
+
|
4
|
+
module ReportPortal
|
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
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'pathname'
|
3
|
+
require 'tempfile'
|
4
|
+
require_relative '../reportportal'
|
5
|
+
|
6
|
+
namespace :reportportal do
|
7
|
+
desc 'Start launch in Report Portal and print its id to $stdout (for use with attach_to_launch formatter mode)'
|
8
|
+
task :start_launch do
|
9
|
+
description = ENV['description'] || ReportPortal::Settings.instance.description
|
10
|
+
file_to_write_launch_id = ENV['file_for_launch_id'] || ReportPortal::Settings.instance.file_with_launch_id
|
11
|
+
file_to_write_launch_id ||= Pathname(Dir.tmpdir) + 'rp_launch_id.tmp'
|
12
|
+
launch_id = ReportPortal.start_launch(description)
|
13
|
+
File.write(file_to_write_launch_id, launch_id)
|
14
|
+
puts launch_id
|
15
|
+
end
|
16
|
+
|
17
|
+
desc 'Finish launch in Report Portal (for use with attach_to_launch formatter mode)'
|
18
|
+
task :finish_launch do
|
19
|
+
launch_id = ENV['launch_id'] || ReportPortal::Settings.instance.launch_id
|
20
|
+
file_with_launch_id = ENV['file_with_launch_id'] || ReportPortal::Settings.instance.file_with_launch_id
|
21
|
+
puts "Launch id isn't provided. Provide it either via RP_LAUNCH_ID or RP_FILE_WITH_LAUNCH_ID environment variables" if !launch_id && !file_with_launch_id
|
22
|
+
puts 'Both RP_LAUNCH_ID and RP_FILE_WITH_LAUNCH_ID are provided via environment variables' if launch_id && file_with_launch_id
|
23
|
+
ReportPortal.launch_id = launch_id || File.read(file_with_launch_id)
|
24
|
+
ReportPortal.close_child_items(nil)
|
25
|
+
ReportPortal.finish_launch
|
26
|
+
end
|
27
|
+
end
|
data/lib/reportportal.rb
ADDED
@@ -0,0 +1,215 @@
|
|
1
|
+
require 'base64'
|
2
|
+
require 'cgi'
|
3
|
+
require 'http'
|
4
|
+
require 'json'
|
5
|
+
require 'mime/types'
|
6
|
+
require 'pathname'
|
7
|
+
require 'tempfile'
|
8
|
+
require 'uri'
|
9
|
+
|
10
|
+
require_relative 'report_portal/event_bus'
|
11
|
+
require_relative 'report_portal/models/item_search_options'
|
12
|
+
require_relative 'report_portal/models/test_item'
|
13
|
+
require_relative 'report_portal/settings'
|
14
|
+
require_relative 'report_portal/http_client'
|
15
|
+
|
16
|
+
module ReportPortal
|
17
|
+
LOG_LEVELS = { error: 'ERROR', warn: 'WARN', info: 'INFO', debug: 'DEBUG', trace: 'TRACE', fatal: 'FATAL', unknown: 'UNKNOWN' }.freeze
|
18
|
+
|
19
|
+
class << self
|
20
|
+
attr_accessor :launch_id, :current_scenario
|
21
|
+
|
22
|
+
def now
|
23
|
+
(current_time.to_f * 1000).to_i
|
24
|
+
end
|
25
|
+
|
26
|
+
def status_to_level(status)
|
27
|
+
case status
|
28
|
+
when :passed
|
29
|
+
LOG_LEVELS[:info]
|
30
|
+
when :failed, :undefined, :pending, :error
|
31
|
+
LOG_LEVELS[:error]
|
32
|
+
when :skipped
|
33
|
+
LOG_LEVELS[:warn]
|
34
|
+
else
|
35
|
+
LOG_LEVELS.fetch(status, LOG_LEVELS[:info])
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def start_launch(description, start_time = now)
|
40
|
+
required_data = { name: Settings.instance.launch, start_time: start_time, description:
|
41
|
+
description, mode: Settings.instance.launch_mode }
|
42
|
+
data = prepare_options(required_data, Settings.instance)
|
43
|
+
@launch_id = send_request(:post, 'launch', json: data)['id']
|
44
|
+
end
|
45
|
+
|
46
|
+
def finish_launch(end_time = now)
|
47
|
+
data = { end_time: end_time }
|
48
|
+
@finished_launch = send_request(:put, "launch/#{@launch_id}/finish", json: data)
|
49
|
+
@launch_link = @finished_launch['link']
|
50
|
+
print @launch_link
|
51
|
+
end
|
52
|
+
|
53
|
+
def start_item(item_node)
|
54
|
+
path = 'item'
|
55
|
+
path += "/#{item_node.parent.content.id}" unless item_node.parent&.is_root?
|
56
|
+
item = item_node.content
|
57
|
+
data = { start_time: item.start_time, name: item.name[0, 255], type: item.type.to_s, launch_id: @launch_id, description: item.description }
|
58
|
+
data[:tags] = item.tags unless item.tags.empty?
|
59
|
+
event_bus.broadcast(:prepare_start_item_request, request_data: data)
|
60
|
+
send_request(:post, path, json: data)['id']
|
61
|
+
end
|
62
|
+
|
63
|
+
def finish_item(item, status = nil, end_time = nil, force_issue = nil)
|
64
|
+
unless item.nil? || item.id.nil? || item.closed
|
65
|
+
data = { end_time: end_time.nil? ? now : end_time }
|
66
|
+
data[:status] = status unless status.nil?
|
67
|
+
if force_issue && status != :passed # TODO: check for :passed status is probably not needed
|
68
|
+
data[:issue] = { issue_type: 'AUTOMATION_BUG', comment: force_issue.to_s }
|
69
|
+
elsif status == :skipped
|
70
|
+
data[:issue] = { issue_type: 'NOT_ISSUE' }
|
71
|
+
end
|
72
|
+
send_request(:put, "item/#{item.id}", json: data)
|
73
|
+
item.closed = true
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# TODO: implement force finish
|
78
|
+
|
79
|
+
def send_log(status, message, time)
|
80
|
+
unless @current_scenario.nil? || @current_scenario.closed # it can be nil if scenario outline in expand mode is executed
|
81
|
+
data = { item_id: @current_scenario.id, time: time, level: status_to_level(status), message: message.to_s }
|
82
|
+
send_request(:post, 'log', json: data)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def send_file(status, path_or_src, label = nil, time = now, mime_type = 'image/png')
|
87
|
+
str_without_nils = path_or_src.to_s.gsub("\0", '') # file? does not allow NULLs inside the string
|
88
|
+
if File.file?(str_without_nils)
|
89
|
+
send_file_from_path(status, path_or_src, label, time, mime_type)
|
90
|
+
else
|
91
|
+
if mime_type =~ /;base64$/
|
92
|
+
mime_type = mime_type[0..-8]
|
93
|
+
path_or_src = Base64.decode64(path_or_src)
|
94
|
+
end
|
95
|
+
extension = ".#{MIME::Types[mime_type].first.extensions.first}"
|
96
|
+
Tempfile.open(['report_portal', extension]) do |tempfile|
|
97
|
+
tempfile.binmode
|
98
|
+
tempfile.write(path_or_src)
|
99
|
+
tempfile.rewind
|
100
|
+
send_file_from_path(status, tempfile.path, label, time, mime_type)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
# @option options [Hash] options, see ReportPortal::ItemSearchOptions
|
106
|
+
def get_items(filter_options = {})
|
107
|
+
page_size = 100
|
108
|
+
max_pages = 100
|
109
|
+
all_items = []
|
110
|
+
1.step.each do |page_number|
|
111
|
+
raise 'Too many pages with the results were returned' if page_number > max_pages
|
112
|
+
|
113
|
+
options = ItemSearchOptions.new({ page_size: page_size, page_number: page_number }.merge(filter_options))
|
114
|
+
page_items = send_request(:get, 'item', params: options.query_params)['content'].map do |item_params|
|
115
|
+
TestItem.new(item_params)
|
116
|
+
end
|
117
|
+
all_items += page_items
|
118
|
+
break if page_items.size < page_size
|
119
|
+
end
|
120
|
+
all_items
|
121
|
+
end
|
122
|
+
|
123
|
+
# @param item_ids [Array<String>] an array of items to remove (represented by ids)
|
124
|
+
def delete_items(item_ids)
|
125
|
+
send_request(:delete, 'item', params: { ids: item_ids })
|
126
|
+
end
|
127
|
+
|
128
|
+
# needed for parallel formatter
|
129
|
+
def item_id_of(name, parent_node)
|
130
|
+
path = if parent_node.is_root? # folder without parent folder
|
131
|
+
"item?filter.eq.launch=#{@launch_id}&filter.eq.name=#{CGI.escape(name)}&filter.size.path=0"
|
132
|
+
else
|
133
|
+
"item?filter.eq.parent=#{parent_node.content.id}&filter.eq.name=#{CGI.escape(name)}"
|
134
|
+
end
|
135
|
+
data = send_request(:get, path)
|
136
|
+
if data.key? 'content'
|
137
|
+
data['content'].empty? ? nil : data['content'][0]['id']
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
# needed for parallel formatter
|
142
|
+
def close_child_items(parent_id)
|
143
|
+
path = if parent_id.nil?
|
144
|
+
"item?filter.eq.launch=#{@launch_id}&filter.size.path=0&page.page=1&page.size=100"
|
145
|
+
else
|
146
|
+
"item?filter.eq.parent=#{parent_id}&page.page=1&page.size=100"
|
147
|
+
end
|
148
|
+
ids = []
|
149
|
+
loop do
|
150
|
+
data = send_request(:get, path)
|
151
|
+
if data.key?('links')
|
152
|
+
link = data['links'].find { |i| i['rel'] == 'next' }
|
153
|
+
url = link.nil? ? nil : link['href']
|
154
|
+
else
|
155
|
+
url = nil
|
156
|
+
end
|
157
|
+
data['content'].each do |i|
|
158
|
+
ids << i['id'] if i['has_childs'] && i['status'] == 'IN_PROGRESS'
|
159
|
+
end
|
160
|
+
break if url.nil?
|
161
|
+
end
|
162
|
+
|
163
|
+
ids.each do |id|
|
164
|
+
close_child_items(id)
|
165
|
+
finish_item(TestItem.new(id: id))
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
# Registers an event. The proc will be called back with the event object.
|
170
|
+
def on_event(name, &proc)
|
171
|
+
event_bus.on(name, &proc)
|
172
|
+
end
|
173
|
+
|
174
|
+
private
|
175
|
+
|
176
|
+
def send_file_from_path(status, path, label, time, mime_type)
|
177
|
+
File.open(File.realpath(path), 'rb') do |file|
|
178
|
+
filename = File.basename(file)
|
179
|
+
json = [{ level: status_to_level(status), message: label || filename, item_id: @current_scenario.id, time: time, file: { name: filename } }]
|
180
|
+
form = {
|
181
|
+
json_request_part: HTTP::FormData::Part.new(JSON.dump(json), content_type: 'application/json'),
|
182
|
+
binary_part: HTTP::FormData::File.new(file, filename: filename, content_type: MIME::Types[mime_type].first.to_s)
|
183
|
+
}
|
184
|
+
send_request(:post, 'log', form: form)
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
def send_request(verb, path, options = {})
|
189
|
+
http_client.send_request(verb, path, options)
|
190
|
+
end
|
191
|
+
|
192
|
+
def http_client
|
193
|
+
@http_client ||= HttpClient.new
|
194
|
+
end
|
195
|
+
|
196
|
+
def current_time
|
197
|
+
# `now_without_mock_time` is provided by Timecop and returns a real, not mocked time
|
198
|
+
return Time.now_without_mock_time if Time.respond_to?(:now_without_mock_time)
|
199
|
+
|
200
|
+
Time.now
|
201
|
+
end
|
202
|
+
|
203
|
+
def event_bus
|
204
|
+
@event_bus ||= EventBus.new
|
205
|
+
end
|
206
|
+
|
207
|
+
def prepare_options(data, config = {})
|
208
|
+
if config.attributes
|
209
|
+
data[:attributes] = config.attributes
|
210
|
+
elsif (data[:tags] = config.tags)
|
211
|
+
end
|
212
|
+
data
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|
metadata
ADDED
@@ -0,0 +1,118 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: reportportal_new
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: '0.7'
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Aliaksandr Trush
|
8
|
+
- Sergey Gvozdyukevich
|
9
|
+
- Andrei Botalov
|
10
|
+
autorequire:
|
11
|
+
bindir: bin
|
12
|
+
cert_chain: []
|
13
|
+
date: 2022-01-19 00:00:00.000000000 Z
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: http
|
17
|
+
requirement: !ruby/object:Gem::Requirement
|
18
|
+
requirements:
|
19
|
+
- - "~>"
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '4.0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
requirements:
|
26
|
+
- - "~>"
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
version: '4.0'
|
29
|
+
- !ruby/object:Gem::Dependency
|
30
|
+
name: mime-types
|
31
|
+
requirement: !ruby/object:Gem::Requirement
|
32
|
+
requirements:
|
33
|
+
- - ">="
|
34
|
+
- !ruby/object:Gem::Version
|
35
|
+
version: '0'
|
36
|
+
type: :runtime
|
37
|
+
prerelease: false
|
38
|
+
version_requirements: !ruby/object:Gem::Requirement
|
39
|
+
requirements:
|
40
|
+
- - ">="
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: '0'
|
43
|
+
- !ruby/object:Gem::Dependency
|
44
|
+
name: rubytree
|
45
|
+
requirement: !ruby/object:Gem::Requirement
|
46
|
+
requirements:
|
47
|
+
- - ">="
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: 0.9.3
|
50
|
+
type: :runtime
|
51
|
+
prerelease: false
|
52
|
+
version_requirements: !ruby/object:Gem::Requirement
|
53
|
+
requirements:
|
54
|
+
- - ">="
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
version: 0.9.3
|
57
|
+
- !ruby/object:Gem::Dependency
|
58
|
+
name: rubocop
|
59
|
+
requirement: !ruby/object:Gem::Requirement
|
60
|
+
requirements:
|
61
|
+
- - '='
|
62
|
+
- !ruby/object:Gem::Version
|
63
|
+
version: '0.71'
|
64
|
+
type: :development
|
65
|
+
prerelease: false
|
66
|
+
version_requirements: !ruby/object:Gem::Requirement
|
67
|
+
requirements:
|
68
|
+
- - '='
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
version: '0.71'
|
71
|
+
description: Cucumber and RSpec clients for EPAM ReportPortal system
|
72
|
+
email: dzmitry_humianiuk@epam.com
|
73
|
+
executables: []
|
74
|
+
extensions: []
|
75
|
+
extra_rdoc_files: []
|
76
|
+
files:
|
77
|
+
- README.md
|
78
|
+
- lib/report_portal/cucumber/formatter.rb
|
79
|
+
- lib/report_portal/cucumber/parallel_formatter.rb
|
80
|
+
- lib/report_portal/cucumber/parallel_report.rb
|
81
|
+
- lib/report_portal/cucumber/report.rb
|
82
|
+
- lib/report_portal/event_bus.rb
|
83
|
+
- lib/report_portal/events/prepare_start_item_request.rb
|
84
|
+
- lib/report_portal/http_client.rb
|
85
|
+
- lib/report_portal/logging/log4r_outputter.rb
|
86
|
+
- lib/report_portal/logging/logger.rb
|
87
|
+
- lib/report_portal/logging/logging_appender.rb
|
88
|
+
- lib/report_portal/models/item_search_options.rb
|
89
|
+
- lib/report_portal/models/test_item.rb
|
90
|
+
- lib/report_portal/rspec/formatter.rb
|
91
|
+
- lib/report_portal/settings.rb
|
92
|
+
- lib/report_portal/tasks.rb
|
93
|
+
- lib/report_portal/version.rb
|
94
|
+
- lib/reportportal.rb
|
95
|
+
homepage: https://github.com/reportportal/agent-ruby
|
96
|
+
licenses:
|
97
|
+
- Apache-2.0
|
98
|
+
metadata: {}
|
99
|
+
post_install_message:
|
100
|
+
rdoc_options: []
|
101
|
+
require_paths:
|
102
|
+
- lib
|
103
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
104
|
+
requirements:
|
105
|
+
- - ">="
|
106
|
+
- !ruby/object:Gem::Version
|
107
|
+
version: 2.3.0
|
108
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
109
|
+
requirements:
|
110
|
+
- - ">="
|
111
|
+
- !ruby/object:Gem::Version
|
112
|
+
version: '0'
|
113
|
+
requirements: []
|
114
|
+
rubygems_version: 3.1.2
|
115
|
+
signing_key:
|
116
|
+
specification_version: 4
|
117
|
+
summary: ReportPortal Ruby Client
|
118
|
+
test_files: []
|