telly 0.1.1

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.
Files changed (5) hide show
  1. checksums.yaml +7 -0
  2. data/bin/telly +57 -0
  3. data/lib/telly.rb +289 -0
  4. data/lib/testrail.rb +106 -0
  5. metadata +90 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: daa063e14ee499acd934b81ead0304419094190b
4
+ data.tar.gz: a8f8240fe98212a75bca1f81c722981b735930cf
5
+ SHA512:
6
+ metadata.gz: 9060bdcd028c788d6c2224b591d342d905510e2f9f75fab049ef4d8f92a241139d7d27888ae07d64141bfdc6958e54c81ddea93e6e907b205e6eb09a3a1ec4ec
7
+ data.tar.gz: 0f8d56d22cbccfe8d6e88650f2ab480db43bf88e561d003d40213f0aa224ad9efda3291e52d90f8cfbd8ce29b1bcd063acc2e6fd4b9adf33cb45f18cb4e070f4
data/bin/telly ADDED
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # == telly.rb
4
+ # This script adds test results in testrail from a finished beaker run
5
+ # It takes in a beaker junit file and a TestRail Testrun ID
6
+ #
7
+ # It matches the beaker tests with TestRail testcases by looking for the
8
+ # test case ID in the beaker script. The combination of a test run and a test case
9
+ # allows the script to add a result for a particular instance of a test case.
10
+ # In TestRail parlance, this is confusingly called a test.
11
+ # From the TestRail API docs:
12
+ # "In TestRail, tests are part of a test run and the test cases are part of the
13
+ # related test suite. So, when you create a new test run, TestRail creates a test
14
+ # for each test case found in the test suite of the run. You can therefore think
15
+ # of a test as an “instance” of a test case which can have test results, comments
16
+ # and a test status.""
17
+
18
+ require 'telly'
19
+ require 'optparse'
20
+
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)
data/lib/telly.rb ADDED
@@ -0,0 +1,289 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'nokogiri'
4
+ require 'yaml'
5
+ require 'pp'
6
+ require 'testrail'
7
+
8
+ # == telly.rb
9
+ # This module provides functions to add test results in testrail from a
10
+ # finished beaker run.
11
+ #
12
+ # It takes in a beaker junit file and a TestRail Testrun ID
13
+ #
14
+ # It matches the beaker tests with TestRail testcases by looking for the
15
+ # test case ID in the beaker script. The combination of a test run and a test case
16
+ # allows the script to add a result for a particular instance of a test case.
17
+ # In TestRail parlance, this is confusingly called a test.
18
+ # From the TestRail API docs:
19
+ # "In TestRail, tests are part of a test run and the test cases are part of the
20
+ # related test suite. So, when you create a new test run, TestRail creates a test
21
+ # for each test case found in the test suite of the run. You can therefore think
22
+ # of a test as an “instance” of a test case which can have test results, comments
23
+ # and a test status.""
24
+ module Telly
25
+
26
+ TESTRAIL_URL = 'https://testrail.ops.puppetlabs.net/'
27
+ CREDENTIALS_FILE = '~/.testrail_credentials.yaml'
28
+
29
+ # Used for extracted the test case ID from beaker scripts
30
+ TESTCASE_ID_REGEX = /.*(?<jira_ticket>\w+-\d+).*[cC](?<testrun_id>\d+)/
31
+
32
+ # Testrail Status IDs
33
+ PASSED = 1
34
+ BLOCKED = 2
35
+ FAILED = 5
36
+
37
+ def do_stub_test(credentials)
38
+ api = get_testrail_api(credentials)
39
+
40
+ api.send_post
41
+ end
42
+
43
+
44
+ ##################################
45
+ # Main
46
+ ##################################
47
+
48
+ # Run the importer
49
+ #
50
+ # @param [Hash] An optparse object
51
+ #
52
+ # @return [Void]
53
+ #
54
+ # @example password = Telly::main(parse_opts)
55
+ def Telly.main(options)
56
+ # Get pass/fail/skips from junit file
57
+ results = load_junit_results(options[:junit_file])
58
+
59
+ puts "Run results:"
60
+ puts "#{results[:passes].length} Passing"
61
+ puts "#{results[:failures].length} Failing or Erroring"
62
+ puts "#{results[:skips].length} Skipped"
63
+
64
+ # Set results in testrail
65
+ bad_results = set_testrail_results(results, options[:junit_file], options[:testrun_id])
66
+
67
+ # Print error messages
68
+ if not bad_results.empty?
69
+ puts "Error: There were problems processing these test scripts:"
70
+ bad_results.each do |test_script, error|
71
+ puts "#{test_script}:\n\t#{error}"
72
+ end
73
+ end
74
+ end
75
+
76
+
77
+ ##################################
78
+ # TestRail API
79
+ ##################################
80
+
81
+ # Load testrail credentials from file
82
+ #
83
+ # @return [Hash] Contains testrail_username and testrail_password
84
+ #
85
+ # @example password = load_credentials()["testrail_password"]
86
+ def Telly.load_credentials(credentials_file)
87
+ begin
88
+ YAML.load_file(File.expand_path(credentials_file))
89
+ rescue
90
+ puts "Error: Could not find #{credentials_file}"
91
+ puts "Create #{credentials_file} with the following:"
92
+ puts "testrail_username: your.username\ntestrail_password: yourpassword"
93
+
94
+ exit
95
+
96
+ end
97
+ end
98
+
99
+
100
+ # Returns a testrail API object that talks to testrail
101
+ #
102
+ # @param [Hash] credentials A hash containing at least two keys, testrail_username and testrail_password
103
+ #
104
+ # @return [TestRail::APIClient] The API object for talking to TestRail
105
+ #
106
+ # @example api = get_testrail_api(load_credentials)
107
+ def Telly.get_testrail_api(credentials)
108
+ client = TestRail::APIClient.new(TESTRAIL_URL)
109
+ client.user = credentials["testrail_username"]
110
+ client.password = credentials["testrail_password"]
111
+
112
+ return client
113
+ end
114
+
115
+ # Sets the results in testrail.
116
+ # Tests that have testrail API exceptions are kept track of in bad_results
117
+ #
118
+ # @param [Hash] results A hash of lists of xml objects from the junit output file.
119
+ # @param [String] junit_file The path to the junit xml file
120
+ # Needed for determining the path of the test file in add_failure, etc
121
+ # @param [String] testrun_id The TestRail test run ID
122
+ #
123
+ # @return [Void]
124
+ #
125
+ def Telly.set_testrail_results(results, junit_file, testrun_id)
126
+ credentials = load_credentials(CREDENTIALS_FILE)
127
+ api = get_testrail_api(credentials)
128
+
129
+ # Results that couldn't be set in testrail for some reason
130
+ bad_results = {}
131
+
132
+ # passes
133
+ results[:passes].each do |junit_result|
134
+ begin
135
+ submit_result(api, PASSED, junit_result, junit_file, testrun_id)
136
+ rescue TestRail::APIError => e
137
+ bad_results[junit_result[:name]] = e.message
138
+ end
139
+ end
140
+
141
+ # Failures
142
+ results[:failures].each do |junit_result|
143
+ begin
144
+ submit_result(api, FAILED, junit_result, junit_file, testrun_id)
145
+ rescue TestRail::APIError => e
146
+ bad_results[junit_result[:name]] = e.message
147
+ end
148
+ end
149
+
150
+ # Skips
151
+ results[:skips].each do |junit_result|
152
+ begin
153
+ submit_result(api, BLOCKED, junit_result, junit_file, testrun_id)
154
+ rescue TestRail::APIError => e
155
+ bad_results[junit_result[:name]] = e.message
156
+ end
157
+ end
158
+
159
+ return bad_results
160
+ end
161
+
162
+ # Submits a test result to TestRail
163
+ #
164
+ # @param [TestRail::APIClient] api TestRail API object
165
+ # @param [int] status The testrail status to set
166
+ # @param [Nokogiri::XML::Element] junit_result The nokogiri node that holds the junit result
167
+ # @param [String] junit_file Path to the junit file the test result originated from
168
+ # @param [String] testrun_id The testrun ID
169
+ #
170
+ # @return [Void]
171
+ #
172
+ # @raise [TestRail::APIError] When there is a problem with the API request, testrail raises
173
+ # this exception. Should be caught for error reporting
174
+ #
175
+ # @example submit_result(api, BLOCKED, junit_result, junit_file, testrun_id)
176
+ def Telly.submit_result(api, status, junit_result, junit_file, testrun_id)
177
+ test_file_path = beaker_test_path(junit_file, junit_result)
178
+
179
+ puts junit_result.class
180
+ testcase_id = testcase_id_from_beaker_script(test_file_path)
181
+
182
+ time_elapsed = make_testrail_time(junit_result[:time])
183
+
184
+ # Make appropriate comment for testrail
185
+ case status
186
+ when FAILED
187
+ error_message = junit_result.xpath('./failure').first[:message]
188
+ testrail_comment = "Failed with message:\n#{error_message}"
189
+ when BLOCKED
190
+ skip_message = junit_result.xpath('system-out').first.text
191
+ testrail_comment = "Skipped with message:\n#{skip_message}"
192
+ else
193
+ testrail_comment = "Passed"
194
+ end
195
+
196
+ puts "\nSetting result for test case: #{testcase_id}"
197
+ puts "Adding comment:\n#{testrail_comment}"
198
+
199
+ api.send_post("add_result_for_case/#{testrun_id}/#{testcase_id}",
200
+ {
201
+ status_id: status,
202
+ comment: testrail_comment,
203
+ elapsed: time_elapsed,
204
+ }
205
+ )
206
+ end
207
+
208
+
209
+ # Returns a string that testrail accepts as an elapsed time
210
+ # Input from beaker is a float in seconds, so it rounds it to the
211
+ # nearest second, and adds an 's' at the end
212
+ #
213
+ # Testrail throws an exception if it gets "0s", so it returns a
214
+ # minimum of "1s"
215
+ #
216
+ # @param [String] seconds_string A string that contains only a number, the elapsed time of a test
217
+ #
218
+ # @return [String] The elapsed time of the test run, rounded and with an 's' appended
219
+ #
220
+ # @example puts make_testrail_time("2.34") # "2s"
221
+ def Telly.make_testrail_time(seconds_string)
222
+ # If time is 0, make it 1
223
+ rounded_time = [seconds_string.to_f.round, 1].max
224
+ # Test duration
225
+ time_elapsed = "#{rounded_time}s"
226
+
227
+ return time_elapsed
228
+ end
229
+
230
+
231
+ ##################################
232
+ # Junit and Beaker file functions
233
+ ##################################
234
+
235
+ # Loads the results of a beaker run.
236
+ # Returns hash of failures, passes, and skips that each hold a list of
237
+ # junit xml objects
238
+ #
239
+ # @param [String] junit_file Path to a junit xml file
240
+ #
241
+ # @return [Hash] A hash containing xml objects for the failures, skips, and passes
242
+ #
243
+ # @example load_junit_results("~/junit/latest/beaker_junit.xml")
244
+ def Telly.load_junit_results(junit_file)
245
+ junit_doc = Nokogiri::XML(File.read(junit_file))
246
+
247
+ failures = junit_doc.xpath('//testcase[failure]')
248
+ skips = junit_doc.xpath('//testcase[skip]')
249
+ passes = junit_doc.xpath('//testcase[not(failure) and not(skip)]')
250
+
251
+ return {failures: failures, skips: skips, passes: passes}
252
+ end
253
+
254
+
255
+ # Extracts the test case id from the test script
256
+ #
257
+ # @param [String] beaker_file Path to a beaker test script
258
+ #
259
+ # @return [String] The test case ID
260
+ #
261
+ # @example testcase_id_from_beaker_script("~/tests/test_the_things.rb") # 1234
262
+ def Telly.testcase_id_from_beaker_script(beaker_file)
263
+ # Find first matching line
264
+ match = File.readlines(beaker_file).map { |line| line.match(TESTCASE_ID_REGEX) }.compact.first
265
+
266
+ match[:testrun_id]
267
+ end
268
+
269
+
270
+ # Calculates the path to a beaker test file by combining the junit file path
271
+ # with the test name from the junit results.
272
+ # Makes the assumption that junit folder that beaker creates will always be
273
+ # 2 directories up from the beaker script base directory.
274
+ # TODO somewhat hacky, maybe a config/command line option
275
+ #
276
+ # @param [String] junit_file_path Path to a junit xml file
277
+ # @param [String] junit_result Path to a junit xml file
278
+ #
279
+ # @return [String] The path to the beaker script from the junit test result
280
+ #
281
+ # @example load_junit_results("~/junit/latest/beaker_junit.xml")
282
+ def Telly.beaker_test_path(junit_file_path, junit_result)
283
+ beaker_folder_path = junit_result[:classname]
284
+ test_filename = junit_result[:name]
285
+
286
+ File.join(File.dirname(junit_file_path), "../../", beaker_folder_path, test_filename)
287
+ end
288
+
289
+ end
data/lib/testrail.rb ADDED
@@ -0,0 +1,106 @@
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
metadata ADDED
@@ -0,0 +1,90 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: telly
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Joe Pinsonault
8
+ - Puppetlabs
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2014-12-15 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: webmock
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - ">="
19
+ - !ruby/object:Gem::Version
20
+ version: '0'
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ version: '0'
28
+ - !ruby/object:Gem::Dependency
29
+ name: nokogiri
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - ">="
33
+ - !ruby/object:Gem::Version
34
+ version: '0'
35
+ type: :runtime
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ version: '0'
42
+ - !ruby/object:Gem::Dependency
43
+ name: rspec
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: '0'
49
+ type: :development
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: '0'
56
+ description: Imports beaker results into TestRail test run results
57
+ email: joe.pinsonault@gmail.com
58
+ executables:
59
+ - telly
60
+ extensions: []
61
+ extra_rdoc_files: []
62
+ files:
63
+ - bin/telly
64
+ - lib/telly.rb
65
+ - lib/testrail.rb
66
+ homepage: http://rubygems.org/gems/telly
67
+ licenses:
68
+ - Apache
69
+ metadata: {}
70
+ post_install_message:
71
+ rdoc_options: []
72
+ require_paths:
73
+ - lib
74
+ required_ruby_version: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ version: '0'
79
+ required_rubygems_version: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: '0'
84
+ requirements: []
85
+ rubyforge_project:
86
+ rubygems_version: 2.4.5
87
+ signing_key:
88
+ specification_version: 4
89
+ summary: Imports beaker results into TestRail test run results
90
+ test_files: []