reportportal_new 0.7

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,3 @@
1
+ module ReportPortal
2
+ VERSION = '0.7'.freeze
3
+ end
@@ -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: []