Coverage for testrail_api_reporter/engines/at_coverage_reporter.py: 9%

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

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

3

4 from requests.exceptions import ReadTimeout

5 from testrail_api import TestRailAPI # type: ignore

6

7 from ..utils.case_stat import CaseStat

8 from ..utils.csv_parser import CSVParser

9 from ..utils.logger_config import setup_logger, DEFAULT_LOGGING_LEVEL

10 from ..utils.reporter_utils import format_error, init_get_cases_process

11

12

13 class 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