diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 12dda70..ab64644 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -19,7 +19,7 @@ Metrics/BlockLength: # Offense count: 2 # Configuration parameters: CountComments. Metrics/ClassLength: - Max: 169 + Max: 350 # Offense count: 3 Metrics/CyclomaticComplexity: @@ -33,7 +33,7 @@ Metrics/MethodLength: # Offense count: 1 # Configuration parameters: CountComments. Metrics/ModuleLength: - Max: 139 + Max: 250 # Offense count: 5 Metrics/PerceivedComplexity: @@ -51,8 +51,6 @@ Style/Documentation: - 'spec/**/*' - 'test/**/*' - 'lib/report_portal/cucumber/formatter.rb' - - 'lib/report_portal/cucumber/parallel_formatter.rb' - - 'lib/report_portal/cucumber/parallel_report.rb' - 'lib/report_portal/logging/logger.rb' - 'lib/report_portal/rspec/formatter.rb' - 'lib/report_portal/settings.rb' @@ -90,7 +88,6 @@ Style/GlobalVars: # Configuration parameters: MinBodyLength. Style/GuardClause: Exclude: - - 'lib/report_portal/cucumber/parallel_report.rb' - 'lib/report_portal/cucumber/report.rb' - 'lib/report_portal/rspec/formatter.rb' - 'lib/reportportal.rb' diff --git a/Gemfile b/Gemfile index e45c32c..86a906a 100644 --- a/Gemfile +++ b/Gemfile @@ -3,10 +3,11 @@ source 'https://rubygems.org' gemspec gem 'cucumber', '~> 3' -gem 'parallel_tests' -gem 'rake' -gem 'rspec' -gem 'rubytree', git: 'https://github.com/razboev/RubyTree' - +gem 'faraday' gem 'log4r' gem 'logging' +gem 'parallel_tests', '~> 2.15.0' +gem 'rake' +gem 'rspec' +gem 'rubytree' +gem 'sys-proctable', '~> 1.1.5' diff --git a/README.md b/README.md index b70ddbe..99769b1 100644 --- a/README.md +++ b/README.md @@ -28,12 +28,18 @@ Add `gem 'reportportal', git: 'https://github.com/reportportal/agent-ruby.git'` * With Cucumber: -```cucumber -f ReportPortal::Cucumber::Formatter``` +```cucumber -f ReportPortal::Cucumber::Formatter -o ''``` -* With Cucumber and parallel_tests gem: - -```parallel_cucumber -o ' -f ReportPortal::Cucumber::ParallelFormatter'``` +* With Cucumber (Advanced) +```ruby +AfterConfiguration do |config| + ... + #rp_log_file = + ... + config.formats.push(["ReportPortal::Cucumber::Formatter", {}, rp_log_file]) +end +``` * With RSpec: ```rspec -f ReportPortal::RSpec::Formatter``` @@ -54,6 +60,8 @@ Supported settings: - launch_id - id of previously created launch (to be used if formatter_modes contains attach_to_launch) - file_with_launch_id - path to file with id of launch (to be used if formatter_modes contains attach_to_launch) - 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: + - launch_uuid - when formatter_modes contains `attach_to_launch`, launch_uuid will be used to create uniq report group (tmp dir should be shared across all launches) + - log_level - this is log level for report_portal agent (useful for troubleshooting issues when run in parallel mode) ``` 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 ``` @@ -75,13 +83,37 @@ WebMock.disable_net_connect!(:net_http_connect_on_start => true, :allow_localhos ## Formatter modes The following modes are supported: + + + + + + + + + + + + + + + +
NamePurpose
attach_to_launch +Add executing features/scenarios to same launch. +Use following options are available to configure that. + + 1. launch_id + 2. file_with_launch_id + 3. launch_uuid + 4. rp_launch_id_for_.lock in `Dir.tmpdir` + + *** If launch is not created, cucumber process will create one. Such that user does not need to worry about creating new launch. +
use_same_thread_for_reporting +Send reporting commands in the same main thread used for running tests. This mode is useful for debugging +Report Portal client. It changes default behavior to send commands in the separate thread. +Default behavior is there not to slow test execution.
skip_reporting_hierarchy +Do not create items for folders and feature files
-| Name | Purpose | -| --- | --- | -| 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`) -| 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. | -| skip_reporting_hierarchy | Do not create items for folders and feature files | -| use_persistent_connection | Use persistent connection to connect to the server | ## Logging Experimental support for three common logging frameworks was added: @@ -92,12 +124,6 @@ Experimental support for three common logging frameworks was added: 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. -## Parallel formatter - -ReportPortal::Cucumber::ParallelFormatter can be used for tests started via parallel_tests gem. - -Note: Launch id is shared between independent processes (as is the case with parallel_tests gem) via a file in `Dir.tmpdir`. - ## Links - [ReportPortal](https://github.com/reportportal/) diff --git a/lib/report_portal/client.rb b/lib/report_portal/client.rb new file mode 100644 index 0000000..138eca9 --- /dev/null +++ b/lib/report_portal/client.rb @@ -0,0 +1,52 @@ +module ReportPortal + # @api private + class Client + attr_accessor :logger + + def initialize(logger) + @logger = logger + end + + def process_request(path, method, *options) + tries = 2 + begin + response = rp_client.send(method, path, *options) + rescue Faraday::ClientError => e + logger.error("TRACE[#{e.backtrace}]") + response = JSON.parse(e.response[:body]) + logger.warn("Exception[#{e}], response:[#{response}]], retry_count: [#{tries}]") + m = response['message'].match(%r{Start time of child \['(.+)'\] item should be same or later than start time \['(.+)'\] of the parent item\/launch '.+'}) + case response['error_code'] + when 4001 + return + end + + if m + parent_time = Time.strptime(m[2], '%a %b %d %H:%M:%S %z %Y') + data = JSON.parse(options[0]) + logger.warn("RP error : 40025, time of a child: [#{data['start_time']}], paren time: [#{(parent_time.to_f * 1000).to_i}]") + data['start_time'] = (parent_time.to_f * 1000).to_i + 1000 + options[0] = data.to_json + ReportPortal.last_used_time = data['start_time'] + end + + retry unless (tries -= 1).zero? + end + JSON.parse(response.body) + end + + def rp_client + @connection ||= Faraday.new(url: Settings.instance.project_url) do |f| + f.headers = { Authorization: "Bearer #{Settings.instance.uuid}", Accept: 'application/json', 'Content-type': 'application/json' } + verify_ssl = Settings.instance.disable_ssl_verification + f.ssl.verify = !verify_ssl unless verify_ssl.nil? + f.request :multipart + f.request :url_encoded + f.response :raise_error + f.adapter :net_http_persistent + end + + @connection + end + end +end diff --git a/lib/report_portal/cucumber/formatter.rb b/lib/report_portal/cucumber/formatter.rb index 888292f..0d6a85b 100644 --- a/lib/report_portal/cucumber/formatter.rb +++ b/lib/report_portal/cucumber/formatter.rb @@ -5,12 +5,10 @@ module Cucumber class Formatter # @api private def initialize(config) - ENV['REPORT_PORTAL_USED'] = 'true' - + @logger = Logger.new(config.out_stream) + @logger.level = ReportPortal::Settings.instance.log_level || :warn setup_message_processing - @io = config.out_stream - [:test_case_started, :test_case_finished, :test_step_started, :test_step_finished, :test_run_finished].each do |event_name| config.on_event event_name do |event| process_message(event_name, event) @@ -21,8 +19,6 @@ def initialize(config) def puts(message) process_message(:puts, message) - @io.puts(message) - @io.flush end def embed(*args) @@ -32,7 +28,7 @@ def embed(*args) private def report - @report ||= ReportPortal::Cucumber::Report.new + @report ||= ReportPortal::Cucumber::Report.new(@logger) end def setup_message_processing diff --git a/lib/report_portal/cucumber/parallel_formatter.rb b/lib/report_portal/cucumber/parallel_formatter.rb deleted file mode 100644 index a769685..0000000 --- a/lib/report_portal/cucumber/parallel_formatter.rb +++ /dev/null @@ -1,14 +0,0 @@ -require_relative 'formatter' -require_relative 'parallel_report' - -module ReportPortal - module Cucumber - class ParallelFormatter < Formatter - private - - def report - @report ||= ReportPortal::Cucumber::ParallelReport.new - end - end - end -end diff --git a/lib/report_portal/cucumber/parallel_report.rb b/lib/report_portal/cucumber/parallel_report.rb deleted file mode 100644 index f1648e3..0000000 --- a/lib/report_portal/cucumber/parallel_report.rb +++ /dev/null @@ -1,54 +0,0 @@ -require 'parallel_tests' - -require_relative 'report' - -module ReportPortal - module Cucumber - class ParallelReport < Report - FILE_WITH_LAUNCH_ID = Pathname(Dir.tmpdir) + "parallel_launch_id_for_#{Process.ppid}.lck" - - def parallel? - true - end - - def initialize - @root_node = Tree::TreeNode.new('') - @parent_item_node = @root_node - @last_used_time ||= 0 - - if ParallelTests.first_process? - File.open(FILE_WITH_LAUNCH_ID, 'w') do |f| - f.flock(File::LOCK_EX) - start_launch - f.write(ReportPortal.launch_id) - f.flush - f.flock(File::LOCK_UN) - end - else - File.open(FILE_WITH_LAUNCH_ID, 'r') do |f| - f.flock(File::LOCK_SH) - ReportPortal.launch_id = f.read - f.flock(File::LOCK_UN) - end - end - end - - def test_run_finished(_event, desired_time = ReportPortal.now) - end_feature(desired_time) unless @parent_item_node.is_root? - - if ParallelTests.first_process? - ParallelTests.wait_for_other_processes_to_finish - - File.delete(FILE_WITH_LAUNCH_ID) - - unless attach_to_launch? - $stdout.puts "Finishing launch #{ReportPortal.launch_id}" - ReportPortal.close_child_items(nil) - time_to_send = time_to_send(desired_time) - ReportPortal.finish_launch(time_to_send) - end - end - end - end - end -end diff --git a/lib/report_portal/cucumber/report.rb b/lib/report_portal/cucumber/report.rb index b354851..16642bc 100644 --- a/lib/report_portal/cucumber/report.rb +++ b/lib/report_portal/cucumber/report.rb @@ -1,7 +1,9 @@ -require 'cucumber/formatter/io' -require 'cucumber/formatter/hook_query_visitor' require 'tree' require 'securerandom' +require 'tempfile' +require 'parallel_tests' +require 'sys/proctable' +require 'fileutils' require_relative '../../reportportal' require_relative '../logging/logger' @@ -10,40 +12,89 @@ module ReportPortal module Cucumber # @api private class Report - def parallel? - false - end + attr_accessor :parallel, :started_launch def attach_to_launch? ReportPortal::Settings.instance.formatter_modes.include?('attach_to_launch') end - def initialize - @last_used_time = 0 + def initialize(logger) + @logger = logger + ReportPortal.last_used_time = 0 + ReportPortal.initialize(logger) @root_node = Tree::TreeNode.new('') @parent_item_node = @root_node - start_launch + + set_parallel_tests_vars + + if ParallelTests.first_process? + @logger.debug("First process: #{@pid_of_parallel_tests}") + start_launch(ReportPortal.now) + else + @logger.debug("Child process: #{@pid_of_parallel_tests}") + start_time = monotonic_time + loop do + break if File.exist?(lock_file) + if monotonic_time - start_time > wait_time_for_launch_create + raise "File with launch ID wasn't created after waiting #{wait_time_for_launch_create} seconds" + end + + @logger.debug "File with launch ID wasn't created after waiting #{monotonic_time - start_time} seconds" + + sleep 0.5 + end + ReportPortal.launch_id = read_lock_file(lock_file) + @logger.debug "Attaching to launch using lock_file [#{lock_file}], launch_id: [#{ReportPortal.launch_id}] " + add_process_description + end end - def start_launch(desired_time = ReportPortal.now) + def start_launch(desired_time, cmd_args = ARGV) if attach_to_launch? ReportPortal.launch_id = if ReportPortal::Settings.instance.launch_id ReportPortal::Settings.instance.launch_id else - file_path = ReportPortal::Settings.instance.file_with_launch_id || (Pathname(Dir.tmpdir) + 'rp_launch_id.tmp') - File.read(file_path) + file_path = lock_file + if File.file?(file_path) + read_lock_file(file_path) + else + self.started_launch = true + new_launch(desired_time, cmd_args, file_path) + end end - $stdout.puts "Attaching to launch #{ReportPortal.launch_id}" + @logger.info "Attaching to launch #{ReportPortal.launch_id}" else - description = ReportPortal::Settings.instance.description - description ||= ARGV.map { |arg| arg.gsub(/rp_uuid=.+/, "rp_uuid=[FILTERED]") }.join(' ') - ReportPortal.start_launch(description, time_to_send(desired_time)) + new_launch(desired_time, cmd_args) end end - # TODO: time should be a required argument - def test_case_started(event, desired_time = ReportPortal.now) + def new_launch(desired_time, cmd_args = ARGV, lock_file = nil) + @logger.info("Creating new launch at: [#{desired_time}], with cmd: [#{cmd_args}] and file lock: [#{lock_file}]") + ReportPortal.start_launch(description(cmd_args), time_to_send(desired_time)) + set_file_lock_with_launch_id(lock_file, ReportPortal.launch_id) if lock_file + ReportPortal.launch_id + end + + def description(cmd_args = ARGV) + description ||= ReportPortal::Settings.instance.description + description ||= cmd_args.map { |arg| arg.gsub(/rp_uuid=.+/, "rp_uuid=[FILTERED]") }.join(' ') + description + end + + def set_file_lock_with_launch_id(lock_file, launch_id) + FileUtils.mkdir_p File.dirname(lock_file) + File.open(lock_file, 'w') do |f| + f.flock(File::LOCK_EX) + f.write(launch_id) + f.flush + f.flock(File::LOCK_UN) + end + end + + # scenario starts in separate treads + def test_case_started(event, desired_time) + @logger.debug "test_case_started: [#{event}], " test_case = event.test_case feature = test_case.feature if report_hierarchy? && !same_feature_as_previous_test_case?(feature) @@ -62,7 +113,8 @@ def test_case_started(event, desired_time = ReportPortal.now) ReportPortal.current_scenario.id = ReportPortal.start_item(scenario_node) end - def test_case_finished(event, desired_time = ReportPortal.now) + def test_case_finished(event, desired_time) + @logger.debug "test_case_finished: [#{event}], " result = event.result status = result.to_sym issue = nil @@ -74,7 +126,8 @@ def test_case_finished(event, desired_time = ReportPortal.now) ReportPortal.current_scenario = nil end - def test_step_started(event, desired_time = ReportPortal.now) + def test_step_started(event, desired_time) + @logger.debug "test_step_started: [#{event}], " test_step = event.test_step if step?(test_step) # `after_test_step` is also invoked for hooks step_source = test_step.source.last @@ -88,7 +141,7 @@ def test_step_started(event, desired_time = ReportPortal.now) end end - def test_step_finished(event, desired_time = ReportPortal.now) + def test_step_finished(event, desired_time) test_step = event.test_step result = event.result status = result.to_sym @@ -116,26 +169,108 @@ def test_step_finished(event, desired_time = ReportPortal.now) end end - def test_run_finished(_event, desired_time = ReportPortal.now) + def test_run_finished(_event, desired_time) end_feature(desired_time) unless @parent_item_node.is_root? - - unless attach_to_launch? - close_all_children_of(@root_node) # Folder items are closed here as they can't be closed after finishing a feature - time_to_send = time_to_send(desired_time) - ReportPortal.finish_launch(time_to_send) + @logger.info("Test run finish: [#{@parent_item_node}]") + close_all_children_of(@root_node) # Folder items are closed here as they can't be closed after finishing a feature + if parallel + @logger.debug("Parallel process: #{@pid_of_parallel_tests}") + if ParallelTests.first_process? && started_launch + ParallelTests.wait_for_other_processes_to_finish + File.delete(lock_file) + @logger.info("close launch , delete lock") + complete_launch(desired_time) + end + else + complete_launch(desired_time) end end - def puts(message, desired_time = ReportPortal.now) + def add_process_description + description = ReportPortal.remote_launch['description'].split(' ') + description.push(self.description.split(' ')).flatten! + ReportPortal.update_launch(description: description.uniq.join(' ')) + end + + def puts(message, desired_time) ReportPortal.send_log(:info, message, time_to_send(desired_time)) end - def embed(src, mime_type, label, desired_time = ReportPortal.now) + def embed(src, mime_type, label, desired_time) ReportPortal.send_file(:info, src, label, time_to_send(desired_time), mime_type) end private + def complete_launch(desired_time) + if started_launch || !attach_to_launch? + time_to_send = time_to_send(desired_time) + ReportPortal.finish_launch(time_to_send) + end + end + + def lock_file(file_path = nil) + file_path ||= ReportPortal::Settings.instance.file_with_launch_id + @logger.debug("Lock file (RReportPortal::Settings.instance.file_with_launch_id): #{file_path}") if file_path + file_path ||= Dir.tmpdir + "/report_portal_#{ReportPortal::Settings.instance.launch_uuid}.lock" if ReportPortal::Settings.instance.launch_uuid + @logger.debug("Lock file (ReportPortal::Settings.instance.launch_uuid): #{file_path}") if file_path + file_path ||= Dir.tmpdir + "/rp_launch_id_for_#{@pid_of_parallel_tests}.lock" if @pid_of_parallel_tests + @logger.debug("Lock file (/rp_launch_id_for_#{@pid_of_parallel_tests}.lock): #{file_path}") if file_path + + file_path + end + + def set_parallel_tests_vars + process_list = Sys::ProcTable.ps + @logger.debug("set_test_variables: #{process_list}") + runner_process ||= get_parallel_test_process(process_list) + @logger.debug("Parallel cucumber runner pid: #{runner_process.pid}") if runner_process + runner_process ||= get_cucumber_test_process(process_list) + @logger.debug("Cucumber runner pid: #{runner_process.pid}") if runner_process + raise 'Failed to find any cucumber related test process' if runner_process.nil? + + @pid_of_parallel_tests = runner_process.pid + end + + def get_parallel_test_process(process_list) + process_list.each do |process| + next unless process.cmdline.match(%r{bin(?:\/|\\)parallel_(?:cucumber|test)(.+)}) + + @parallel = true + @logger.debug("get_parallel_test_process: #{process.cmdline}") + return process + end + nil + end + + def get_cucumber_test_process(process_list) + process_list.each do |process| + if process.cmdline.match(%r{bin(?:\/|\\)(?:cucumber)(.+)}) + @logger.debug("get_cucumber_test_process: #{process.cmdline}") + return process + end + end + nil + end + + def wait_time_for_launch_create + ENV['rp_parallel_launch_wait_time'] || 60 + end + + def monotonic_time + Process.clock_gettime(Process::CLOCK_MONOTONIC) + end + + def read_lock_file(file_path) + content = nil + File.open(file_path, 'r') do |f| + f.flock(File::LOCK_SH) + content = File.read(file_path) + f.flock(File::LOCK_UN) + end + content + end + # Report Portal sorts logs by time. However, several logs might have the same time. # So to get Report Portal sort them properly the time should be different in all logs related to the same item. # And thus it should be stored. @@ -144,10 +279,10 @@ def embed(src, mime_type, label, desired_time = ReportPortal.now) # * that process/thread can't start the next test until it's done with the previous one def time_to_send(desired_time) time_to_send = desired_time - if time_to_send <= @last_used_time - time_to_send = @last_used_time + 1 + if time_to_send <= ReportPortal.last_used_time + time_to_send = ReportPortal.last_used_time + 1 end - @last_used_time = time_to_send + ReportPortal.last_used_time = time_to_send end def same_feature_as_previous_test_case?(feature) @@ -155,6 +290,7 @@ def same_feature_as_previous_test_case?(feature) end def start_feature_with_parentage(feature, desired_time) + @logger.debug("start_feature_with_parentage: [#{feature}], [#{desired_time}]") parent_node = @root_node child_node = nil path_components = feature.location.file.split(File::SEPARATOR) @@ -173,7 +309,7 @@ def start_feature_with_parentage(feature, desired_time) type = :TEST end # TODO: multithreading # Parallel formatter always executes scenarios inside the same feature in the same process - if parallel? && + if parallel && index < path_components.size - 1 && # is folder? (id_of_created_item = ReportPortal.item_id_of(name, parent_node)) # get id for folder from report portal # get child id from other process @@ -201,9 +337,22 @@ def end_feature(desired_time) end def close_all_children_of(root_node) + @logger.debug("close_all_children_of: [#{root_node.children}]") root_node.postordered_each do |node| - if !node.is_root? && !node.content.closed - ReportPortal.finish_item(node.content) + @logger.debug("close_all_children_of:postordered_each [#{node.content}]") + unless node.is_root? || node.content.closed + begin + item = ReportPortal.remote_item(node.content[:id]) + @logger.debug("started_launch?: [#{started_launch}], item details: [#{item}]") + if item.key?('end_time') + started_launch ? @logger.warn("Main process: item already closed skipping.") : ReportPortal.finish_item(node.content) + elsif started_launch + ReportPortal.close_child_items(node.content[:id]) + ReportPortal.finish_item(node.content) + else + @logger.warn("Child process: item in use cannot close it. [#{item}]") + end + end end end end diff --git a/lib/report_portal/patches/faraday.rb b/lib/report_portal/patches/faraday.rb new file mode 100644 index 0000000..2ce6c86 --- /dev/null +++ b/lib/report_portal/patches/faraday.rb @@ -0,0 +1,25 @@ +module Faraday + class Request + # Middleware for supporting multi-part requests. + class Multipart + def create_multipart(env, params) + boundary = env.request.boundary + parts = process_params(params) do |key, value| + if begin + JSON.parse(value) + rescue StandardError + false + end + Faraday::Parts::Part.new(boundary, key, value, 'Content-Type' => 'application/json') + else + Faraday::Parts::Part.new(boundary, key, value) + end + end + parts << Faraday::Parts::EpiloguePart.new(boundary) + body = Faraday::CompositeReadIO.new(parts) + env.request_headers[Faraday::Env::ContentLength] = body.length.to_s + body + end + end + end +end diff --git a/lib/report_portal/settings.rb b/lib/report_portal/settings.rb index 3013e74..cced746 100644 --- a/lib/report_portal/settings.rb +++ b/lib/report_portal/settings.rb @@ -24,10 +24,11 @@ def initialize 'tags' => false, 'is_debug' => false, 'disable_ssl_verification' => false, - # for parallel execution only 'use_standard_logger' => false, 'launch_id' => false, 'file_with_launch_id' => false, + 'launch_uuid' => false, + 'log_level' => false } keys.each do |key, is_required| diff --git a/lib/report_portal/version.rb b/lib/report_portal/version.rb index 3d4011d..13ac7d5 100644 --- a/lib/report_portal/version.rb +++ b/lib/report_portal/version.rb @@ -1,3 +1,3 @@ module ReportPortal - VERSION = '0.7' + VERSION = '1.0' end diff --git a/lib/reportportal.rb b/lib/reportportal.rb index f155b4d..8f911a3 100644 --- a/lib/reportportal.rb +++ b/lib/reportportal.rb @@ -1,19 +1,27 @@ require 'cgi' require 'http' require 'json' +require 'uri' require 'pathname' require 'tempfile' -require 'uri' +require 'faraday' +require_relative 'report_portal/patches/faraday' require_relative 'report_portal/settings' -require_relative 'report_portal/http_client' +require_relative 'report_portal/client' + module ReportPortal TestItem = Struct.new(:name, :type, :id, :start_time, :description, :closed, :tags) LOG_LEVELS = { error: 'ERROR', warn: 'WARN', info: 'INFO', debug: 'DEBUG', trace: 'TRACE', fatal: 'FATAL', unknown: 'UNKNOWN' } - class << self - attr_accessor :launch_id, :current_scenario + attr_accessor :launch_id, :current_scenario, :last_used_time, :logger + + def initialize(logger) + @logger = logger + @client = ReportPortal::Client.new(logger) + end + def now (Time.now.to_f * 1000).to_i @@ -34,25 +42,36 @@ def status_to_level(status) def start_launch(description, start_time = now) data = { name: Settings.instance.launch, start_time: start_time, tags: Settings.instance.tags, description: description, mode: Settings.instance.launch_mode } - @launch_id = send_request(:post, 'launch', json: data)['id'] + @launch_id = @client.process_request('launch', :post, data.to_json)['id'] + end + + def remote_launch + @client.process_request("launch/#{@launch_id}", :get) + end + + def update_launch(data) + @client.process_request("launch/#{@launch_id}/update", :put, data.to_json) end def finish_launch(end_time = now) + logger.debug "finish_launch: [#{end_time}]" data = { end_time: end_time } - send_request(:put, "launch/#{@launch_id}/finish", json: data) + @client.process_request("launch/#{@launch_id}/finish", :put, data.to_json) end def start_item(item_node) - path = 'item' - path += "/#{item_node.parent.content.id}" unless item_node.parent && item_node.parent.is_root? item = item_node.content data = { start_time: item.start_time, name: item.name[0, 255], type: item.type.to_s, launch_id: @launch_id, description: item.description } data[:tags] = item.tags unless item.tags.empty? - send_request(:post, path, json: data)['id'] + url = 'item' + url += "/#{item_node.parent.content.id}" unless item_node.parent && item_node.parent.is_root? + @client.process_request(url, :post, data.to_json)['id'] end def finish_item(item, status = nil, end_time = nil, force_issue = nil) - unless item.nil? || item.id.nil? || item.closed + if item.nil? || item.id.nil? || item.closed + logger.debug 'finish_item: Item details are missing or already closed' + else data = { end_time: end_time.nil? ? now : end_time } data[:status] = status unless status.nil? if force_issue && status != :passed # TODO: check for :passed status is probably not needed @@ -60,7 +79,14 @@ def finish_item(item, status = nil, end_time = nil, force_issue = nil) elsif status == :skipped data[:issue] = { issue_type: 'NOT_ISSUE' } end - send_request(:put, "item/#{item.id}", json: data) + logger.debug "finish_item:id[#{item}], data: #{data} " + begin + response = @client.process_request("item/#{item.id}", :put, data.to_json) + logger.debug "finish_item: response [#{response}] " + rescue RestClient::Exception => e + response = JSON.parse(e.response) + raise e unless response['error_code'] == 40_018 + end item.closed = true end end @@ -68,9 +94,10 @@ def finish_item(item, status = nil, end_time = nil, force_issue = nil) # TODO: implement force finish def send_log(status, message, time) + @logger.debug "send_log: [#{status}],[#{message}], #{@current_scenario} " unless @current_scenario.nil? || @current_scenario.closed # it can be nil if scenario outline in expand mode is executed data = { item_id: @current_scenario.id, time: time, level: status_to_level(status), message: message.to_s } - send_request(:post, 'log', json: data) + @client.process_request('log', :post, data.to_json) end end @@ -87,42 +114,53 @@ def send_file(status, path, label = nil, time = now, mime_type = 'image/png') temp.rewind path = temp end - File.open(File.realpath(path), 'rb') do |file| - filename = File.basename(file) - json = [{ level: status_to_level(status), message: label || filename, item_id: @current_scenario.id, time: time, file: { name: filename } }] - form = { - json_request_part: HTTP::FormData::Part.new(JSON.dump(json), content_type: 'application/json'), - binary_part: HTTP::FormData::File.new(file, filename: filename) - } - send_request(:post, 'log', form: form) + file_name = File.basename(path) + label ||= file_name + json = { level: status_to_level(status), + message: label, + item_id: @current_scenario.id, + time: time, + file: { name: file_name.to_s }, + "Content-Type": 'application/json' } + payload = { 'json_request_part': [json].to_json, + file_name => Faraday::UploadIO.new(path, mime_type) } + @client.process_request('log', :post, payload, content_type: 'multipart/form-data') + end + + def get_item(name, parent_node) + if parent_node.is_root? # folder without parent folder + url = "item?filter.eq.launch=#{@launch_id}&filter.eq.name=#{URI.escape(name)}&filter.size.path=0" + else + url = "item?filter.eq.launch=#{@launch_id}&filter.eq.parent=#{parent_node.content.id}&filter.eq.name=#{URI.escape(name)}" end + @client.process_request(url, :get) + end + + def remote_item(item_id) + @client.process_request("item/#{item_id}", :get) end - # needed for parallel formatter def item_id_of(name, parent_node) - path = if parent_node.is_root? # folder without parent folder - "item?filter.eq.launch=#{@launch_id}&filter.eq.name=#{CGI.escape(name)}&filter.size.path=0" - else - "item?filter.eq.parent=#{parent_node.content.id}&filter.eq.name=#{CGI.escape(name)}" - end - data = send_request(:get, path) + data = get_item(name, parent_node) if data.key? 'content' data['content'].empty? ? nil : data['content'][0]['id'] + else + nil # item isn't started yet end end - # needed for parallel formatter def close_child_items(parent_id) - path = if parent_id.nil? - "item?filter.eq.launch=#{@launch_id}&filter.size.path=0&page.page=1&page.size=100" - else - "item?filter.eq.parent=#{parent_id}&page.page=1&page.size=100" - end + logger.debug "closing child items: #{parent_id} " + if parent_id.nil? + url = "item?filter.eq.launch=#{@launch_id}&filter.size.path=0&page.page=1&page.size=100" + else + url = "item?filter.eq.launch=#{@launch_id}&filter.eq.parent=#{parent_id}&page.page=1&page.size=100" + end ids = [] loop do - data = send_request(:get, path) - if data.key?('links') - link = data['links'].find { |i| i['rel'] == 'next' } + response = @client.process_request(url, :get) + if response.key?('links') + link = response['links'].find { |i| i['rel'] == 'next' } url = link.nil? ? nil : link['href'] else url = nil @@ -139,15 +177,5 @@ def close_child_items(parent_id) finish_item(TestItem.new(nil, nil, id, nil, nil, nil, nil)) end end - - private - - def send_request(verb, path, options = {}) - http_client.send_request(verb, path, options) - end - - def http_client - @http_client ||= HttpClient.new - end end end diff --git a/reportportal.gemspec b/reportportal.gemspec index fd6d6a6..a9070a5 100644 --- a/reportportal.gemspec +++ b/reportportal.gemspec @@ -14,8 +14,11 @@ Gem::Specification.new do |s| s.required_ruby_version = '>= 2.3.0' s.license = 'Apache-2.0' - s.add_dependency('http', '~> 4.0') - s.add_dependency('rubytree', '>=0.9.3') + s.add_dependency('net-http-persistent', '~> 3.0') + s.add_runtime_dependency('faraday', '~> 0.15') + s.add_runtime_dependency('parallel_tests', '~> 2.15') + s.add_runtime_dependency('rubytree', '~> 1.0') + s.add_runtime_dependency('sys-proctable', '1.1.5') s.add_development_dependency('rubocop', '0.71') end