trail_marker 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,169 @@
1
+ # Executes parsing and posting results to TestRail
2
+ class MarkTests
3
+
4
+ EXIT_MSG = "\nYour test results have been marked on TestRails!\n"
5
+ DEFAULT_COMMENT = "Marked by Automation"
6
+
7
+ #
8
+ def initialize(argobj, config_filename)
9
+ @argument = argobj
10
+ cf = ConfigFile.new(config_filename)
11
+ cf.check_create_configfile
12
+ @user = @argument.get_arg_value('-u') == '' ? cf.username : @argument.get_arg_value('-u')
13
+ @token = @argument.get_arg_value('-pw') == '' ? cf.token : @argument.get_arg_value('-pw')
14
+ @url = @argument.get_arg_value('-url') == '' ? cf.testrail_url : @argument.get_arg_value('-url')
15
+ @default_comment = @argument.get_arg_value('-com') == '' ? cf.default_comment : @argument.get_arg_value('-com')
16
+ puts "VALUES: #{@user} #{@token}"
17
+ end
18
+
19
+ # If user name and password (or token) are passed in the command line arguments
20
+ # it would use those instead of the preset values.
21
+ #
22
+ def get_client
23
+ client = TestRail::APIClient.new(@url)
24
+ client.user = @user
25
+ client.password = @token
26
+ return client
27
+ end
28
+
29
+ # Initial setup - creates API client and API testrail objects
30
+ #
31
+ def setup
32
+ if @client.nil? || @api_testrail.nil?
33
+ @client = get_client
34
+ @api_testrail = ApiTestRail.new(@client)
35
+ end
36
+ end
37
+
38
+
39
+ # Determine if results are for a run or a plan.
40
+ # Calls method to mark results.
41
+ #
42
+ def markoff_all_results
43
+ mark_type = 'testrun'
44
+ runner = @argument.get_optional_arg(['-r', '-cr'])
45
+ if runner.nil? || runner == ""
46
+ mark_type = "testplan"
47
+ runner = @argument.get_optional_arg(['-t', '-ct'])
48
+ end
49
+ run_ids = []
50
+ project_id = @api_testrail.get_project_id(@argument.get_arg_value('-p'))
51
+ case mark_type
52
+ when "testrun"
53
+ run_name = @argument.get_optional_arg(['-r', '-cr'])
54
+ run_ids << @api_testrail.get_test_runplan_id(project_id, run_name)
55
+ when "testplan"
56
+ plan_name = @argument.get_optional_arg(['-t', '-ct'])
57
+ run_ids = @api_testrail.get_testplan_run_ids(project_id, plan_name)
58
+ end
59
+ markoff(run_ids, project_id)
60
+ end
61
+
62
+ # Parses the XML files and makes API calls to post results to TestRail
63
+ def markoff(run_ids, pid = nil)
64
+ project_id = pid.nil? ? @api_testrail.get_project_id(@argument.get_arg_value('-p')) : pid
65
+ is_dir = @argument.arg_exists?('-x')
66
+ is_file = @argument.arg_exists?('-f')
67
+ if is_dir || is_file
68
+ results_parser = nil
69
+ #results_file = nil
70
+ if is_dir
71
+ results_parser = ResultsParser.new(@argument.get_arg_value('-x'))
72
+ results_files = results_parser.getAllXMLFiles()
73
+ else
74
+ specific_file = @argument.get_arg_value('-f')
75
+ results_parser = ResultsParser.new(specific_file)
76
+ results_files = Array.new
77
+ results_files << specific_file
78
+ end
79
+
80
+ if results_files.size <= 0
81
+ puts "No XML Results found. Please check your directory"
82
+ exit(0)
83
+ end
84
+ results_files.each do |one_file|
85
+ case_results = results_parser.read_XML_file(one_file)
86
+ if ! case_results.nil? && case_results.kind_of?(Array)
87
+ case_results.each do |one_case|
88
+ testrun_ids = []
89
+ testrun_ids += run_ids
90
+ markoff_test_case(@api_testrail, one_case, testrun_ids)
91
+ end
92
+ else
93
+ puts "No Results found in : #{one_file} (Might be empty)"
94
+ end
95
+ end
96
+ end
97
+
98
+ end
99
+
100
+ def markoff_test_case(api_obj, result_hash, run_ids)
101
+ case_id = result_hash[:trail_case].gsub(/[Cc]/, "")
102
+ test_id = nil
103
+ run_id = nil
104
+ puts "=====> #{run_ids}"
105
+ if run_ids.size > 1
106
+ run_ids.each do |rid|
107
+ tempid = api_obj.get_testid(rid, case_id)
108
+ if ! tempid.nil?
109
+ test_id = tempid
110
+ run_id = rid
111
+ break
112
+ end
113
+ end
114
+ else
115
+ run_id = run_ids.pop()
116
+ end
117
+ passed = result_hash[:passed]
118
+ api_obj.markoff_test(case_id, run_id, passed, @default_comment)
119
+ end
120
+
121
+ def create_milestone
122
+ project_name = @argument.get_arg_value('-p')
123
+ milestone_name = @argument.get_arg_value('-m')
124
+ if ! milestone_name.nil?
125
+ @api_testrail.create_milestone(project_name, milestone_name)
126
+ end
127
+ end
128
+
129
+ # USER DOES NOT HAVE PERMISSION.
130
+ # Verify you have super user account or maybe
131
+ # not yet implemented in TestRail. GUI has no delete as well.
132
+ def delete_milestone
133
+ project_name = @argument.get_arg_value('-p')
134
+ milestone_name = @argument.get_arg_value('-dm')
135
+ if ! milestone_name.nil?
136
+ @api_testrail.delete_milestone(project_name, milestone_name)
137
+ end
138
+ end
139
+
140
+ def create_testrun
141
+ project_name = @argument.get_arg_value('-p')
142
+ milestone_name = @argument.get_optional_arg(['-m', '-cm'])
143
+ testrun_name = @argument.get_arg_value('-cr')
144
+ if ! testrun_name.nil?
145
+ @api_testrail.create_testrun(project_name, milestone_name, testrun_name)
146
+ end
147
+ end
148
+
149
+
150
+ def check_create
151
+ project_name = @argument.get_arg_value('-p')
152
+ if @argument.arg_exists?('-cm')
153
+ milestone_name = @argument.get_arg_value('-cm')
154
+ @api_testrail.create_milestone(project_name, milestone_name)
155
+ end
156
+ run_name = @argument.get_arg_value('-cr')
157
+ if @argument.arg_exists?('-cr')
158
+ milestone_name = @argument.get_optional_arg(['-m', '-cm'])
159
+ suite_name = @argument.get_arg_value('-s')
160
+ @api_testrail.create_testrun(project_name, milestone_name, run_name, suite_name)
161
+ end
162
+ end
163
+
164
+ def show_exit_msg
165
+ puts "\n#{EXIT_MSG}\n"
166
+ end
167
+
168
+
169
+ end
@@ -0,0 +1,122 @@
1
+ require_relative 'testrail'
2
+
3
+ # Calls testrail API
4
+ # History: 2/26/18 Removed retry for failed calls
5
+ # 2/27/18 Combined post/get methods DRY
6
+ # Change retry to skip only if TC not found.
7
+ #
8
+ class Request
9
+ RETRIES = 1
10
+
11
+ def initialize(client, debug_mode = false)
12
+ @client = client
13
+ @debug_mode = get_debug_value(debug_mode)
14
+ @debug_mode = true
15
+ end
16
+
17
+ # Retries an API call max number of times if an
18
+ # exception is raised. Sleeps 4, 8, 12 .... seconds
19
+ # for each retry.
20
+ #
21
+ def exec_api_call(rest_type, req, data = nil, exit_on_fail = false)
22
+ max_retry = RETRIES
23
+ get_hash = nil
24
+ attempts = 0
25
+ is_good = false
26
+ while !is_good && attempts < max_retry
27
+ attempts += 1
28
+ get_hash = call_api(req, rest_type, data)
29
+ is_good = get_hash[:status]
30
+ unless is_good
31
+ exit_script() if exit_on_fail
32
+ puts "Got Error making API call - #{req} "
33
+ if attempts < max_retry
34
+ puts "Retrying #{attempts}"
35
+ sleep(4 * attempts)
36
+ end
37
+ end
38
+ end
39
+ get_hash
40
+ end
41
+
42
+ # TODO: DRY with exec_get
43
+ def exec_post(req, data, exit_on_fail = false)
44
+ response_hash = exec_api_call('POST', req, data, exit_on_fail)
45
+ response_hash[:response]
46
+ end
47
+
48
+ def exec_get(req, exit_on_fail = false)
49
+ response_hash = exec_api_call('GET', req, nil, exit_on_fail)
50
+ check_authentication_fail(response_hash) unless response_hash[:status]
51
+ response_hash[:response]
52
+ end
53
+
54
+ # Executes a TestRail API call but catches any
55
+ # exception raised to prevent script from crashing.
56
+ # Used by exec_get to do retries.
57
+ #
58
+ def call_api(req, rtype, data=nil)
59
+ msg("#{rtype} REQ: #{req}")
60
+ res_hash = {:status => true, :response => ""}
61
+ begin
62
+ if rtype == 'POST'
63
+ get_response = @client.send_post(req, data)
64
+ else
65
+ get_response = @client.send_get(req)
66
+ end
67
+ rescue Exception => e
68
+ puts "Raised Exception: #{e.message}."
69
+ res_hash[:status] = false
70
+ res_hash[:response] = e.message
71
+ else
72
+ res_hash[:status] = true
73
+ res_hash[:response] = get_response
74
+ end
75
+ msg("RESPONSE: #{res_hash}")
76
+ return res_hash
77
+ end
78
+
79
+ private
80
+
81
+ def exit_script()
82
+ msg("Exiting script.")
83
+ exit(1)
84
+ end
85
+
86
+ def msg(msg_txt)
87
+ if @debug_mode
88
+ puts msg_txt
89
+ end
90
+ end
91
+
92
+ class APIError < StandardError
93
+ end
94
+
95
+ def get_debug_value(debug_val)
96
+ boolval = false
97
+ if !! debug_val == debug_val
98
+ boolval = debug_val
99
+ else
100
+ boolval = string_to_boolean(debug_val)
101
+ end
102
+ return boolval
103
+ end
104
+
105
+ def string_to_boolean(strval)
106
+ retval = false
107
+ if strval.downcase == "true"
108
+ retval = true
109
+ end
110
+ return retval
111
+ end
112
+
113
+ def check_authentication_fail(response_hash)
114
+ failed_str = "Authentication failed"
115
+ max_attempts_str = "maximum number of failed"
116
+ if response_hash[:response].include?(failed_str) || response_hash[:response].include?(max_attempts_str)
117
+ puts "Cannot authenticate User or password.\n"
118
+ exit(1)
119
+ end
120
+ end
121
+
122
+ end
@@ -0,0 +1,121 @@
1
+ require 'json'
2
+
3
+ # Holds the TestRail response to easily access fields and
4
+ # give an easy user selector.
5
+ # TODO: Clean this UP!
6
+ class Response
7
+
8
+ @response_type = nil
9
+
10
+ def initialize(response)
11
+ @response_type = response.class.name
12
+ @json_data = ""
13
+ @raw_data = response
14
+ end
15
+
16
+ def update(response)
17
+ initialize(response)
18
+ end
19
+
20
+ def list_projects()
21
+
22
+ end
23
+
24
+ def get_value(key)
25
+ retval = nil
26
+ case @response_type
27
+ when "Array"
28
+ retval = parse_array(@raw_data, key)
29
+ when "Hash"
30
+ retval = parse_hash(@raw_data, key)
31
+ end
32
+ return retval
33
+ end
34
+
35
+ def get_id(key, value)
36
+ retval = nil
37
+ case @response_type
38
+ when "Array"
39
+ retval = parse_array_kv(@raw_data, key, value, 'id')
40
+ when "Hash"
41
+ retval = @raw_data['id']
42
+ end
43
+ return retval
44
+ end
45
+
46
+ # Returns ID of item selected via user key enter
47
+ #
48
+ def picker(key)
49
+ min_val = 1
50
+ max_val = 1
51
+ valarr = get_value(key)
52
+ puts "Options Available: "
53
+ valarr.each_with_index do |one_select, index|
54
+ dis_index = index + 1
55
+ puts "#{dis_index}) #{one_select}"
56
+ max_val = dis_index
57
+ end
58
+
59
+ puts "q) TO QUIT"
60
+ print "Enter number of your selection: "
61
+
62
+ user_choice = pick_filter(min_val, max_val, true)
63
+ puts "You SELECTED #{valarr[user_choice - 1]}"
64
+ puts ""
65
+ return valarr[user_choice - 1]
66
+ end
67
+
68
+ private
69
+
70
+ def parse_array_kv(rdata, k, v, key_return)
71
+ fnd = rdata.detect {|unhash| unhash[k] == v}
72
+ return fnd['id']
73
+ end
74
+
75
+ def parse_array(rdata, key)
76
+ retarr = []
77
+ rdata.map do |h|
78
+ retarr.push(h[key])
79
+ end
80
+ return retarr
81
+ end
82
+
83
+ def parse_hash(rdata, key)
84
+ retval = rdata[key]
85
+ end
86
+
87
+ # TODO: Add user keyboard input checks.
88
+ #
89
+ def pick_filter(min, max, add_quit = true)
90
+ guide_msg = "Valid values are from #{min} to #{max}"
91
+ if add_quit
92
+ guide_msg += " or 'q' to quit"
93
+ end
94
+ retval = nil
95
+ entered_valid = false
96
+ atmps = 0
97
+ integer_pattern = /[0-9]+/
98
+ while ! entered_valid do
99
+ user_input = $stdin.gets.chomp.downcase
100
+ if (user_input == 'q' && add_quit) || (atmps > 7)
101
+ puts "Exiting!"
102
+ exit(0)
103
+ end
104
+ if user_input =~ integer_pattern
105
+ entered_value = user_input.to_i
106
+ if entered_value >= min && entered_value <= max
107
+ retval = entered_value
108
+ entered_valid = true
109
+ end
110
+ end
111
+ if ! entered_valid
112
+ puts guide_msg
113
+ end
114
+ atmps += 1
115
+ end
116
+ return retval
117
+ end
118
+
119
+
120
+
121
+ end
@@ -0,0 +1,241 @@
1
+ require 'nokogiri'
2
+ require 'json'
3
+
4
+ # HISTORY: 1/7/2016 modified to handle just 1 file
5
+ # 3/21/2016 Allows indexing of test case
6
+ # e.g. (TestRail: [C11, C22], [C33, C44], 1) Last argument is an index (starts at 0)
7
+ # to mark test case C33 and C44. Modified method parse_trail_tag to implement this.
8
+ #
9
+ # TODO: Parse Failure element CDATA to add to comment - will require changes in request.rb, etc.
10
+ #
11
+ class ResultsParser
12
+
13
+ DEBUG_MODE = false
14
+
15
+ def initialize(results_path)
16
+ checkIfDirectoryExists(results_path)
17
+ @results_path = results_path
18
+ #xmls = getAllXMLFiles(results_path)
19
+ #readAllXMLFiles(xmls)
20
+ end
21
+
22
+ def checkIfDirectoryExists(xml_loc)
23
+ if ! File.directory?(xml_loc) && ! File.file?(xml_loc)
24
+ puts "ERROR: Cannot find the XML results directory or file:"
25
+ puts " #{xml_loc}"
26
+ puts " Please check the path of the directory where the results are located.\n\n"
27
+ exit(1)
28
+ end
29
+ end
30
+
31
+ # Retrieves all .xml files in the directory passed as parameter
32
+ # also recurssively gets subdirectory files because it uses Dir.glob
33
+ # Return: array of file names with path where it is located.
34
+ def getAllXMLFiles()
35
+ originalDir = Dir.pwd
36
+ Dir.chdir @results_path
37
+ filesArray = Array.new
38
+ debug_msg "#{@results_path}/**/*.xml"
39
+ Dir.glob("**/*.xml") do |f|
40
+ debug_msg "XML: #{f}"
41
+ filesArray << "#{@results_path}/" + f
42
+ end
43
+ Dir.chdir originalDir
44
+ return filesArray
45
+ end
46
+
47
+ def readAllXMLFiles(xml_files)
48
+ xml_files.each do |xml_file|
49
+ read_XML_file(xml_file)
50
+ end
51
+ end
52
+
53
+ # Reads XML file and uses nokogiri to parse
54
+ #
55
+ def read_XML_file(xfile)
56
+ result_array = []
57
+ if File.file?(xfile)
58
+ puts "\n\nOPENING #{xfile}"
59
+ f = File.open(xfile)
60
+ doc = Nokogiri::XML(f)
61
+ if is_squish?(doc)
62
+ result_array = squish_parser(doc)
63
+ else
64
+ result_array = rspec_parser(doc)
65
+ end
66
+ else
67
+ puts "\nNot a file: #{xfile} - verify options (-x for directory, -f for a specific file)"
68
+ end
69
+ return result_array
70
+ end
71
+
72
+ # Parses the SquishReport XML results file
73
+ def squish_parser(nokigiri_doc)
74
+ result_arr = []
75
+ verifications = nokigiri_doc.xpath("//verification")
76
+ verifications.each do |verification|
77
+ #puts "ONEVER: #{verification}"
78
+ trail_cases = parse_trail_tag(verification["name"])
79
+ result = verification.xpath("result").first
80
+ is_passed = squish_check_passed(result)
81
+ debug_msg "\nVERIFICATION: #{verification["name"]} :: #{trail_cases}"
82
+ trail_cases = parse_trail_tag(verification["name"])
83
+ trail_cases.each do |trail_case|
84
+ result_arr.push({:trail_case => trail_case, :passed => is_passed})
85
+ end
86
+ end
87
+ debug_msg "Squish FINAL: #{result_arr}"
88
+ return result_arr
89
+ end
90
+
91
+ # Parses the results of an RSPEC XML file.
92
+ # TODO:
93
+ def rspec_parser(nokigiri_doc)
94
+ result_arr = []
95
+ testsuites = nokigiri_doc.xpath("//testsuites")
96
+ testsuites.each do |suites|
97
+ testsuite = suites.xpath("./testsuite")
98
+ testsuite.each do |suite|
99
+ debug_msg "\nSUITE: #{suite["name"]} "
100
+ testsuite_name = suite["name"]
101
+ testcases = suite.xpath("./testcase")
102
+ testcases.each do |tcase|
103
+ debug_msg "TESTCASE: #{tcase}"
104
+ is_passed = false
105
+ failure = tcase.xpath("./failure")
106
+ if ! failure.nil?
107
+ is_passed = true
108
+ if failure.size > 0
109
+ is_passed = false
110
+ end
111
+ end
112
+ debug_msg " TC: #{tcase["name"]}"
113
+ testcase_name = tcase["name"]
114
+ trail_cases = parse_trail_tag(testcase_name)
115
+ trail_cases.each do |trail_case|
116
+ result_arr.push({:trail_case => trail_case, :passed => is_passed})
117
+ end
118
+ end
119
+ end
120
+ debug_msg "FINAL: #{result_arr}"
121
+ return result_arr
122
+ end
123
+ end
124
+
125
+ # Parses the 'name' attribute of <testcase> and returns all
126
+ # test cases found. Returns an array in case a single test can mark off multiple test cases.
127
+ # Ex. <testcase name="Login: should allow all valued users to successfully login (TestRail: C123, C888)
128
+ # Returns ['C123', 'C888']
129
+ def parse_trail_tag(name_line)
130
+ case_arr = []
131
+ ndex = get_index(name_line)
132
+ if ndex < 0
133
+ pattern = /\(TestRail:([Cc0-9\,\s]+)\)/
134
+ just_cases = pattern.match(name_line)
135
+ if ! just_cases.nil?
136
+ debug_msg "HowMany #{just_cases[1]}"
137
+ split_cases = just_cases[1].split(',')
138
+ split_cases.each do |onecase|
139
+ case_arr.push(onecase.strip)
140
+ end
141
+ end
142
+ debug_msg "ARR: #{case_arr}"
143
+ else
144
+ case_arr = get_indexed_cases(name_line, ndex)
145
+ end
146
+ return case_arr
147
+ end
148
+
149
+ private
150
+
151
+ # Called if the test cases are indexed as described in JIRA ticket ACI-206
152
+ #
153
+ def get_indexed_cases(name_line, nx)
154
+ cases = []
155
+ pattern = /\(TestRail:([Cc0-9\,\s\[\]\|]+)\:[0-9\s]+\)/
156
+ just_cases = pattern.match(name_line)
157
+ if ! just_cases.nil?
158
+ case_tags = just_cases[1]
159
+ debug_msg "PARSING: #{case_tags}"
160
+ if (case_tags =~ /^\s*[Cc0-9\,\s]+$/)
161
+ debug_msg "CASE COMMAS"
162
+ indexed_case = case_tags.split(',')[nx].strip
163
+ cases.push(indexed_case)
164
+ elsif (case_tags =~ /^\s*[Cc0-9\,\s\|]+$/)
165
+ debug_msg "CASE PIPES"
166
+ indexed_case = case_tags.split('|')[nx]
167
+ cases = strip_array(indexed_case.split(','))
168
+ elsif (case_tags =~ /^\s*[Cc0-9\,\s\[\]]+$/)
169
+ debug_msg "CASE BRACKETS"
170
+ subed_tags = sub_brackets(case_tags)
171
+ indexed_case = subed_tags.split('|')[nx]
172
+ cases = strip_array(indexed_case.split(','))
173
+ end
174
+ end
175
+ return cases
176
+ end
177
+
178
+ # Replaces ],[ and ], and ,[ with '|'
179
+ def sub_brackets(nline)
180
+ local_line = nline
181
+ local_line.gsub!(/\]\s*\,\s*\[/, '|')
182
+ local_line.gsub!(/\]\s*\,/, '|')
183
+ local_line.gsub!(/\,\s*\[/, '|')
184
+ return local_line.tr('[]', '')
185
+ end
186
+
187
+ # Parses for (TestRail: [C12, C13], [C21, C24], [C33]:2)
188
+ # Returns the index number if found or -1 if not found
189
+ def get_index(name_line)
190
+ index_found = -1
191
+ pattern = /\(TestRail:.*\:\s*([0-9]+)\s*\)/
192
+ just_cases = pattern.match(name_line)
193
+ if ! just_cases.nil?
194
+ index_found = just_cases[1].to_i
195
+ end
196
+ return index_found
197
+ end
198
+
199
+ def is_squish?(nokigiri_doc)
200
+ retval = false
201
+ squish_report = nokigiri_doc.xpath("/SquishReport")
202
+ if ! squish_report.nil?
203
+ if squish_report.size > 0
204
+ retval = true
205
+ end
206
+ end
207
+ return retval
208
+ end
209
+
210
+ def squish_check_passed(result_doc)
211
+ retval = false
212
+ if ! result_doc.nil?
213
+ debug_msg "RESULT: #{result_doc}"
214
+ get_type = result_doc['type']
215
+ if get_type.downcase == "pass"
216
+ retval = true
217
+ end
218
+ end
219
+ debug_msg "PASED: #{retval}"
220
+ return retval
221
+ end
222
+
223
+ # Removed all white spaces before/after each cell in
224
+ # an array. Returns array.
225
+ def strip_array(clothed_arr)
226
+ fully_stripped = []
227
+ clothed_arr.each do |onecase|
228
+ fully_stripped.push(onecase.strip)
229
+ end
230
+ return fully_stripped
231
+ end
232
+
233
+ def debug_msg(msg)
234
+ if DEBUG_MODE
235
+ puts msg
236
+ end
237
+ end
238
+
239
+ end
240
+
241
+
@@ -0,0 +1,12 @@
1
+ require_relative 'config_file'
2
+
3
+ ### MAIN
4
+ #
5
+ #
6
+
7
+ confile = "trail_config.yml"
8
+ currpath = File.dirname(__FILE__)
9
+
10
+ full_filepath = "#{currpath}/#{confile}"
11
+ cf = ConfigFile.new(full_filepath)
12
+ cf.read_configfile