Coverage for testrail_api_reporter/engines/at_coverage_reporter.py: 9%
156 statements
« prev ^ index » next coverage.py v7.6.1, created at 2024-08-29 15:21 +0200
« prev ^ index » next coverage.py v7.6.1, created at 2024-08-29 15:21 +0200
1# -*- coding: utf-8 -*-
2""" Engine to generate obtain TestRail data and prepare reports """
4from requests.exceptions import ReadTimeout
5from testrail_api import TestRailAPI # type: ignore
7from ..utils.case_stat import CaseStat
8from ..utils.csv_parser import CSVParser
9from ..utils.logger_config import setup_logger, DEFAULT_LOGGING_LEVEL
10from ..utils.reporter_utils import format_error, init_get_cases_process
13class ATCoverageReporter:
14 """Class for data generator for automation coverage reports (or similar data) from TestRails"""
16 def __init__(
17 self,
18 url: str,
19 email: str,
20 password: str,
21 priority=None,
22 project=None,
23 type_platforms=None,
24 automation_platforms=None,
25 suite_id=None,
26 logger=None,
27 log_level=DEFAULT_LOGGING_LEVEL,
28 ):
29 """
30 General init
32 :param url: url of TestRail, string, required
33 :param email: email of TestRail user with proper access rights, string, required
34 :param password: password of TestRail user with proper access rights, string, required
35 :param priority: default priority level for testcases, integer, usually it's "4" within following list:
36 ['Low', 'Medium', 'High', 'Critical']
37 :param project: project id, integer, required
38 :param type_platforms: list of dicts, with sections ids, where dict = {'name': 'UI',
39 'sections': [16276]}
40 :param automation_platforms: list of dicts of automation platforms, dict = {'name': 'Desktop Chrome',
41 'internal_name': 'type_id',
42 'sections': [16276],
43 'auto_code': 3,
44 'na_code': 4}
45 :param suite_id: suite id, integer, optional, if no suite-management is activated
46 :param logger: logger object, optional
47 :param log_level: logging level, optional, by default is logging.DEBUG
48 """
49 if not logger:
50 self.___logger = setup_logger(name="ATCoverageReporter", log_file="ATCoverageReporter.log", level=log_level)
51 else:
52 self.___logger = logger
53 self.___logger.debug("Initializing AT Coverage Reporter")
54 if url is None or email is None or password is None:
55 raise ValueError("No TestRails credentials are provided!")
56 self.__automation_platforms = automation_platforms # should be passed with specific TestRails sections
57 self.__type_platforms = type_platforms # should be passed with specific TestRails sections
58 self.__project = project
59 self.__priority = priority
60 self.__api = TestRailAPI(url=url, email=email, password=password)
61 self.__suite_id = suite_id
63 def __get_sections(self, parent_list: list, project=None, suite_id=None):
64 """
65 Wrapper to get all sections ids of TestRails project/suite
67 :param parent_list: list for all sections, initially a top section should be passed
68 :param project: project id, integer, required
69 :param suite_id: suite id, integer, optional, an if no suite-management is activated
70 :return: list with ids all the sections
71 """
72 project = project if project else self.__project
73 suite_id = suite_id if suite_id else self.__suite_id
74 if not project:
75 raise ValueError("No project specified, report aborted!")
76 all_sections = self.__get_all_sections(project_id=project, suite_id=suite_id)
77 for section in all_sections:
78 if section["parent_id"] in parent_list:
79 parent_list.append(section["id"])
80 return parent_list
82 def __get_all_sections(self, project_id=None, suite_id=None):
83 """
84 Wrapper to get all sections of TestRails project/suite
86 :param project_id: project id, integer, required
87 :param suite_id: suite id, integer, optional, if no suite-management is activated
88 :return: list, contains all the sections
89 """
90 project = project_id if project_id else self.__project
91 suite_id = suite_id if suite_id else self.__suite_id
92 sections = []
93 if not project:
94 raise ValueError("No project specified, report aborted!")
95 first_run = True
96 criteria = None
97 response = None
98 while criteria is not None or first_run:
99 if first_run:
100 try:
101 response = self.__api.sections.get_sections(project_id=project, suite_id=suite_id)
102 except Exception as error: # pylint: disable=broad-except
103 self.___logger.error(
104 "Get sections failed. Please validate your settings!\nError%s", format_error(error)
105 )
106 return None
107 first_run = False
108 elif response["_links"]["next"] is not None: # pylint: disable=unsubscriptable-object
109 offset = int(
110 response["_links"]["next"] # pylint: disable=unsubscriptable-object
111 .partition("offset=")[2]
112 .partition("&")[0]
113 )
114 response = self.__api.sections.get_sections(project_id=project, suite_id=suite_id, offset=offset)
115 sections = sections + response["sections"] # pylint: disable=unsubscriptable-object
116 criteria = response["_links"]["next"] # pylint: disable=unsubscriptable-object
117 self.___logger.debug(
118 "Found %s existing sections in TestRails for project %s, suite %s", len(sections), project, suite_id
119 )
120 return sections
122 def __get_all_cases(
123 self,
124 project_id=None,
125 suite_id=None,
126 section_id=None,
127 priority_id=None,
128 retries=3,
129 ):
130 """
131 Wrapper to get all test cases for selected project, suite, section and priority
133 :param project_id: project id, integer, required
134 :param suite_id: suite id, integer, optional, if no suite-management is activated
135 :param section_id: section id, integer, section where testcases should be found, optional
136 :param priority_id: priority, list of integers, id of priority for test case to search
137 :param retries: number of retries, integer, optional
138 :return: list with all cases
139 """
140 project_id = project_id if project_id else self.__project
141 suite_id = suite_id if suite_id else self.__suite_id
142 cases_list, first_run, criteria, response, retry = init_get_cases_process()
143 while criteria is not None or first_run:
144 if first_run:
145 try:
146 response = self.__api.cases.get_cases(
147 project_id=project_id,
148 suite_id=suite_id,
149 section_id=section_id,
150 priority_id=priority_id,
151 )
152 except ReadTimeout as error:
153 if retry < retries:
154 retry += 1
155 self.___logger.debug("Timeout error, retrying %s/%s...", retry, retries)
156 continue
157 raise ValueError(
158 f"Get cases failed. Please validate your settings!\nError{format_error(error)}"
159 ) from error
160 except Exception as error: # pylint: disable=broad-except
161 raise ValueError(
162 f"Get cases failed. Please validate your settings!\nError{format_error(error)}"
163 ) from error
164 first_run = False
165 retry = 0
166 elif response["_links"]["next"] is not None: # pylint: disable=unsubscriptable-object
167 offset = int(
168 response["_links"]["next"] # pylint: disable=unsubscriptable-object
169 .partition("offset=")[2]
170 .partition("&")[0]
171 )
172 try:
173 response = self.__api.cases.get_cases(
174 project_id=project_id,
175 suite_id=suite_id,
176 section_id=section_id,
177 priority_id=priority_id,
178 offset=offset,
179 )
180 except ReadTimeout as error:
181 if retry < retries:
182 retry += 1
183 self.___logger.debug("Timeout error, retrying %s/%s...", retry, retries)
184 continue
185 raise ValueError(
186 f"Get cases failed. Please validate your settings!\nError{format_error(error)}"
187 ) from error
188 except Exception as error:
189 raise ValueError(
190 f"Get cases failed. Please validate your settings!\nError{format_error(error)}"
191 ) from error
192 retry = 0
194 cases_list = cases_list + response["cases"] # pylint: disable=unsubscriptable-object
195 criteria = response["_links"]["next"] # pylint: disable=unsubscriptable-object
197 self.___logger.debug(
198 "Found %s existing tests in TestRails for project %s, suite %s, section %s, priority %s",
199 len(cases_list),
200 project_id,
201 suite_id,
202 section_id,
203 priority_id,
204 )
205 return cases_list
207 def automation_state_report(
208 self,
209 priority=None,
210 project=None,
211 automation_platforms=None,
212 filename_pattern="current_automation",
213 suite=None,
214 ):
215 """
216 Generates data of automation coverage for stacked bar chart or staked line chart
217 with values "Automated", "Not automated", "N/A". This distribution generated using values form fields
218 with "internal_name" for specific parent section(s)
220 :param priority: priority, list of integers, id of priority for test case to search
221 :param project: project id, integer, required
222 :param automation_platforms: list of dicts of automation platforms, dict = {'name': 'Desktop Chrome',
223 'internal_name': 'type_id',
224 'sections': [16276],
225 'auto_code': 3,
226 'na_code': 4}
227 :param filename_pattern: pattern for filename, string
228 :param suite: suite id, integer, optional, if no suite-management is activated
229 :return: list of results in CaseStat format
230 """
231 project = project if project else self.__project
232 suite = suite if suite else self.__suite_id
233 priority = priority if priority else self.__priority
234 automation_platforms = automation_platforms if automation_platforms else self.__automation_platforms
235 if not project:
236 raise ValueError("No project specified, report aborted!")
237 if not priority:
238 raise ValueError("No critical priority specified, report aborted!")
239 if not automation_platforms:
240 raise ValueError("No automation platforms specified, report aborted!")
241 self.___logger.debug("=== Starting generation of report for current automation state ===")
242 index = 0
243 results = []
244 for platform in automation_platforms:
245 self.___logger.debug("Processing platform %s", platform["name"])
246 results.append(CaseStat(platform["name"]))
247 sections = self.__get_sections(platform["sections"])
248 for section in sections:
249 self.___logger.debug(" Passing section %s", section)
250 cases = self.__get_all_cases(
251 project_id=project,
252 suite_id=suite,
253 section_id=section,
254 priority_id=priority,
255 )
256 results[index].set_total(results[index].get_total() + len(cases))
257 for case in cases:
258 if case[platform["internal_name"]] == platform["auto_code"]:
259 results[index].set_automated(results[index].get_automated() + 1)
260 else:
261 if case[platform["internal_name"]] == platform["na_code"]:
262 results[index].set_not_applicable(results[index].get_not_applicable() + 1)
263 results[index].set_not_automated(
264 results[index].get_total() - results[index].get_automated() - results[index].get_not_applicable()
265 )
266 # save history data
267 filename = f"{filename_pattern}_{results[index].get_name().replace(' ', '_')}.csv"
268 CSVParser(log_level=self.___logger.level, filename=filename).save_history_data(report=results[index])
269 index += 1
270 return results
272 def test_case_by_priority(self, project=None, suite=None):
273 """
274 Generates data for pie/line chart with priority distribution
276 :param project: project id, integer, required
277 :param suite: suite id, integer, optional, if no suite-management is activated
278 :return: list with values (int) for bar chart
279 """
280 project = project if project else self.__project
281 suite = suite if suite else self.__suite_id
282 if not project:
283 raise ValueError("No project specified, report aborted!")
284 self.___logger.debug("=== Starting generation of report for test case priority distribution ===")
285 results = []
286 for i in range(1, 5):
287 self.___logger.debug("Processing priority %s", str(i))
288 results.append(len(self.__get_all_cases(project_id=project, suite_id=suite, priority_id=str(i))))
289 return results
291 def test_case_by_type(
292 self,
293 project=None,
294 type_platforms=None,
295 filename_pattern="current_area_distribution",
296 suite=None,
297 ):
298 """
299 Generates data for pie/line chart with distribution by type of platforms (guided by top section).
301 :param project: project id, integer, required
302 :param type_platforms: list of dicts, with sections ids, where dict = {'name': 'UI',
303 'sections': [16276]}
304 :param filename_pattern: pattern for filename, string
305 :param suite: suite id, integer, optional, if no suite-management is activated
306 :return: list with values (int) for bar chart
307 """
308 type_platforms = type_platforms if type_platforms else self.__type_platforms
309 project = project if project else self.__project
310 suite = suite if suite else self.__suite_id
311 if not project:
312 raise ValueError("No project specified, report aborted!")
313 if not type_platforms:
314 raise ValueError("No platform types are provided, report aborted!")
315 project = project if project else self.__project
316 self.___logger.debug("=== Starting generation of report for test case type distribution ===")
317 index = 0
318 results = []
319 for platform in type_platforms:
320 self.___logger.debug("Processing platform %s", platform["name"])
321 results.append(CaseStat(platform["name"]))
322 sections = self.__get_sections(platform["sections"])
323 for section in sections:
324 self.___logger.debug(" Passing section %s", section)
325 cases = self.__get_all_cases(project_id=project, suite_id=suite, section_id=section)
326 results[index].set_total(results[index].get_total() + len(cases))
327 # save history data
328 filename = f"{filename_pattern}_{results[index].get_name().replace(' ', '_')}.csv"
329 CSVParser(log_level=self.___logger.level, filename=filename).save_history_data(report=results[index])
330 index += 1
331 return results