itriagetestrail 1.0.36

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,187 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # TestRail API binding for Ruby (API v2, available since TestRail 3.0)
5
+ #
6
+ # Learn more:
7
+ #
8
+ # http://docs.gurock.com/testrail-api2/start
9
+ # http://docs.gurock.com/testrail-api2/accessing
10
+ #
11
+ # Copyright Gurock Software GmbH. See license.md for details.
12
+ #
13
+
14
+ require 'net/http'
15
+ require 'net/https'
16
+ require 'uri'
17
+ require 'json'
18
+
19
+ module TestRail
20
+ class APIClient
21
+ @url = ''
22
+ @user = ''
23
+ @password = ''
24
+ @response_code = nil
25
+
26
+ attr_accessor :user
27
+ attr_accessor :password
28
+ attr_reader :response_code
29
+
30
+ def initialize(base_url)
31
+ valid_regex = %r{/\/$/}
32
+ base_url += '/' unless valid_regex.match(base_url)
33
+ @url = base_url + 'index.php?/api/v2/'
34
+ end
35
+
36
+ #
37
+ # Send Get
38
+ #
39
+ # Issues a GET request (read) against the API and returns the result
40
+ # (as Ruby hash).
41
+ #
42
+ # Arguments:
43
+ #
44
+ # uri The API method to call including parameters
45
+ # (e.g. get_case/1)
46
+ #
47
+ def send_get(uri)
48
+ res = read_cache(uri)
49
+ res || _send_request('GET', uri, nil, true)
50
+ end
51
+
52
+ #
53
+ # Send POST
54
+ #
55
+ # Issues a POST request (write) against the API and returns the result
56
+ # (as Ruby hash).
57
+ #
58
+ # Arguments:
59
+ #
60
+ # uri The API method to call including parameters
61
+ # (e.g. add_case/1)
62
+ # data The data to submit as part of the request (as
63
+ # Ruby hash, strings must be UTF-8 encoded)
64
+ #
65
+ def send_post(uri, data)
66
+ _send_request('POST', uri, data)
67
+ end
68
+
69
+ private
70
+
71
+ def make_connection(url)
72
+ conn = Net::HTTP.new(url.host, url.port)
73
+ if url.scheme == 'https'
74
+ conn.use_ssl = true
75
+ conn.verify_mode = OpenSSL::SSL::VERIFY_NONE
76
+ end
77
+ conn
78
+ end
79
+
80
+ def _parse_result(response)
81
+ if response.body && !response.body.empty?
82
+ JSON.parse(response.body)
83
+ else
84
+ {}
85
+ end
86
+ end
87
+
88
+ def _error_check(response, result)
89
+ return if response.code == '200'
90
+ error = if !result.nil? && result.key?('error')
91
+ '"' + result['error'] + '"'
92
+ else
93
+ 'No additional error message received'
94
+ end
95
+ raise APIError, format('TestRail API returned HTTP %<response_code>s\n%<error>s',
96
+ response_code: response.code, error: error)
97
+ end
98
+
99
+ def _send_request(method, uri, data, write_cache = false)
100
+ url = URI.parse(@url + uri)
101
+ if method == 'POST'
102
+ request = Net::HTTP::Post.new(url.path + '?' + url.query)
103
+ request.body = JSON.dump(data)
104
+ else
105
+ request = Net::HTTP::Get.new(url.path + '?' + url.query)
106
+ end
107
+ request.basic_auth(@user, @password)
108
+ request.add_field('Content-Type', 'application/json')
109
+
110
+ conn = make_connection(url)
111
+
112
+ retry_count = 0
113
+ while retry_count < 10
114
+ response = conn.request(request)
115
+ @response_code = response.code
116
+ if @response_code == '429'
117
+ write_http_status_to_file('testrail_429s', url)
118
+ sleep(response.header['retry-after'].to_i)
119
+ elsif @response_code == '500'
120
+ puts response.to_s
121
+ # this might require different handling for 500 'Deadlock found when
122
+ # trying to get lock; try restarting transaction'
123
+ sleep(2)
124
+ else
125
+ write_http_status_to_file('testrail_200s', url)
126
+ break
127
+ end
128
+ retry_count += 1
129
+ end
130
+
131
+ result = _parse_result(response)
132
+
133
+ _error_check(response, result)
134
+
135
+ if write_cache
136
+ cache_to_file(uri, result)
137
+ end
138
+
139
+ result
140
+ end
141
+
142
+ # This method is only used publicly
143
+ def write_http_status_to_file(filename, endpoint)
144
+ Dir.mkdir('./tmp') unless File.exist?('./tmp')
145
+ file = File.open("./tmp/#{filename}", 'a')
146
+ file.write("#{Time.now},#{endpoint}\n")
147
+ file.close
148
+ end
149
+
150
+ def cache_to_file(url, content)
151
+ payload = {'response' => content}
152
+ Dir.mkdir('./tmp') unless File.exist?('./tmp')
153
+ filename = sanitize_filename(url)
154
+ file = File.open("./tmp/#{filename}", 'w')
155
+ file.write(payload.to_json)
156
+ file.close
157
+ end
158
+
159
+ def read_cache(url)
160
+ filename = sanitize_filename(url)
161
+ return unless File.exist?("./tmp/#{filename}")
162
+ file = File.open("./tmp/#{filename}", 'r')
163
+ content = JSON.parse(file.read)
164
+ file.close
165
+ content[:response]
166
+ end
167
+
168
+ def sanitize_filename(filename)
169
+ # Split the name when finding a period which is preceded by some
170
+ # character, and is followed by some character other than a period,
171
+ # if there is no following period that is followed by something
172
+ # other than a period (yeah, confusing, I know)
173
+ fn = filename.split /(?<=.)\.(?=[^.])(?!.*\.[^.])/m
174
+
175
+ # We now have one or two parts (depending on whether we could find
176
+ # a suitable period). For each of these parts, replace any unwanted
177
+ # sequence of characters with an underscore
178
+ fn.map! { |s| s.gsub /[^a-z0-9\-]+/i, '_' }
179
+
180
+ # Finally, join the parts with a period and return the result
181
+ return fn.join '.'
182
+ end
183
+ end
184
+
185
+ class APIError < StandardError
186
+ end
187
+ end
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'tzinfo'
4
+
5
+ module Itriagetestrail
6
+ module Milestones
7
+ def normalize_origin
8
+ case @testrail_config[:origin]
9
+ when 'prd', 'production', 'origin/production'
10
+ 'Production'
11
+ when 'stg', 'staging', 'origin/staging'
12
+ 'Staging'
13
+ when 'dev', 'development', 'origin/development'
14
+ 'Development'
15
+ when 'local', ''
16
+ 'Local'
17
+ when 'master', 'origin/master'
18
+ 'Master'
19
+ else
20
+ 'Dev Branch'
21
+ end
22
+ end
23
+
24
+ # Establish the milestone name based on origin passed in,
25
+ # usually origin represents a branch or environment
26
+ def normalize_milestone
27
+ if @testrail_config[:milestone].nil? || @testrail_config[:milestone].empty?
28
+ normalize_origin
29
+ else
30
+ @testrail_config[:milestone]
31
+ end
32
+ end
33
+
34
+ # returns timestamp for begining of first day of current quarter
35
+ def milestone_period_start
36
+ time_zone = TZInfo::Timezone.get('America/Denver')
37
+ current_year = time_zone.now.year
38
+ month = time_zone.now.mon
39
+
40
+ # determine which quarter we are in
41
+ if month <= 3
42
+ time_zone.utc_to_local(Time.utc(current_year, 1, 1))
43
+ elsif month <= 6
44
+ time_zone.utc_to_local(Time.utc(current_year, 4, 1))
45
+ elsif month <= 9
46
+ time_zone.utc_to_local(Time.utc(current_year, 7, 1))
47
+ else
48
+ time_zone.utc_to_local(Time.utc(current_year, 10, 1))
49
+ end
50
+ end
51
+
52
+ # determine the due date (end of quarter) for a new milestone being added
53
+ def milestone_due_date
54
+ time_zone = TZInfo::Timezone.get('America/Denver')
55
+ current_year = time_zone.now.year
56
+ month = time_zone.now.mon
57
+
58
+ # determine which quarter we are in
59
+ if month <= 3
60
+ Time.utc(current_year, 3, 31).strftime('%s')
61
+ elsif month <= 6
62
+ Time.utc(current_year, 6, 30).strftime('%s')
63
+ elsif month <= 9
64
+ Time.utc(current_year, 9, 30).strftime('%s')
65
+ else
66
+ Time.new(current_year, 12, 31).strftime('%s')
67
+ end
68
+ end
69
+
70
+ # returns the id for a requested milestone by name, or creates one if the milestone does not exist
71
+ def fetch_milestone(requested_milestone_name)
72
+ milestones = @client.send_get("get_milestones/#{@project_id}")
73
+ res = -1
74
+ milestones.each do |milestone|
75
+ res = milestone['id'] if milestone['name'] == requested_milestone_name
76
+ end
77
+
78
+ if res == -1
79
+ # We need to add the milestone to TestRail
80
+
81
+ body = {
82
+ name: requested_milestone_name,
83
+ due_on: milestone_due_date
84
+ }
85
+
86
+ res = @client.send_post("add_milestone/#{@project_id}", body)['id']
87
+ end
88
+ res
89
+ end
90
+
91
+ # return a standardized name for a milestone to be archived
92
+ def milestone_archive_name(milestone_name, date)
93
+ year = date.year
94
+ month = date.mon
95
+
96
+ if month <= 3
97
+ "#{milestone_name} #{year}-Q1"
98
+ elsif month <= 6
99
+ "#{milestone_name} #{year}-Q2"
100
+ elsif month <= 9
101
+ "#{milestone_name} #{year}-Q3"
102
+ else
103
+ "#{milestone_name} #{year}-Q4"
104
+ end
105
+ end
106
+
107
+ # return all the runs associated with an existing milestone
108
+ def milestone_runs(milestone_name)
109
+ # use the matching milestone id for project
110
+ milestone_id = fetch_milestone(milestone_name)
111
+
112
+ # fetch all test runs associated with the milestone id for project
113
+ @client.send_get("get_runs/#{@project_id}&milestone_id=#{milestone_id}&is_completed=1") || []
114
+ end
115
+
116
+ # testrail call to rename a milestone (for archiving)
117
+ def rename_milestone(id, new_name)
118
+ # TODO: rename milestone with previous_milestone
119
+ body = { name: new_name }
120
+ @client.send_post("update_milestone/#{id}", body)['id']
121
+ end
122
+
123
+ # this archives a milestone at the turn of a quarter and creates a new one in its place
124
+ def reset_milestone(milestone_name)
125
+ runs = milestone_runs(milestone_name)
126
+ return if runs.empty?
127
+ last_run_time = Time.at(runs.last['completed_on'])
128
+
129
+ # if last run time is smaller than period start, do below
130
+ return unless last_run_time < milestone_period_start
131
+ rename_milestone(@milestone_id, milestone_archive_name(milestone_name, last_run_time))
132
+ @milestone_id = fetch_milestone(@milestone_name)
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'itriagetestrail/version'
4
+ require 'itriagetestrail/testrail_binding'
5
+ require 'itriagetestrail/pool'
6
+ require 'tzinfo'
7
+
8
+ module Itriagetestrail
9
+ module Projects
10
+ # populate projects instance variable with all projects objects in the testrail site
11
+ def projects
12
+ @projects ||= @client.send_get('get_projects')
13
+ end
14
+
15
+ def project_by_name(name)
16
+ res = -1
17
+ projects.each do |project|
18
+ res = project if project['name'] == name
19
+ end
20
+ res
21
+ end
22
+
23
+ # return the project object for a given project id
24
+ def project_by_id(id)
25
+ res = -1
26
+ projects.each do |project|
27
+ res = project if project['id'] == id.to_i
28
+ end
29
+ res
30
+ end
31
+
32
+ # set the project_id by a requested project from config/environment variable
33
+ def set_project
34
+ requested_id = @testrail_config[:projectId]
35
+ case requested_id
36
+ when nil, ''
37
+ # a project id was not provided, fetch it from TestRail by project name
38
+ res = project_by_name(@testrail_config[:projectName])
39
+ if res == -1
40
+ @execute = false
41
+ return
42
+ end
43
+ @project_id = res['id']
44
+ @suite_mode = res['suite_mode']
45
+ else
46
+ # use the requested project id
47
+ @project_id = requested_id
48
+ @suite_mode = project_by_id(@project_id)['suite_mode']
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Itriagetestrail
4
+ module Sections
5
+ # TestRail Sections
6
+ def testrail_sections
7
+ case @suite_mode
8
+ when 2, 3
9
+ @suite_id = testrail_suite_id(@suite_name)
10
+ @sections = @client.send_get("get_sections/#{@project_id}&suite_id=#{@suite_id}")
11
+ else
12
+ @sections = @client.send_get("get_sections/#{@project_id}")
13
+ end
14
+ end
15
+
16
+ def testrail_section_id(section_title)
17
+ res = -1
18
+ @sections.each do |section|
19
+ res = section['id'] if section['name'] == section_title
20
+ end
21
+ res
22
+ end
23
+
24
+ def add_testrail_section(section_title)
25
+ body = if @suite_name
26
+ {
27
+ name: section_title,
28
+ suite_id: testrail_suite_id(@suite_name)
29
+ }
30
+ else
31
+ {
32
+ name: section_title
33
+ }
34
+ end
35
+
36
+ res = @client.send_post("add_section/#{@project_id}", body)
37
+
38
+ testrail_section = res['id']
39
+
40
+ # re-establish sections
41
+ testrail_sections
42
+
43
+ testrail_section
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Itriagetestrail
4
+ module Suites
5
+ # TestRail Suites
6
+ def testrail_suites
7
+ case @suite_mode
8
+ when 2, 3
9
+ @suites = @client.send_get("get_suites/#{@project_id}")
10
+ end
11
+ end
12
+
13
+ def testrail_suite_id(suite_name)
14
+ res = -1
15
+ @suites.each do |suite|
16
+ res = suite['id'] if suite['name'] == suite_name
17
+ end
18
+ res
19
+ end
20
+
21
+ def add_testrail_suite(suite_name)
22
+ body = { name: suite_name }
23
+ res = @client.send_post("add_suite/#{@project_id}", body)
24
+ testrail_suite = res['id']
25
+
26
+ # re-establish suites
27
+ testrail_suites
28
+
29
+ testrail_suite
30
+ end
31
+ end
32
+ end