telly 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
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: []