qat-reporter-xray 6.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +674 -0
- data/lib/qat/cucumber/core_ext/result.rb +16 -0
- data/lib/qat/cucumber/core_ext.rb +3 -0
- data/lib/qat/formatter/xray/test_ids.rb +96 -0
- data/lib/qat/formatter/xray.rb +186 -0
- data/lib/qat/reporter/xray/config.rb +153 -0
- data/lib/qat/reporter/xray/issue.rb +46 -0
- data/lib/qat/reporter/xray/publisher/base.rb +229 -0
- data/lib/qat/reporter/xray/publisher/cloud.rb +74 -0
- data/lib/qat/reporter/xray/publisher/hosted.rb +56 -0
- data/lib/qat/reporter/xray/publisher.rb +3 -0
- data/lib/qat/reporter/xray/tasks/tests/helpers.rb +112 -0
- data/lib/qat/reporter/xray/tasks/tests/report.rb +47 -0
- data/lib/qat/reporter/xray/tasks/tests.rb +142 -0
- data/lib/qat/reporter/xray/tasks.rb +5 -0
- data/lib/qat/reporter/xray/test.rb +44 -0
- data/lib/qat/reporter/xray/test_execution.rb +79 -0
- data/lib/qat/reporter/xray/version.rb +11 -0
- data/lib/qat/reporter/xray.rb +3 -0
- metadata +193 -0
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'cucumber/core/test/result'
|
2
|
+
|
3
|
+
#Patch for Cucumber::Core::Test::Result::Unknown to implement methods used by the formatters
|
4
|
+
#@since 1.1.0
|
5
|
+
class Cucumber::Core::Test::Result::Unknown
|
6
|
+
|
7
|
+
#Dummy function
|
8
|
+
def with_appended_backtrace(_)
|
9
|
+
''
|
10
|
+
end
|
11
|
+
|
12
|
+
#Dummy function
|
13
|
+
def with_filtered_backtrace(_)
|
14
|
+
''
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
require 'cucumber/formatter/io'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
module QAT
|
5
|
+
module Formatter
|
6
|
+
class Xray
|
7
|
+
class TestIds
|
8
|
+
include Cucumber::Formatter::Io
|
9
|
+
|
10
|
+
def initialize(runtime, path_or_io, options)
|
11
|
+
@io = ensure_io(path_or_io)
|
12
|
+
@tags = []
|
13
|
+
@scenario_tags = []
|
14
|
+
@no_test_id = {}
|
15
|
+
@max_test_id = 0
|
16
|
+
@duplicate_test_ids = {}
|
17
|
+
@test_id_mapping = {}
|
18
|
+
@options = options
|
19
|
+
end
|
20
|
+
|
21
|
+
def before_feature(feature)
|
22
|
+
@in_scenarios = false
|
23
|
+
end
|
24
|
+
|
25
|
+
def tag_name(tag_name)
|
26
|
+
@scenario_tags << tag_name if @in_scenarios
|
27
|
+
end
|
28
|
+
|
29
|
+
def after_tags(tags)
|
30
|
+
@in_scenarios = true unless @in_scenarios
|
31
|
+
end
|
32
|
+
|
33
|
+
def scenario_name(keyword, name, file_colon_line, source_indent)
|
34
|
+
if @scenario_tags.any? { |tag| tag.match(/@id:(\d+)/) }
|
35
|
+
id = @scenario_tags.map { |tag| tag.match(/@id:(\d+)/) }.compact.first.captures.first.to_i
|
36
|
+
@max_test_id = id if id > @max_test_id
|
37
|
+
|
38
|
+
test_id_info = { name: name,
|
39
|
+
path: file_colon_line }
|
40
|
+
|
41
|
+
if @test_id_mapping[id]
|
42
|
+
if @duplicate_test_ids[id]
|
43
|
+
@duplicate_test_ids[id] << test_id_info
|
44
|
+
else
|
45
|
+
@duplicate_test_ids[id] = [@test_id_mapping[id], test_id_info]
|
46
|
+
end
|
47
|
+
else
|
48
|
+
@test_id_mapping[id] = test_id_info
|
49
|
+
end
|
50
|
+
|
51
|
+
else
|
52
|
+
@no_test_id[name] = file_colon_line unless @scenario_tags.include?('@dummy_test')
|
53
|
+
end
|
54
|
+
@scenario_tags = []
|
55
|
+
end
|
56
|
+
|
57
|
+
def after_features(features)
|
58
|
+
publish_result
|
59
|
+
@io.flush
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def publish_result
|
65
|
+
content = {
|
66
|
+
max: @max_test_id,
|
67
|
+
untagged: @no_test_id,
|
68
|
+
mapping: Hash[@test_id_mapping.sort],
|
69
|
+
duplicate: Hash[@duplicate_test_ids.sort]
|
70
|
+
}
|
71
|
+
|
72
|
+
if @duplicate_test_ids.any?
|
73
|
+
dups_info = @duplicate_test_ids.map do |id, dups|
|
74
|
+
text = dups.map { |dup| "Scenario: #{dup[:name]} - #{dup[:path]}" }.join("\n")
|
75
|
+
"TEST ID #{id}:\n#{text}\n"
|
76
|
+
end
|
77
|
+
|
78
|
+
duplicates_info = <<-TXT.gsub(/^\s*/, '')
|
79
|
+
------------------------------------
|
80
|
+
Duplicate test ids found!
|
81
|
+
------------------------------------
|
82
|
+
#{dups_info.join("\n")}
|
83
|
+
TXT
|
84
|
+
puts duplicates_info
|
85
|
+
end
|
86
|
+
|
87
|
+
@io.puts(content.to_json({
|
88
|
+
indent: ' ',
|
89
|
+
space: ' ',
|
90
|
+
object_nl: "\n"
|
91
|
+
}))
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,186 @@
|
|
1
|
+
require 'cucumber/formatter/io'
|
2
|
+
require 'json'
|
3
|
+
require 'fileutils'
|
4
|
+
require 'qat/logger'
|
5
|
+
require 'time'
|
6
|
+
require 'base64'
|
7
|
+
require_relative '../reporter/xray/config'
|
8
|
+
require_relative '../reporter/xray/test_execution'
|
9
|
+
|
10
|
+
module QAT
|
11
|
+
# Namespace for custom Cucumber formatters and helpers.
|
12
|
+
#@since 0.1.0
|
13
|
+
module Formatter
|
14
|
+
# Namespace for Xray formatter
|
15
|
+
#@since 1.0.0
|
16
|
+
class Xray
|
17
|
+
include ::Cucumber::Formatter::Io
|
18
|
+
include QAT::Logger
|
19
|
+
|
20
|
+
#@api private
|
21
|
+
def initialize(runtime, path_or_io, options)
|
22
|
+
@io = ensure_io(path_or_io)
|
23
|
+
@options = options
|
24
|
+
@tests = []
|
25
|
+
end
|
26
|
+
|
27
|
+
#@api private
|
28
|
+
def tag_name(tag_name)
|
29
|
+
@test_jira_id = tag_name.to_s.split('_')[1] if tag_name.match(test_tag_regex)
|
30
|
+
end
|
31
|
+
|
32
|
+
#@api private
|
33
|
+
def before_test_case(test_case)
|
34
|
+
@current_scenario = test_case.source[1]
|
35
|
+
|
36
|
+
@exception = nil
|
37
|
+
|
38
|
+
@start_time = Time.now
|
39
|
+
@evidences = []
|
40
|
+
@file_counter = 0
|
41
|
+
end
|
42
|
+
|
43
|
+
#@api private
|
44
|
+
def after_test_case(_, status)
|
45
|
+
# When jira type is cloud the test result string must be different (accordingly with xray api)
|
46
|
+
test_status = if status.is_a? ::Cucumber::Core::Test::Result::Passed
|
47
|
+
jira_type == 'cloud' ? 'PASSED' : 'PASS'
|
48
|
+
elsif status.is_a? ::Cucumber::Core::Test::Result::Failed
|
49
|
+
jira_type == 'cloud' ? 'FAILED' : 'FAIL'
|
50
|
+
else
|
51
|
+
'NO RUN'
|
52
|
+
end
|
53
|
+
|
54
|
+
@end_time = Time.now
|
55
|
+
|
56
|
+
comment = status.respond_to?(:exception) ? build_exception(status.exception) : ''
|
57
|
+
|
58
|
+
log.warn 'Jira ID is not defined!' unless @test_jira_id
|
59
|
+
if @current_scenario.is_a? ::Cucumber::Core::Ast::ScenarioOutline
|
60
|
+
save_current_scenario_outline(test_status, comment)
|
61
|
+
else
|
62
|
+
save_current_scenario(test_status, comment)
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
|
67
|
+
#@api private
|
68
|
+
def after_features(*_)
|
69
|
+
publish_result
|
70
|
+
end
|
71
|
+
|
72
|
+
def embed(src, mime_type, label)
|
73
|
+
|
74
|
+
|
75
|
+
data = if File.file?(src)
|
76
|
+
File.open(src) do |file|
|
77
|
+
Base64.strict_encode64(file.read)
|
78
|
+
end
|
79
|
+
elsif src =~ /^data:image\/(png|gif|jpg|jpeg);base64,/
|
80
|
+
src
|
81
|
+
else
|
82
|
+
Base64.strict_encode64(src)
|
83
|
+
end
|
84
|
+
|
85
|
+
ext = mime_type.split('/').last
|
86
|
+
|
87
|
+
file_name = if File.file?(src)
|
88
|
+
File.basename(src)
|
89
|
+
elsif label.to_s.empty?
|
90
|
+
"file_#{@file_counter += 1}.#{ext}"
|
91
|
+
else
|
92
|
+
"#{label}.#{ext}"
|
93
|
+
end
|
94
|
+
|
95
|
+
@evidences << { data: data, filename: file_name, contentType: mime_type }
|
96
|
+
end
|
97
|
+
|
98
|
+
private
|
99
|
+
|
100
|
+
def jira_type
|
101
|
+
QAT::Reporter::Xray::Config.jira_type
|
102
|
+
end
|
103
|
+
|
104
|
+
def test_prefix
|
105
|
+
QAT::Reporter::Xray::Config.test_prefix
|
106
|
+
end
|
107
|
+
|
108
|
+
def project_key
|
109
|
+
QAT::Reporter::Xray::Config.project_key
|
110
|
+
end
|
111
|
+
|
112
|
+
def test_tag_regex
|
113
|
+
/@#{test_prefix}(#{project_key}-\d+)/
|
114
|
+
end
|
115
|
+
|
116
|
+
def build_exception(exception)
|
117
|
+
"#{exception.message} (#{exception.class})\n#{exception.backtrace.join("\n")}"
|
118
|
+
end
|
119
|
+
|
120
|
+
def save_current_scenario(status, comment = '')
|
121
|
+
test_info = {
|
122
|
+
testKey: @test_jira_id,
|
123
|
+
start: @start_time.iso8601,
|
124
|
+
finish: @end_time.iso8601,
|
125
|
+
comment: comment,
|
126
|
+
status: status
|
127
|
+
}
|
128
|
+
test_info[:evidences] = @evidences unless @evidences.empty?
|
129
|
+
@tests << test_info
|
130
|
+
end
|
131
|
+
|
132
|
+
def save_current_scenario_outline(status, comment = '')
|
133
|
+
if @tests.any? { |test| test[:testKey] == @test_jira_id }
|
134
|
+
outline = @tests.select { |test| test[:testKey] == @test_jira_id }.first
|
135
|
+
|
136
|
+
outline[:finish] = @end_time.iso8601
|
137
|
+
unless outline[:status] == 'FAIL' || outline[:status] == 'FAILED'
|
138
|
+
outline[:status] = status
|
139
|
+
outline[:comment] = comment
|
140
|
+
end
|
141
|
+
outline[:examples] << status
|
142
|
+
|
143
|
+
@tests.delete_if { |test| test[:testKey] == @test_jira_id }
|
144
|
+
@tests << outline
|
145
|
+
else
|
146
|
+
@tests << {
|
147
|
+
testKey: @test_jira_id,
|
148
|
+
start: @start_time.iso8601,
|
149
|
+
finish: @end_time.iso8601,
|
150
|
+
comment: comment,
|
151
|
+
status: status,
|
152
|
+
examples: [
|
153
|
+
status
|
154
|
+
]
|
155
|
+
}
|
156
|
+
if @tests.is_a? Array
|
157
|
+
@tests.first[:evidences] = @evidences unless @evidences.empty?
|
158
|
+
else
|
159
|
+
@tests[:evidences] = @evidences unless @evidences.empty?
|
160
|
+
end
|
161
|
+
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
# Writes results to a JSON file
|
166
|
+
def publish_result
|
167
|
+
content = {
|
168
|
+
tests: @tests
|
169
|
+
}
|
170
|
+
@io.puts(JSON.pretty_generate(content))
|
171
|
+
|
172
|
+
jira_id = ENV['XRAY_TEST_EXECUTION'].to_s
|
173
|
+
|
174
|
+
if jira_id.empty?
|
175
|
+
log.warn 'Importing Test results to a new Test Execution.'
|
176
|
+
jira_id = nil
|
177
|
+
else
|
178
|
+
log.info "Importing Test results to Test Execution '#{jira_id}'."
|
179
|
+
end
|
180
|
+
|
181
|
+
QAT::Reporter::Xray::TestExecution.new(jira_id).import_execution_results(content)
|
182
|
+
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
@@ -0,0 +1,153 @@
|
|
1
|
+
require_relative 'publisher'
|
2
|
+
|
3
|
+
module QAT
|
4
|
+
module Reporter
|
5
|
+
class Xray
|
6
|
+
# QAT::Reporter::Xray configuration module
|
7
|
+
module Config
|
8
|
+
class << self
|
9
|
+
|
10
|
+
attr_accessor :project_key, :test_prefix, :story_prefix, :jira_url, :xray_default_api_url, :login_credentials, :publisher, :jira_type,
|
11
|
+
:cloud_xray_api_credentials, :xray_test_environment, :xray_test_version, :xray_test_revision, :xray_export_test_keys, :xray_export_test_filter
|
12
|
+
|
13
|
+
# Default xray API url (Jira Cloud)
|
14
|
+
DEFAULT_XRAY_URL = 'https://xray.cloud.xpand-it.com'
|
15
|
+
|
16
|
+
# Default test tag prefix
|
17
|
+
DEFAULT_TEST_PREFIX = 'TEST_'
|
18
|
+
# Default story tag prefix
|
19
|
+
DEFAULT_STORY_PREFIX = 'STORY_'
|
20
|
+
|
21
|
+
# Returns the xray instanced type (hosted or cloud)
|
22
|
+
def jira_type
|
23
|
+
@jira_type
|
24
|
+
end
|
25
|
+
|
26
|
+
# Returns the jira url
|
27
|
+
def jira_url
|
28
|
+
@jira_url
|
29
|
+
end
|
30
|
+
|
31
|
+
# Returns the default xray jira url for cloud api
|
32
|
+
def xray_default_api_url
|
33
|
+
DEFAULT_XRAY_URL
|
34
|
+
end
|
35
|
+
|
36
|
+
# Returns the login credentials array could -> [username, password, apiToken]
|
37
|
+
def login_credentials
|
38
|
+
@login_credentials
|
39
|
+
end
|
40
|
+
|
41
|
+
# Returns the login credentials array for cloud api [client_id, client_secret]
|
42
|
+
def cloud_xray_api_credentials
|
43
|
+
@cloud_xray_api_credentials || nil
|
44
|
+
end
|
45
|
+
|
46
|
+
# Returns the test keys to export
|
47
|
+
def xray_export_test_keys
|
48
|
+
@keys || nil
|
49
|
+
end
|
50
|
+
|
51
|
+
# Returns the test filter to export
|
52
|
+
def xray_export_test_filter
|
53
|
+
@filter || nil
|
54
|
+
end
|
55
|
+
|
56
|
+
# Returns the project key value
|
57
|
+
def project_key
|
58
|
+
@project_key
|
59
|
+
end
|
60
|
+
|
61
|
+
# Returns the test tag prefix value
|
62
|
+
def test_prefix
|
63
|
+
@test_prefix || DEFAULT_TEST_PREFIX
|
64
|
+
end
|
65
|
+
|
66
|
+
# Returns the story tag prefix value
|
67
|
+
def story_prefix
|
68
|
+
@story_prefix || DEFAULT_STORY_PREFIX
|
69
|
+
end
|
70
|
+
|
71
|
+
# Returns the xray test environment value
|
72
|
+
def xray_test_environment
|
73
|
+
@xray_test_environment || get_env_from_qat_config
|
74
|
+
end
|
75
|
+
|
76
|
+
# Returns the xray test version value
|
77
|
+
def xray_test_version
|
78
|
+
@xray_test_version || get_version_from_qat_config
|
79
|
+
end
|
80
|
+
|
81
|
+
# Returns the xray test revision value
|
82
|
+
def xray_test_revision
|
83
|
+
@xray_test_revision || get_revision_from_qat_config
|
84
|
+
end
|
85
|
+
|
86
|
+
def publisher=(publisher)
|
87
|
+
@publisher = publisher
|
88
|
+
end
|
89
|
+
|
90
|
+
def publisher
|
91
|
+
@publisher
|
92
|
+
end
|
93
|
+
|
94
|
+
|
95
|
+
private
|
96
|
+
|
97
|
+
def get_env_from_qat_config
|
98
|
+
begin
|
99
|
+
QAT.configuration.dig(:xray, :environment_name)
|
100
|
+
rescue ArgumentError
|
101
|
+
raise(NoEnvironmentDefined, 'JIRA\'s environment must be defined!')
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def get_version_from_qat_config
|
106
|
+
begin
|
107
|
+
QAT.configuration.dig(:xray, :version)
|
108
|
+
rescue ArgumentError
|
109
|
+
raise(NoVersionDefined, 'JIRA\'s version must be defined!')
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def get_revision_from_qat_config
|
114
|
+
begin
|
115
|
+
QAT.configuration.dig(:xray, :revision)
|
116
|
+
rescue ArgumentError
|
117
|
+
raise(NoRevisionDefined, 'JIRA\'s revision must be defined!')
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
# Error returned when the QAT project has not defined the Jira Environment
|
122
|
+
class NoEnvironmentDefined < StandardError
|
123
|
+
end
|
124
|
+
# Error returned when the QAT project has not defined the Jira Version
|
125
|
+
class NoVersionDefined < StandardError
|
126
|
+
end
|
127
|
+
# Error returned when the QAT project has not defined the Jira Revision
|
128
|
+
class NoRevisionDefined < StandardError
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
class << self
|
134
|
+
# Configures the QAT::Formatter::Xray
|
135
|
+
def configure(&block)
|
136
|
+
yield Config
|
137
|
+
|
138
|
+
QAT::Reporter::Xray::Config.publisher = QAT::Reporter::Xray::Publisher.const_get(QAT::Reporter::Xray::Config.jira_type.capitalize).new
|
139
|
+
|
140
|
+
raise(ProjectKeyUndefinedError, 'JIRA\'s project key must be defined!') unless QAT::Reporter::Xray::Config.project_key
|
141
|
+
raise(LoginCredentialsUndefinedError, 'JIRA\'s login credentials must be defined!') unless QAT::Reporter::Xray::Config.login_credentials
|
142
|
+
end
|
143
|
+
|
144
|
+
# Error returned when the the JIRA project key is not defined
|
145
|
+
class ProjectKeyUndefinedError < StandardError
|
146
|
+
end
|
147
|
+
# Error returned when the the JIRA login credentials is not defined
|
148
|
+
class LoginCredentialsUndefinedError < StandardError
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'rest-client'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
module QAT
|
5
|
+
module Reporter
|
6
|
+
class Xray
|
7
|
+
# QAT::Reporter::Xray::Issue represents an abstract Xray issue
|
8
|
+
class Issue
|
9
|
+
|
10
|
+
attr_reader :jira_id
|
11
|
+
|
12
|
+
# Initializes Xray Publisher url and login information
|
13
|
+
def initialize(jira_id = nil)
|
14
|
+
@jira_id = jira_id
|
15
|
+
if jira_id
|
16
|
+
raise(InvalidIssueType, "The given issue '#{jira_id}' type does not correspond!") unless valid_test_execution?
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# Creates a issue
|
21
|
+
def create(data)
|
22
|
+
QAT::Reporter::Xray::Config.publisher.create_issue(data)
|
23
|
+
end
|
24
|
+
|
25
|
+
# Error returned when the the JIRA Issue does not correspond
|
26
|
+
class InvalidIssueType < StandardError
|
27
|
+
end
|
28
|
+
# Error returned when publisher string is not known
|
29
|
+
class PublisherNotKnownError < StandardError
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def valid_test_execution?
|
35
|
+
base = Publisher::Base.new
|
36
|
+
response = JSON.parse(Publisher::Base::Client.new(base.base_url).get("/rest/api/2/issue/#{jira_id}", base.default_headers))
|
37
|
+
if response.dig('fields', 'issuetype', 'name').eql? 'Test Execution'
|
38
|
+
true
|
39
|
+
else
|
40
|
+
false
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|