trail_marker 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.
@@ -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