lib/hiptest-publisher/client.rb
require 'erb'
require 'i18n'
require 'json'
require 'net/http'
require 'uri'
require_relative 'export_cache'
require_relative 'formatters/reporter'
module Hiptest
class ClientError < StandardError
end
class RedirectionError < StandardError
attr_reader :redirect
def initialize(msg, redirect)
@redirect = redirect
super(msg)
end
end
class AsyncExportUnavailable < StandardError
end
class MaximumRedirectionReachedError < StandardError
end
MAX_REDIRECTION = 10
class Client
attr_reader :cli_options
attr_writer :async_options
def initialize(cli_options, reporter = nil)
@cli_options = cli_options
@reporter = reporter || NullReporter.new
@async_options = { max_attempts: 200, sleep_time_between_attemps: 5 }
end
def url
if cli_options.push?
"#{cli_options.site}/import_test_results/#{cli_options.token}/#{cli_options.push_format}#{push_query_parameters}"
elsif test_run_id
"#{base_publication_path}/test_run/#{test_run_id}#{test_run_export_filter}"
else
"#{base_publication_path}/#{cli_options.leafless_export ? 'leafless_tests' : 'project'}#{project_export_filters}"
end
end
def global_failure_url
"#{cli_options.site}/report_global_failure/#{cli_options.token}/#{cli_options.test_run_id}/"
end
def project_export_filters
mapping = {
filter_on_scenario_ids: 'filter_scenario_ids',
filter_on_folder_ids: 'filter_folder_ids',
filter_on_scenario_name: 'filter_scenario_name',
filter_on_folder_name: 'filter_folder_name',
filter_on_tags: 'filter_tags'
}
options = []
mapping.each do |key, filter_name|
value = @cli_options[key]
next if value.nil? || value.empty?
if [:filter_on_scenario_ids, :filter_on_folder_ids, :filter_on_tags].include?(key)
value = value.split(',').map(&:strip).map{ |s| ERB::Util.url_encode(s) }.join(',')
else
value = ERB::Util.url_encode(value)
end
options << "#{filter_name}=#{value}"
if [:filter_on_folder_ids, :filter_on_folder_name].include?(key) && @cli_options[:not_recursive]
options << "not_recursive=true"
end
end
return options.empty? ? '' : "?#{options.join('&')}"
end
def test_run_export_filter
value = @cli_options.filter_on_status
return '' if value.nil? || value.empty?
return "?filter_status=#{value}"
end
def fetch_project
cached = export_cache.cache_for(url)
unless cached.nil?
@reporter.with_status_message I18n.t(:using_cached_data) do
return cached
end
end
content = @reporter.with_status_message I18n.t(:fetching_data) do
break fetch_project_export if use_synchronous_fetch?
begin
fetch_project_export_asynchronously
rescue AsyncExportUnavailable
fetch_project_export
end
end
export_cache.cache(url, content)
content
end
def available_test_runs
@available_test_runs ||= begin
response = send_get_request("#{base_publication_path}/test_runs")
if response.code_type == Net::HTTPNotFound
:api_not_available
else
json_response = JSON.parse(response.body)
json_response["test_runs"]
end
end
end
def push_results
# Code from: https://github.com/nicksieger/multipart-post
uploaded = {}
Dir.glob(cli_options.push.gsub('\\', '/')).each do |filename|
uploaded["file-#{filename.normalize}"] = filename
end
if cli_options.global_failure_on_missing_reports && uploaded.empty?
return send_post_request(global_failure_url)
end
send_multipart_request(url, uploaded)
end
private
def use_synchronous_fetch?
cli_options.push? || cli_options.leafless_export || test_run_id
end
def fetch_project_export
response = send_get_request(url)
if response.code_type == Net::HTTPNotFound
raise ClientError, I18n.t('errors.project_not_found')
end
response.body
end
def fetch_project_export_asynchronously
publication_export_id = fetch_asynchronous_publication_export_id
url = "#{base_publication_path}/async_project/#{publication_export_id}"
response = nil
# the server should respond with a timeout after 15 minutes
# it is about 180 attempts with a sleep time of 5 seconds between each requests
sleep_time_between_attemps = @async_options[:sleep_time_between_attemps]
max_attempts = @async_options[:max_attempts]
loop do
response = send_get_request(url)
break unless response.code_type == Net::HTTPAccepted
break if 0 >= (max_attempts -= 1)
sleep(sleep_time_between_attemps)
end
response.body
end
def fetch_asynchronous_publication_export_id
url = "#{base_publication_path}/async_project#{project_export_filters}"
response = send_post_request(url)
raise AsyncExportUnavailable if response.code_type == Net::HTTPNotFound
JSON.parse(response.body)['publication_export_id']
end
def export_cache
@export_cache ||= ExportCache.new(
@cli_options.cache_dir,
@cli_options.cache_duration,
reporter: @reporter)
end
def test_run_id
return unless cli_options.test_run_id? || cli_options.test_run_name?
if cli_options.test_run_id?
key = "id"
searched_value = cli_options.test_run_id
elsif cli_options.test_run_name?
key = "name"
searched_value = cli_options.test_run_name
end
if available_test_runs == :api_not_available
if cli_options.test_run_id?
cli_options.test_run_id
else
raise ClientError, I18n.t('errors.test_run_list_unavailable')
end
else
matching_test_run = available_test_runs.find { |test_run| test_run[key] == searched_value }
if matching_test_run.nil?
raise ClientError, no_matching_test_runs_error_message
end
matching_test_run["id"]
end
end
def no_matching_test_runs_error_message
if available_test_runs.empty?
I18n.t('errors.no_test_runs')
else
I18n.t('errors.no_matching_test_run', test_runs: columnize_test_runs(available_test_runs))
end
end
def columnize_test_runs(test_runs)
lines = []
lines << ["ID", "Name"]
lines << ["--", "----"]
lines += test_runs.map { |tr| [tr["id"], tr["name"]] }
first_column_width = lines.map { |line| line[0].length }.max
lines.map! { |line| " #{line[0].ljust(first_column_width)} #{line[1]}" }
lines.join("\n")
end
def base_publication_path
"#{cli_options.site}/publication/#{cli_options.token}"
end
def send_get_request(url, attempt = MAX_REDIRECTION)
raise MaximumRedirectionReachedError if attempt < 0
uri = URI.parse(url)
send_request(Net::HTTP::Get.new(uri))
rescue RedirectionError => err
send_get_request(err.redirect, attempt - 1)
end
def send_post_request(url, attempt = MAX_REDIRECTION)
raise MaximumRedirectionReachedError if attempt < 0
uri = URI.parse(url)
send_request(Net::HTTP::Post.new(uri))
rescue RedirectionError => err
send_post_request(err.redirect, attempt - 1)
end
def send_multipart_request(url, uploaded, attempt = MAX_REDIRECTION)
raise MaximumRedirectionReachedError if attempt < 0
uri = URI.parse(url)
files = {}
uploaded.each do |fieldname, filename|
files[fieldname] = UploadIO.new(File.new(filename), "text", filename)
end
send_request(Net::HTTP::Post::Multipart.new(uri, files))
rescue RedirectionError => err
send_multipart_request(err.redirect, uploaded, attempt - 1)
end
def send_request(request)
request["User-Agent"] = "Ruby/hiptest-publisher"
use_ssl = request.uri.scheme == "https"
proxy_uri = find_proxy_uri(request.uri.hostname, request.uri.port)
if proxy_uri
proxy_address = proxy_uri.hostname
proxy_port = proxy_uri.port
proxy_user, proxy_pass = proxy_uri.userinfo.split(':', 2) if proxy_uri.userinfo
end
Net::HTTP.start(
request.uri.hostname, request.uri.port,
proxy_address, proxy_port, proxy_user, proxy_pass,
use_ssl: use_ssl,
verify_mode: OpenSSL::SSL::VERIFY_NONE) do |http|
@reporter.show_verbose_message(I18n.t(:request_sent, uri: request.uri))
response = http.request(request)
raise RedirectionError.new("Got redirected", response['location']) if response.is_a?(Net::HTTPRedirection)
response
end
end
def find_proxy_uri(address, port)
return URI.parse(@cli_options.http_proxy) if @cli_options.http_proxy
URI::HTTP.new(
"http", nil, address, port, nil, nil, nil, nil, nil
).find_proxy
end
def push_query_parameters
parameters = {}
unless cli_options.execution_environment.strip.empty?
parameters['execution_environment'] = cli_options.execution_environment
end
unless cli_options.build_id.strip.empty?
parameters['build_id'] = cli_options.build_id
end
unless cli_options.build_name.strip.empty?
parameters['build_name'] = cli_options.build_name
end
parameters.empty? ? "" : "?#{parameters.map {|key, value| "#{key}=#{ERB::Util.url_encode(value)}"}.join("&")}"
end
end
end