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

1# -*- coding: utf-8 -*- 

2""" Engine to generate obtain TestRail data and prepare reports """ 

3 

4from requests.exceptions import ReadTimeout 

5from testrail_api import TestRailAPI # type: ignore 

6 

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 

11 

12 

13class ATCoverageReporter: 

14 """Class for data generator for automation coverage reports (or similar data) from TestRails""" 

15 

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 

31 

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 

62 

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 

66 

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 

81 

82 def __get_all_sections(self, project_id=None, suite_id=None): 

83 """ 

84 Wrapper to get all sections of TestRails project/suite 

85 

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 

121 

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 

132 

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 

193 

194 cases_list = cases_list + response["cases"] # pylint: disable=unsubscriptable-object 

195 criteria = response["_links"]["next"] # pylint: disable=unsubscriptable-object 

196 

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 

206 

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) 

219 

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 

271 

272 def test_case_by_priority(self, project=None, suite=None): 

273 """ 

274 Generates data for pie/line chart with priority distribution 

275 

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 

290 

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). 

300 

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