telly 0.1.2 → 0.2.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.
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ NTdhZmFlMWNiNjY5NjhkOTMxZWM4ODk4MTUxOGViMjBkMjNiZjAwYw==
5
+ data.tar.gz: !binary |-
6
+ MTkxMjViZTE4ODI4OWJmYmIzNmRkNjFiODFjZTFiYzg4Nzc2N2Q4MQ==
7
+ SHA512:
8
+ metadata.gz: !binary |-
9
+ YzVjYmY5OWIxZTIyOGUwMjk3YzU1MDJlMDYxNDFmNGNiMzNkOWUxOTNkZmE3
10
+ ODZkN2Q5Y2UyZjM0MzQ1Zjc1MTM1YTY3MzNhZDZhOWE4OTYyYmZiZjAyNzk0
11
+ NjlmNjlmYWJhMTBhZWQxZjY0MmY2MGExMTY1YzA3YzIwYmQwM2U=
12
+ data.tar.gz: !binary |-
13
+ ZDc0MGI0ZDJmNzg3NDJiYzIwNTE2MWMwODlkYjIxNTlhYjhjYTk2MzQwMDQ0
14
+ Y2E5MGUyOWFmYTM2NTEyNWViMzBlMmE2NjM1NTRjNmQyYjQ2ZGQ4NTU4MzY2
15
+ Yjk4OTBkYTNkNDdiMjA4MjkyZDdjYWE3YTFjOTI1ZDM5YmZlMjQ=
data/bin/telly CHANGED
@@ -16,42 +16,5 @@
16
16
  # and a test status.""
17
17
 
18
18
  require 'telly'
19
- require 'optparse'
20
19
 
21
- # Parses the command line options
22
- # @return [Void]
23
- def parse_opts
24
- options_hash = {}
25
-
26
- optparse = OptionParser.new do|parser|
27
- options_hash = {
28
- testrun_id: nil,
29
- junit_file: nil,
30
- }
31
-
32
- parser.on( '-t', '--testrun-id TESTRUN_ID', 'The testrun id' ) do |testrun_id|
33
- options_hash[:testrun_id] = testrun_id
34
- end
35
-
36
- parser.on( '-j', '--junit-folder JUNIT_FILE', 'Beaker junit file' ) do |junit_file|
37
- options_hash[:junit_file] = junit_file
38
- end
39
-
40
- parser.on( '-h', '--help', 'Display this screen' ) do
41
- puts parser
42
- exit
43
- end
44
-
45
- parser.parse!
46
-
47
- if not options_hash[:testrun_id] or not options_hash[:junit_file]
48
- puts "Error: Missing option(s)"
49
- puts parser
50
- exit
51
- end
52
- end
53
-
54
- options_hash
55
- end
56
-
57
- Telly::main(parse_opts)
20
+ Telly::TestRailTeller.new.send_to_testrail
@@ -1,297 +1,14 @@
1
- #!/usr/bin/env ruby
1
+ require 'rubygems' unless defined?(Gem)
2
2
 
3
- require 'nokogiri'
4
- require 'yaml'
5
- require 'pp'
6
- require 'testrail'
7
3
 
8
-
9
- # == telly.rb
10
- # This module provides functions to add test results in testrail from a
11
- # finished beaker run.
12
- #
13
- # It takes in a beaker junit file and a TestRail Testrun ID
14
- #
15
- # It matches the beaker tests with TestRail testcases by looking for the
16
- # test case ID in the beaker script. The combination of a test run and a test case
17
- # allows the script to add a result for a particular instance of a test case.
18
- # In TestRail parlance, this is confusingly called a test.
19
- # From the TestRail API docs:
20
- # "In TestRail, tests are part of a test run and the test cases are part of the
21
- # related test suite. So, when you create a new test run, TestRail creates a test
22
- # for each test case found in the test suite of the run. You can therefore think
23
- # of a test as an “instance” of a test case which can have test results, comments
24
- # and a test status.""
25
4
  module Telly
26
5
 
27
- # This exception is thrown when a testcase ID can't be found in a beaker
28
- # script. It's meant to be caught and shown to the user as a warning
29
- class MissingTestRailId < StandardError
30
- end
31
-
32
- TESTRAIL_URL = 'https://testrail.ops.puppetlabs.net/'
33
- CREDENTIALS_FILE = '~/.testrail_credentials.yaml'
34
-
35
- # Used for extracted the test case ID from beaker scripts
36
- TESTCASE_ID_REGEX = /.*(?<jira_ticket>\w+-\d+).*[cC](?<testrun_id>\d+)/
37
-
38
- # Testrail Status IDs
39
- PASSED = 1
40
- BLOCKED = 2
41
- FAILED = 5
42
-
43
- def do_stub_test(credentials)
44
- api = get_testrail_api(credentials)
45
-
46
- api.send_post
47
- end
48
-
49
-
50
- ##################################
51
- # Main
52
- ##################################
53
-
54
- # Run the importer
55
- #
56
- # @param [Hash] An optparse object
57
- #
58
- # @return [Void]
59
- #
60
- # @example password = Telly::main(parse_opts)
61
- def Telly.main(options)
62
- # Get pass/fail/skips from junit file
63
- results = load_junit_results(options[:junit_file])
64
-
65
- puts "Run results:"
66
- puts "#{results[:passes].length} Passing"
67
- puts "#{results[:failures].length} Failing or Erroring"
68
- puts "#{results[:skips].length} Skipped"
69
-
70
- # Set results in testrail
71
- bad_results = set_testrail_results(results, options[:junit_file], options[:testrun_id])
72
-
73
- # Print error messages
74
- if not bad_results.empty?
75
- puts "Error: There were problems processing these test scripts:"
76
- bad_results.each do |test_script, error|
77
- puts "#{test_script}:\n\t#{error}"
78
- end
79
- end
80
- end
81
-
82
-
83
- ##################################
84
- # TestRail API
85
- ##################################
86
-
87
- # Load testrail credentials from file
88
- #
89
- # @return [Hash] Contains testrail_username and testrail_password
90
- #
91
- # @example password = load_credentials()["testrail_password"]
92
- def Telly.load_credentials(credentials_file)
6
+ %w( test_rail_teller test_rail version arg_parser ).each do |lib|
93
7
  begin
94
- YAML.load_file(File.expand_path(credentials_file))
95
- rescue
96
- puts "Error: Could not find #{credentials_file}"
97
- puts "Create #{credentials_file} with the following:"
98
- puts "testrail_username: your.username\ntestrail_password: yourpassword"
99
-
100
- exit
101
-
102
- end
103
- end
104
-
105
-
106
- # Returns a testrail API object that talks to testrail
107
- #
108
- # @param [Hash] credentials A hash containing at least two keys, testrail_username and testrail_password
109
- #
110
- # @return [TestRail::APIClient] The API object for talking to TestRail
111
- #
112
- # @example api = get_testrail_api(load_credentials)
113
- def Telly.get_testrail_api(credentials)
114
- client = TestRail::APIClient.new(TESTRAIL_URL)
115
- client.user = credentials["testrail_username"]
116
- client.password = credentials["testrail_password"]
117
-
118
- return client
119
- end
120
-
121
- # Sets the results in testrail.
122
- # Tests that have testrail API exceptions are kept track of in bad_results
123
- #
124
- # @param [Hash] results A hash of lists of xml objects from the junit output file.
125
- # @param [String] junit_file The path to the junit xml file
126
- # Needed for determining the path of the test file in add_failure, etc
127
- # @param [String] testrun_id The TestRail test run ID
128
- #
129
- # @return [Void]
130
- #
131
- def Telly.set_testrail_results(results, junit_file, testrun_id)
132
- credentials = load_credentials(CREDENTIALS_FILE)
133
- api = get_testrail_api(credentials)
134
-
135
- # Results that couldn't be set in testrail for some reason
136
- bad_results = {}
137
-
138
- # passes
139
- results[:passes].each do |junit_result|
140
- begin
141
- submit_result(api, PASSED, junit_result, junit_file, testrun_id)
142
- rescue MissingTestRailId, TestRail::APIError => e
143
- bad_results[junit_result[:name]] = e.message
144
- end
145
- end
146
-
147
- # Failures
148
- results[:failures].each do |junit_result|
149
- begin
150
- submit_result(api, FAILED, junit_result, junit_file, testrun_id)
151
- rescue MissingTestRailId, TestRail::APIError => e
152
- bad_results[junit_result[:name]] = e.message
153
- end
8
+ require "telly/#{lib}"
9
+ rescue LoadError
10
+ require File.expand_path(File.join(File.dirname(__FILE__), 'telly', lib))
154
11
  end
155
-
156
- # Skips
157
- results[:skips].each do |junit_result|
158
- begin
159
- submit_result(api, BLOCKED, junit_result, junit_file, testrun_id)
160
- rescue MissingTestRailId, TestRail::APIError => e
161
- bad_results[junit_result[:name]] = e.message
162
- end
163
- end
164
-
165
- return bad_results
166
- end
167
-
168
- # Submits a test result to TestRail
169
- #
170
- # @param [TestRail::APIClient] api TestRail API object
171
- # @param [int] status The testrail status to set
172
- # @param [Nokogiri::XML::Element] junit_result The nokogiri node that holds the junit result
173
- # @param [String] junit_file Path to the junit file the test result originated from
174
- # @param [String] testrun_id The testrun ID
175
- #
176
- # @return [Void]
177
- #
178
- # @raise [TestRail::APIError] When there is a problem with the API request, testrail raises
179
- # this exception. Should be caught for error reporting
180
- #
181
- # @example submit_result(api, BLOCKED, junit_result, junit_file, testrun_id)
182
- def Telly.submit_result(api, status, junit_result, junit_file, testrun_id)
183
- test_file_path = beaker_test_path(junit_file, junit_result)
184
-
185
- puts junit_result.class
186
- testcase_id = testcase_id_from_beaker_script(test_file_path)
187
-
188
- time_elapsed = make_testrail_time(junit_result[:time])
189
-
190
- # Make appropriate comment for testrail
191
- case status
192
- when FAILED
193
- error_message = junit_result.xpath('./failure').first[:message]
194
- testrail_comment = "Failed with message:\n#{error_message}"
195
- when BLOCKED
196
- skip_message = junit_result.xpath('system-out').first.text
197
- testrail_comment = "Skipped with message:\n#{skip_message}"
198
- else
199
- testrail_comment = "Passed"
200
- end
201
-
202
- puts "\nSetting result for test case: #{testcase_id}"
203
- puts "Adding comment:\n#{testrail_comment}"
204
-
205
- api.send_post("add_result_for_case/#{testrun_id}/#{testcase_id}",
206
- {
207
- status_id: status,
208
- comment: testrail_comment,
209
- elapsed: time_elapsed,
210
- }
211
- )
212
- end
213
-
214
-
215
- # Returns a string that testrail accepts as an elapsed time
216
- # Input from beaker is a float in seconds, so it rounds it to the
217
- # nearest second, and adds an 's' at the end
218
- #
219
- # Testrail throws an exception if it gets "0s", so it returns a
220
- # minimum of "1s"
221
- #
222
- # @param [String] seconds_string A string that contains only a number, the elapsed time of a test
223
- #
224
- # @return [String] The elapsed time of the test run, rounded and with an 's' appended
225
- #
226
- # @example puts make_testrail_time("2.34") # "2s"
227
- def Telly.make_testrail_time(seconds_string)
228
- # If time is 0, make it 1
229
- rounded_time = [seconds_string.to_f.round, 1].max
230
- # Test duration
231
- time_elapsed = "#{rounded_time}s"
232
-
233
- return time_elapsed
234
- end
235
-
236
-
237
- ##################################
238
- # Junit and Beaker file functions
239
- ##################################
240
-
241
- # Loads the results of a beaker run.
242
- # Returns hash of failures, passes, and skips that each hold a list of
243
- # junit xml objects
244
- #
245
- # @param [String] junit_file Path to a junit xml file
246
- #
247
- # @return [Hash] A hash containing xml objects for the failures, skips, and passes
248
- #
249
- # @example load_junit_results("~/junit/latest/beaker_junit.xml")
250
- def Telly.load_junit_results(junit_file)
251
- junit_doc = Nokogiri::XML(File.read(junit_file))
252
-
253
- failures = junit_doc.xpath('//testcase[failure]')
254
- skips = junit_doc.xpath('//testcase[skip]')
255
- passes = junit_doc.xpath('//testcase[not(failure) and not(skip)]')
256
-
257
- return {failures: failures, skips: skips, passes: passes}
258
- end
259
-
260
-
261
- # Extracts the test case id from the test script
262
- #
263
- # @param [String] beaker_file Path to a beaker test script
264
- #
265
- # @return [String] The test case ID
266
- #
267
- # @example testcase_id_from_beaker_script("~/tests/test_the_things.rb") # 1234
268
- def Telly.testcase_id_from_beaker_script(beaker_file)
269
- # Find first matching line
270
- match = File.readlines(beaker_file).map { |line| line.match(TESTCASE_ID_REGEX) }.compact.first
271
-
272
- raise MissingTestRailId, 'Testcase ID could not be found in file' if match.nil?
273
-
274
- match[:testrun_id]
275
- end
276
-
277
-
278
- # Calculates the path to a beaker test file by combining the junit file path
279
- # with the test name from the junit results.
280
- # Makes the assumption that junit folder that beaker creates will always be
281
- # 2 directories up from the beaker script base directory.
282
- # TODO somewhat hacky, maybe a config/command line option
283
- #
284
- # @param [String] junit_file_path Path to a junit xml file
285
- # @param [String] junit_result Path to a junit xml file
286
- #
287
- # @return [String] The path to the beaker script from the junit test result
288
- #
289
- # @example load_junit_results("~/junit/latest/beaker_junit.xml")
290
- def Telly.beaker_test_path(junit_file_path, junit_result)
291
- beaker_folder_path = junit_result[:classname]
292
- test_filename = junit_result[:name]
293
-
294
- File.join(File.dirname(junit_file_path), "../../", beaker_folder_path, test_filename)
295
12
  end
296
13
 
297
14
  end
@@ -0,0 +1,64 @@
1
+ require 'optparse'
2
+
3
+ module Telly
4
+ class ArgParser
5
+
6
+ VERSION_STRING =
7
+ "
8
+ __o_____
9
+ ()/O\\___()
10
+ `-\\\\---' TELLY
11
+ %s!
12
+ __\\/__
13
+ | .... |
14
+ | .... |
15
+ ------
16
+ "
17
+
18
+ # Parses the command line options
19
+ # @return [Void]
20
+ def self.parse_opts
21
+ options_hash = {}
22
+
23
+ optparse = OptionParser.new do|parser|
24
+ options_hash = {
25
+ testrun_id: nil,
26
+ junit_file: nil,
27
+ }
28
+
29
+ parser.on( '-t', '--testrun-id TESTRUN_ID', 'The testrun id' ) do |testrun_id|
30
+ options_hash[:testrun_id] = testrun_id
31
+ end
32
+
33
+ parser.on( '-j', '--junit-folder JUNIT_FILE', 'Beaker junit file' ) do |junit_file|
34
+ options_hash[:junit_file] = junit_file
35
+ end
36
+
37
+ parser.on( '-d', '--dry-run', 'Run without API connection to TestRail' ) do
38
+ options_hash[:dry_run] = true
39
+ end
40
+
41
+ parser.on( '-h', '--help', 'Display this screen' ) do
42
+ puts parser
43
+ exit
44
+ end
45
+
46
+ parser.on( '-v', '--version', 'Report the current version number' ) do
47
+ puts VERSION_STRING % Telly::Version::STRING
48
+ exit
49
+ end
50
+
51
+
52
+ parser.parse!
53
+
54
+ if not options_hash[:testrun_id] or not options_hash[:junit_file]
55
+ puts "Error: Missing option(s)"
56
+ puts parser
57
+ exit
58
+ end
59
+ end
60
+
61
+ options_hash
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,109 @@
1
+ #
2
+ # TestRail API binding for Ruby (API v2, available since TestRail 3.0)
3
+ #
4
+ # Learn more:
5
+ #
6
+ # http://docs.gurock.com/testrail-api2/start
7
+ # http://docs.gurock.com/testrail-api2/accessing
8
+ #
9
+ # Copyright Gurock Software GmbH. See license.md for details.
10
+ #
11
+
12
+ require 'net/http'
13
+ require 'net/https'
14
+ require 'uri'
15
+ require 'json'
16
+
17
+ module Telly
18
+ module TestRail
19
+ class APIClient
20
+ @url = ''
21
+ @user = ''
22
+ @password = ''
23
+
24
+ attr_accessor :user
25
+ attr_accessor :password
26
+
27
+ def initialize(base_url)
28
+ if !base_url.match(/\/$/)
29
+ base_url += '/'
30
+ end
31
+ @url = base_url + 'index.php?/api/v2/'
32
+ end
33
+
34
+ #
35
+ # Send Get
36
+ #
37
+ # Issues a GET request (read) against the API and returns the result
38
+ # (as Ruby hash).
39
+ #
40
+ # Arguments:
41
+ #
42
+ # uri The API method to call including parameters
43
+ # (e.g. get_case/1)
44
+ #
45
+ def send_get(uri)
46
+ _send_request('GET', uri, nil)
47
+ end
48
+
49
+ #
50
+ # Send POST
51
+ #
52
+ # Issues a POST request (write) against the API and returns the result
53
+ # (as Ruby hash).
54
+ #
55
+ # Arguments:
56
+ #
57
+ # uri The API method to call including parameters
58
+ # (e.g. add_case/1)
59
+ # data The data to submit as part of the request (as
60
+ # Ruby hash, strings must be UTF-8 encoded)
61
+ #
62
+ def send_post(uri, data)
63
+ _send_request('POST', uri, data)
64
+ end
65
+
66
+ private
67
+ def _send_request(method, uri, data)
68
+ url = URI.parse(@url + uri)
69
+ if method == 'POST'
70
+ request = Net::HTTP::Post.new(url.path + '?' + url.query)
71
+ request.body = JSON.dump(data)
72
+ else
73
+ request = Net::HTTP::Get.new(url.path + '?' + url.query)
74
+ end
75
+ request.basic_auth(@user, @password)
76
+ request.add_field('Content-Type', 'application/json')
77
+
78
+ conn = Net::HTTP.new(url.host, url.port)
79
+ if url.scheme == 'https'
80
+ conn.use_ssl = true
81
+ conn.verify_mode = OpenSSL::SSL::VERIFY_NONE
82
+ end
83
+ response = conn.request(request)
84
+
85
+ if response.body && !response.body.empty?
86
+ result = JSON.parse(response.body)
87
+ else
88
+ result = {}
89
+ end
90
+
91
+ if response.code != '200'
92
+ if result && result.key?('error')
93
+ error = '"' + result['error'] + '"'
94
+ else
95
+ error = 'No additional error message received'
96
+ end
97
+ raise APIError.new('TestRail API returned HTTP %s (%s)' %
98
+ [response.code, error])
99
+ end
100
+
101
+ result
102
+ end
103
+ end
104
+
105
+ class APIError < StandardError
106
+ end
107
+
108
+ end
109
+ end
@@ -0,0 +1,304 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'nokogiri'
4
+ require 'yaml'
5
+ require 'pp'
6
+
7
+
8
+ # This module provides functions to add test results in testrail from a
9
+ # finished beaker run.
10
+ #
11
+ # It takes in a beaker junit file and a TestRail Testrun ID
12
+ #
13
+ # It matches the beaker tests with TestRail testcases by looking for the
14
+ # test case ID in the beaker script. The combination of a test run and a test case
15
+ # allows the script to add a result for a particular instance of a test case.
16
+ # In TestRail parlance, this is confusingly called a test.
17
+ # From the TestRail API docs:
18
+ # "In TestRail, tests are part of a test run and the test cases are part of the
19
+ # related test suite. So, when you create a new test run, TestRail creates a test
20
+ # for each test case found in the test suite of the run. You can therefore think
21
+ # of a test as an “instance” of a test case which can have test results, comments
22
+ # and a test status.""
23
+ module Telly
24
+ class TestRailTeller
25
+
26
+ # This exception is thrown when a testcase ID can't be found in a beaker
27
+ # script. It's meant to be caught and shown to the user as a warning
28
+ class MissingTestRailId < StandardError
29
+ end
30
+
31
+ TESTRAIL_URL = 'https://testrail.ops.puppetlabs.net/'
32
+ CREDENTIALS_FILE = '~/.testrail_credentials.yaml'
33
+
34
+ # Used for extracted the test case ID from beaker scripts
35
+ TESTCASE_ID_REGEX = /.*(?<jira_ticket>\w+-\d+).*[cC](?<testrun_id>\d+)/
36
+
37
+ # Testrail Status IDs
38
+ PASSED = 1
39
+ BLOCKED = 2
40
+ FAILED = 5
41
+
42
+ def do_stub_test(credentials)
43
+ api = get_testrail_api(credentials)
44
+
45
+ api.send_post
46
+ end
47
+
48
+ ##################################
49
+ # send_to_testrail
50
+ ##################################
51
+
52
+ # Run the importer
53
+ #
54
+ # @return [Void]
55
+ #
56
+ # @example password = Telly::main(parse_opts)
57
+ def send_to_testrail()
58
+ options = ArgParser.parse_opts
59
+ # Get pass/fail/skips from junit file
60
+ results = load_junit_results(options[:junit_file])
61
+
62
+ puts "Run results:"
63
+ puts "#{results[:passes].length} Passing"
64
+ puts "#{results[:failures].length} Failing or Erroring"
65
+ puts "#{results[:skips].length} Skipped"
66
+
67
+ # Set results in testrail
68
+ bad_results = set_testrail_results(results, options[:junit_file], options[:testrun_id], options[:dry_run])
69
+
70
+ # Print error messages
71
+ if not bad_results.empty?
72
+ puts "Error: There were problems processing these test scripts:"
73
+ bad_results.each do |test_script, error|
74
+ puts "#{test_script}:\n\t#{error}"
75
+ end
76
+ end
77
+ end
78
+
79
+
80
+ ##################################
81
+ # TestRail API
82
+ ##################################
83
+
84
+ # Load testrail credentials from file
85
+ #
86
+ # @return [Hash] Contains testrail_username and testrail_password
87
+ #
88
+ # @example password = load_credentials()["testrail_password"]
89
+ def load_credentials(credentials_file)
90
+ begin
91
+ YAML.load_file(File.expand_path(credentials_file))
92
+ rescue
93
+ puts "Error: Could not find #{credentials_file}"
94
+ puts "Create #{credentials_file} with the following:"
95
+ puts "testrail_username: your.username\ntestrail_password: yourpassword"
96
+
97
+ exit
98
+
99
+ end
100
+ end
101
+
102
+
103
+ # Returns a testrail API object that talks to testrail
104
+ #
105
+ # @param [Hash] credentials A hash containing at least two keys, testrail_username and testrail_password
106
+ #
107
+ # @return [TestRail::APIClient] The API object for talking to TestRail
108
+ #
109
+ # @example api = get_testrail_api(load_credentials)
110
+ def get_testrail_api(credentials)
111
+ client = TestRail::APIClient.new(TESTRAIL_URL)
112
+ client.user = credentials["testrail_username"]
113
+ client.password = credentials["testrail_password"]
114
+
115
+ return client
116
+ end
117
+
118
+ # Sets the results in testrail.
119
+ # Tests that have testrail API exceptions are kept track of in bad_results
120
+ #
121
+ # @param [Hash] results A hash of lists of xml objects from the junit output file.
122
+ # @param [String] junit_file The path to the junit xml file
123
+ # Needed for determining the path of the test file in add_failure, etc
124
+ # @param [String] testrun_id The TestRail test run ID
125
+ # @param [Boolean] dry_run Do not create api connection when dry_run = true
126
+ #
127
+ # @return [Void]
128
+ #
129
+ def set_testrail_results(results, junit_file, testrun_id, dry_run = false)
130
+ if not dry_run
131
+ credentials = load_credentials(CREDENTIALS_FILE)
132
+ api = get_testrail_api(credentials)
133
+ else
134
+ api = get_testrail_api( { "testrail_username" => 'name', "testrail_password" => 'pass' } )
135
+ end
136
+
137
+
138
+ # Results that couldn't be set in testrail for some reason
139
+ bad_results = {}
140
+
141
+ # passes
142
+ results[:passes].each do |junit_result|
143
+ begin
144
+ submit_result(api, PASSED, junit_result, junit_file, testrun_id, dry_run)
145
+ rescue MissingTestRailId, TestRail::APIError => e
146
+ bad_results[junit_result[:name]] = e.message
147
+ end
148
+ end
149
+
150
+ # Failures
151
+ results[:failures].each do |junit_result|
152
+ begin
153
+ submit_result(api, FAILED, junit_result, junit_file, testrun_id, dry_run)
154
+ rescue MissingTestRailId, TestRail::APIError => e
155
+ bad_results[junit_result[:name]] = e.message
156
+ end
157
+ end
158
+
159
+ # Skips
160
+ results[:skips].each do |junit_result|
161
+ begin
162
+ submit_result(api, BLOCKED, junit_result, junit_file, testrun_id, dry_run)
163
+ rescue MissingTestRailId, TestRail::APIError => e
164
+ bad_results[junit_result[:name]] = e.message
165
+ end
166
+ end
167
+
168
+ return bad_results
169
+ end
170
+
171
+ # Submits a test result to TestRail
172
+ #
173
+ # @param [TestRail::APIClient] api TestRail API object
174
+ # @param [int] status The testrail status to set
175
+ # @param [Nokogiri::XML::Element] junit_result The nokogiri node that holds the junit result
176
+ # @param [String] junit_file Path to the junit file the test result originated from
177
+ # @param [String] testrun_id The testrun ID
178
+ # @param [Boolean] dry_run When true do not send results to TestRail
179
+ #
180
+ # @return [Void]
181
+ #
182
+ # @raise [TestRail::APIError] When there is a problem with the API request, testrail raises
183
+ # this exception. Should be caught for error reporting
184
+ #
185
+ # @example submit_result(api, BLOCKED, junit_result, junit_file, testrun_id)
186
+ def submit_result(api, status, junit_result, junit_file, testrun_id, dry_run = false)
187
+ test_file_path = beaker_test_path(junit_file, junit_result)
188
+
189
+ puts junit_result.class
190
+ testcase_id = testcase_id_from_beaker_script(test_file_path)
191
+
192
+ time_elapsed = make_testrail_time(junit_result[:time])
193
+
194
+ # Make appropriate comment for testrail
195
+ case status
196
+ when FAILED
197
+ error_message = junit_result.xpath('./failure').first[:message]
198
+ testrail_comment = "Failed with message:\n#{error_message}"
199
+ when BLOCKED
200
+ skip_message = junit_result.xpath('system-out').first.text
201
+ testrail_comment = "Skipped with message:\n#{skip_message}"
202
+ else
203
+ testrail_comment = "Passed"
204
+ end
205
+
206
+ puts "\nSetting result for test case: #{testcase_id}"
207
+ puts "Adding comment:\n#{testrail_comment}"
208
+
209
+ if not dry_run
210
+ api.send_post("add_result_for_case/#{testrun_id}/#{testcase_id}",
211
+ {
212
+ status_id: status,
213
+ comment: testrail_comment,
214
+ elapsed: time_elapsed,
215
+ }
216
+ )
217
+ end
218
+ end
219
+
220
+
221
+ # Returns a string that testrail accepts as an elapsed time
222
+ # Input from beaker is a float in seconds, so it rounds it to the
223
+ # nearest second, and adds an 's' at the end
224
+ #
225
+ # Testrail throws an exception if it gets "0s", so it returns a
226
+ # minimum of "1s"
227
+ #
228
+ # @param [String] seconds_string A string that contains only a number, the elapsed time of a test
229
+ #
230
+ # @return [String] The elapsed time of the test run, rounded and with an 's' appended
231
+ #
232
+ # @example puts make_testrail_time("2.34") # "2s"
233
+ def make_testrail_time(seconds_string)
234
+ # If time is 0, make it 1
235
+ rounded_time = [seconds_string.to_f.round, 1].max
236
+ # Test duration
237
+ time_elapsed = "#{rounded_time}s"
238
+
239
+ return time_elapsed
240
+ end
241
+
242
+
243
+ ##################################
244
+ # Junit and Beaker file functions
245
+ ##################################
246
+
247
+ # Loads the results of a beaker run.
248
+ # Returns hash of failures, passes, and skips that each hold a list of
249
+ # junit xml objects
250
+ #
251
+ # @param [String] junit_file Path to a junit xml file
252
+ #
253
+ # @return [Hash] A hash containing xml objects for the failures, skips, and passes
254
+ #
255
+ # @example load_junit_results("~/junit/latest/beaker_junit.xml")
256
+ def load_junit_results(junit_file)
257
+ junit_doc = Nokogiri::XML(File.read(junit_file))
258
+
259
+ failures = junit_doc.xpath('//testcase[failure]')
260
+ skips = junit_doc.xpath('//testcase[skip]')
261
+ passes = junit_doc.xpath('//testcase[not(failure) and not(skip)]')
262
+
263
+ return {failures: failures, skips: skips, passes: passes}
264
+ end
265
+
266
+
267
+ # Extracts the test case id from the test script
268
+ #
269
+ # @param [String] beaker_file Path to a beaker test script
270
+ #
271
+ # @return [String] The test case ID
272
+ #
273
+ # @example testcase_id_from_beaker_script("~/tests/test_the_things.rb") # 1234
274
+ def testcase_id_from_beaker_script(beaker_file)
275
+ # Find first matching line
276
+ match = File.readlines(beaker_file).map { |line| line.match(TESTCASE_ID_REGEX) }.compact.first
277
+
278
+ raise MissingTestRailId, 'Testcase ID could not be found in file' if match.nil?
279
+
280
+ match[:testrun_id]
281
+ end
282
+
283
+
284
+ # Calculates the path to a beaker test file by combining the junit file path
285
+ # with the test name from the junit results.
286
+ # Makes the assumption that junit folder that beaker creates will always be
287
+ # 2 directories up from the beaker script base directory.
288
+ # TODO somewhat hacky, maybe a config/command line option
289
+ #
290
+ # @param [String] junit_file_path Path to a junit xml file
291
+ # @param [String] junit_result Path to a junit xml file
292
+ #
293
+ # @return [String] The path to the beaker script from the junit test result
294
+ #
295
+ # @example load_junit_results("~/junit/latest/beaker_junit.xml")
296
+ def beaker_test_path(junit_file_path, junit_result)
297
+ beaker_folder_path = junit_result[:classname]
298
+ test_filename = junit_result[:name]
299
+
300
+ File.join(File.dirname(junit_file_path), "../../../", beaker_folder_path, test_filename)
301
+ end
302
+
303
+ end
304
+ end
@@ -0,0 +1,5 @@
1
+ module Telly
2
+ module Version
3
+ STRING = '0.2.0'
4
+ end
5
+ end
metadata CHANGED
@@ -1,8 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: telly
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
5
- prerelease:
4
+ version: 0.2.0
6
5
  platform: ruby
7
6
  authors:
8
7
  - Joe Pinsonault
@@ -15,7 +14,6 @@ dependencies:
15
14
  - !ruby/object:Gem::Dependency
16
15
  name: webmock
17
16
  requirement: !ruby/object:Gem::Requirement
18
- none: false
19
17
  requirements:
20
18
  - - ! '>='
21
19
  - !ruby/object:Gem::Version
@@ -23,7 +21,6 @@ dependencies:
23
21
  type: :runtime
24
22
  prerelease: false
25
23
  version_requirements: !ruby/object:Gem::Requirement
26
- none: false
27
24
  requirements:
28
25
  - - ! '>='
29
26
  - !ruby/object:Gem::Version
@@ -31,7 +28,6 @@ dependencies:
31
28
  - !ruby/object:Gem::Dependency
32
29
  name: nokogiri
33
30
  requirement: !ruby/object:Gem::Requirement
34
- none: false
35
31
  requirements:
36
32
  - - ! '>='
37
33
  - !ruby/object:Gem::Version
@@ -39,7 +35,6 @@ dependencies:
39
35
  type: :runtime
40
36
  prerelease: false
41
37
  version_requirements: !ruby/object:Gem::Requirement
42
- none: false
43
38
  requirements:
44
39
  - - ! '>='
45
40
  - !ruby/object:Gem::Version
@@ -47,7 +42,6 @@ dependencies:
47
42
  - !ruby/object:Gem::Dependency
48
43
  name: rspec
49
44
  requirement: !ruby/object:Gem::Requirement
50
- none: false
51
45
  requirements:
52
46
  - - ! '>='
53
47
  - !ruby/object:Gem::Version
@@ -55,7 +49,20 @@ dependencies:
55
49
  type: :development
56
50
  prerelease: false
57
51
  version_requirements: !ruby/object:Gem::Requirement
58
- none: false
52
+ requirements:
53
+ - - ! '>='
54
+ - !ruby/object:Gem::Version
55
+ version: '0'
56
+ - !ruby/object:Gem::Dependency
57
+ name: rake
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ! '>='
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
63
+ type: :development
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
59
66
  requirements:
60
67
  - - ! '>='
61
68
  - !ruby/object:Gem::Version
@@ -67,32 +74,35 @@ executables:
67
74
  extensions: []
68
75
  extra_rdoc_files: []
69
76
  files:
70
- - lib/telly.rb
71
- - lib/testrail.rb
72
77
  - bin/telly
78
+ - lib/telly.rb
79
+ - lib/telly/arg_parser.rb
80
+ - lib/telly/test_rail.rb
81
+ - lib/telly/test_rail_teller.rb
82
+ - lib/telly/version.rb
73
83
  homepage: http://rubygems.org/gems/telly
74
84
  licenses:
75
85
  - Apache
86
+ metadata: {}
76
87
  post_install_message:
77
88
  rdoc_options: []
78
89
  require_paths:
79
90
  - lib
80
91
  required_ruby_version: !ruby/object:Gem::Requirement
81
- none: false
82
92
  requirements:
83
93
  - - ! '>='
84
94
  - !ruby/object:Gem::Version
85
95
  version: '0'
86
96
  required_rubygems_version: !ruby/object:Gem::Requirement
87
- none: false
88
97
  requirements:
89
98
  - - ! '>='
90
99
  - !ruby/object:Gem::Version
91
100
  version: '0'
92
101
  requirements: []
93
102
  rubyforge_project:
94
- rubygems_version: 1.8.23
103
+ rubygems_version: 2.4.6
95
104
  signing_key:
96
- specification_version: 3
105
+ specification_version: 4
97
106
  summary: Imports beaker results into TestRail test run results
98
107
  test_files: []
108
+ has_rdoc:
@@ -1,106 +0,0 @@
1
- #
2
- # TestRail API binding for Ruby (API v2, available since TestRail 3.0)
3
- #
4
- # Learn more:
5
- #
6
- # http://docs.gurock.com/testrail-api2/start
7
- # http://docs.gurock.com/testrail-api2/accessing
8
- #
9
- # Copyright Gurock Software GmbH. See license.md for details.
10
- #
11
-
12
- require 'net/http'
13
- require 'net/https'
14
- require 'uri'
15
- require 'json'
16
-
17
- module TestRail
18
- class APIClient
19
- @url = ''
20
- @user = ''
21
- @password = ''
22
-
23
- attr_accessor :user
24
- attr_accessor :password
25
-
26
- def initialize(base_url)
27
- if !base_url.match(/\/$/)
28
- base_url += '/'
29
- end
30
- @url = base_url + 'index.php?/api/v2/'
31
- end
32
-
33
- #
34
- # Send Get
35
- #
36
- # Issues a GET request (read) against the API and returns the result
37
- # (as Ruby hash).
38
- #
39
- # Arguments:
40
- #
41
- # uri The API method to call including parameters
42
- # (e.g. get_case/1)
43
- #
44
- def send_get(uri)
45
- _send_request('GET', uri, nil)
46
- end
47
-
48
- #
49
- # Send POST
50
- #
51
- # Issues a POST request (write) against the API and returns the result
52
- # (as Ruby hash).
53
- #
54
- # Arguments:
55
- #
56
- # uri The API method to call including parameters
57
- # (e.g. add_case/1)
58
- # data The data to submit as part of the request (as
59
- # Ruby hash, strings must be UTF-8 encoded)
60
- #
61
- def send_post(uri, data)
62
- _send_request('POST', uri, data)
63
- end
64
-
65
- private
66
- def _send_request(method, uri, data)
67
- url = URI.parse(@url + uri)
68
- if method == 'POST'
69
- request = Net::HTTP::Post.new(url.path + '?' + url.query)
70
- request.body = JSON.dump(data)
71
- else
72
- request = Net::HTTP::Get.new(url.path + '?' + url.query)
73
- end
74
- request.basic_auth(@user, @password)
75
- request.add_field('Content-Type', 'application/json')
76
-
77
- conn = Net::HTTP.new(url.host, url.port)
78
- if url.scheme == 'https'
79
- conn.use_ssl = true
80
- conn.verify_mode = OpenSSL::SSL::VERIFY_NONE
81
- end
82
- response = conn.request(request)
83
-
84
- if response.body && !response.body.empty?
85
- result = JSON.parse(response.body)
86
- else
87
- result = {}
88
- end
89
-
90
- if response.code != '200'
91
- if result && result.key?('error')
92
- error = '"' + result['error'] + '"'
93
- else
94
- error = 'No additional error message received'
95
- end
96
- raise APIError.new('TestRail API returned HTTP %s (%s)' %
97
- [response.code, error])
98
- end
99
-
100
- result
101
- end
102
- end
103
-
104
- class APIError < StandardError
105
- end
106
- end