Coverage for genbadge/main.py: 84%

129 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-11-10 20:37 +0000

1# Authors: Sylvain MARIE <sylvain.marie@se.com> 

2# + All contributors to <https://github.com/smarie/python-genbadge> 

3# 

4# License: 3-clause BSD, <https://github.com/smarie/python-genbadge/blob/master/LICENSE> 

5try: 

6 from pathlib import Path 

7except ImportError: # pragma: no cover 

8 from pathlib2 import Path # python 2 

9 

10import click 

11 

12 

13from .utils_junit import get_test_stats, get_tests_badge 

14from .utils_coverage import get_coverage_badge, get_coverage_stats 

15from .utils_flake8 import get_flake8_stats, get_flake8_badge 

16 

17try: 

18 FileNotFoundError 

19except NameError: 

20 FileNotFoundError = IOError 

21 

22 

23INFILE_HELP_TMP = "An alternate %s file to read. '-' is supported and means <stdin>." 

24OUTFILE_BADGE_HELP = ("An alternate SVG badge file to write to. '-' is supported and means <stdout>. Note that in this " 

25 "case no other message will be printed to <stdout>. In particular the verbose flag will have no " 

26 "effect.") 

27NAME_HELP = ("An alternate SVG badge text name to display on the left-hand side of the badge.") 

28WITH_NAME_HELP = ("Indicates if a badge should be generated with or without the left-hand side of the badge.") 

29SHIELDS_HELP = ("Indicates if badges should be generated using the shields.io HTTP API (default) or the local SVG file " 

30 "template included.") 

31VERBOSE_HELP = ("Use this flag to print details to stdout during the badge generation process. Note that this flag has " 

32 "no effect when '-' is used as output, since the badge is written to <stdout>. It also has no effect " 

33 "when the silent flag `-s` is used.") 

34SILENT_HELP = ("When this flag is active nothing will be written to stdout. Note that this flag has no effect when '-' " 

35 "is used as the output file.") 

36 

37 

38@click.group() 

39def genbadge(): 

40 """ 

41 Commandline utility to generate badges. 

42 To get help on each command use: 

43 

44 genbadge <cmd> --help 

45 

46 """ 

47 pass 

48 

49 

50@genbadge.command(name="tests", 

51 short_help="Generate a badge for the test results (e.g. from a junit.xml).") 

52@click.option('-i', '--input-file', type=click.File('rt'), help=INFILE_HELP_TMP % "test results XML") 

53@click.option('-o', '--output-file', type=click.File('wt'), help=OUTFILE_BADGE_HELP) 

54@click.option('-n', '--name', type=str, default="tests", help=NAME_HELP) 

55@click.option('-t', '--threshold', type=float, 

56 help="An optional success percentage threshold to use. The command will fail with exit code 1 if the" 

57 "actual success percentage is strictly less than the provided value.") 

58@click.option('--withname/--noname', type=bool, default=True, help=WITH_NAME_HELP) 

59@click.option('-w/-l', '--webshields/--local', type=bool, default=True, help=SHIELDS_HELP) 

60@click.option('-v', '--verbose', type=bool, default=False, is_flag=True, help=VERBOSE_HELP) 

61@click.option('-s', '--silent', type=bool, default=False, is_flag=True, help=SILENT_HELP) 

62def gen_tests_badge( 

63 input_file=None, 

64 output_file=None, 

65 name=None, 

66 threshold=None, 

67 withname=None, 

68 webshields=None, 

69 verbose=None, 

70 silent=None 

71): 

72 """ 

73 This command generates a badge for the test results, from an XML file in the 

74 junit format. Such a file can be for example generated from python pytest 

75 using the --junitxml flag, or from java junit. 

76 

77 By default the input file is the relative `./reports/junit/junit.xml` and 

78 the output file is `./tests-badge.svg`. You can change these settings with 

79 the `-i/--input_file` and `-o/--output-file` options. 

80 

81 By default the badge will have the name "tests" as the left-hand side text. 

82 You can change these settings with the `-n/--name` option. The left-hand side 

83 text can be left blank with `-n ""` or have the left-hand side of the badge 

84 completely removed by passing `--noname`. 

85 

86 You can use the verbose flag `-v/--verbose` to display information on the 

87 input file contents, for verification. 

88 

89 The resulting badge will by default look like this: [tests | 6/12] 

90 where 6 is the number of tests that have run successfully, and 12 is the 

91 total number of tests minus the number of skipped tests. When all tests 

92 pass with success, the badge simply shows the number of tests [tests | 12]. 

93 You can change the appearance of the badge with the --format option (not 

94 implemented, todo). 

95 

96 The success percentage is defined as 6/12 = 50.0%. You can use the 

97 `-t/--threshold` flag to setup a minimum success percentage required. If the 

98 success percentage is below the threshold, an error will be raised and the 

99 badge will not be generated. 

100 """ 

101 # Process i/o files 

102 input_file, input_file_path = _process_infile(input_file, "reports/junit/junit.xml") 

103 output_file, output_file_path, is_stdout = _process_outfile(output_file, "tests-badge.svg") 

104 

105 # First retrieve the success percentage from the junit xml 

106 try: 

107 test_stats = get_test_stats(junit_xml_file=input_file) 

108 except FileNotFoundError: 

109 raise click.exceptions.FileError(input_file, hint="File not found") 

110 

111 if not silent and verbose and not is_stdout: 

112 click.echo("""Test statistics parsed successfully from %r 

113 - Nb tests: Total (%s) = Success (%s) + Skipped (%s) + Failed (%s) + Errors (%s) 

114 - Success percentage: %.2f%% (%s / %s) (Skipped tests are excluded) 

115""" % (input_file_path, test_stats.total_with_skipped, test_stats.success, test_stats.skipped, test_stats.failed, 

116 test_stats.errors, test_stats.success_percentage, test_stats.success, test_stats.total_without_skipped)) 

117 

118 # sanity check 

119 if test_stats.total_with_skipped != test_stats.success + test_stats.skipped + test_stats.failed + test_stats.errors: 119 ↛ 120line 119 didn't jump to line 120, because the condition on line 119 was never true

120 raise click.exceptions.ClickException( 

121 "Inconsistent junit results: the sum of all kind of tests is not equal to the total. Please report this " 

122 "issue if you think your file is correct. Details: %r" % test_stats 

123 ) 

124 

125 # Validate against the threshold 

126 if threshold is not None and test_stats.success_percentage < threshold: 

127 raise click.exceptions.ClickException( 

128 "Success percentage %s%% is strictly lower than required threshold %s%%" 

129 % (float(test_stats.success_percentage), threshold) 

130 ) 

131 

132 # Set badge name 

133 clear_left_txt = False 

134 if not withname: 134 ↛ 135line 134 didn't jump to line 135, because the condition on line 134 was never true

135 name = "" # removes left side of badge 

136 elif not name.strip(): 136 ↛ 137line 136 didn't jump to line 137, because the condition on line 136 was never true

137 name = "###" # blank text to replace 

138 clear_left_txt = True # keep left side of badge but remove text 

139 

140 # Generate the badge 

141 badge = get_tests_badge(test_stats, name) 

142 badge.write_to( 

143 output_file if is_stdout else output_file_path, 

144 use_shields=webshields, 

145 clear_left_txt=clear_left_txt 

146 ) 

147 

148 if not silent and not is_stdout: 

149 click.echo("SUCCESS - Tests badge created: %r" % str(output_file_path)) 

150 

151 

152@genbadge.command(name="coverage", 

153 short_help="Generate a badge for the coverage results (e.g. from a coverage.xml).") 

154@click.option('-i', '--input-file', type=click.File('rt'), help=INFILE_HELP_TMP % "coverage results XML") 

155@click.option('-o', '--output-file', type=click.File('wt'), help=OUTFILE_BADGE_HELP) 

156@click.option('-n', '--name', type=str, default="coverage", help=NAME_HELP) 

157@click.option('--withname/--noname', type=bool, default=True, help=WITH_NAME_HELP) 

158@click.option('-w/-l', '--webshields/--local', type=bool, default=True, help=SHIELDS_HELP) 

159@click.option('-v', '--verbose', type=bool, default=False, is_flag=True, help=VERBOSE_HELP) 

160@click.option('-s', '--silent', type=bool, default=False, is_flag=True, help=SILENT_HELP) 

161def gen_coverage_badge( 

162 input_file=None, 

163 output_file=None, 

164 name=None, 

165 withname=None, 

166 webshields=None, 

167 verbose=None, 

168 silent=None 

169): 

170 """ 

171 This command generates a badge for the coverage results, from an XML file in 

172 the 'coverage' format. Such a file can be for example generated using the 

173 python `coverage` tool, or java `cobertura`. 

174 

175 By default the input file is the relative `./reports/coverage/coverage.xml` 

176 and the output file is `./coverage-badge.svg`. You can change these settings 

177 with the `-i/--input_file` and `-o/--output-file` options. 

178  

179 By default the badge will have the name "coverage" as the left-hand side text. 

180 You can change these settings with the `-n/--name` option. The left-hand side 

181 text can be left blank with `-n ""` or have the left-hand side of the badge 

182 completely removed by passing `--noname`. 

183 

184 You can use the verbose flag `-v/--verbose` to display information on the 

185 input file contents, for verification. 

186 

187 The resulting badge will by default look like this: [coverage | 98.1%] where 

188 98.1 is the total coverage, obtained from the branch and line coverages 

189 using the formula 

190 

191 (nb_lines_covered + nb_branches_covered) / (nb_lines / nb_branches) 

192 

193 and multiplying this by 100. 

194 """ 

195 # Process i/o files 

196 input_file, input_file_path = _process_infile(input_file, "reports/coverage/coverage.xml") 

197 output_file, output_file_path, is_stdout = _process_outfile(output_file, "coverage-badge.svg") 

198 

199 # First retrieve the coverage info from the coverage xml 

200 try: 

201 cov_stats = get_coverage_stats(coverage_xml_file=input_file) 

202 except FileNotFoundError: 

203 raise click.exceptions.FileError(input_file, hint="File not found") 

204 

205 if not silent and verbose and not is_stdout: 

206 click.echo("""Coverage results parsed successfully from %(ifp)r 

207 - Branch coverage: %(bcp).2f%% (%(bc)s/%(bv)s) 

208 - Line coverage: %(lcp).2f%% (%(lc)s/%(lv)s) 

209 - Total coverage: %(tcp).2f%% ((%(bc)s+%(lc)s)/(%(bv)s+%(lv)s)) 

210""" % dict(ifp=input_file_path, tcp=cov_stats.total_coverage, 

211 bcp=cov_stats.branch_coverage, bc=cov_stats.branches_covered, bv=cov_stats.branches_valid, 

212 lcp=cov_stats.line_coverage, lc=cov_stats.lines_covered, lv=cov_stats.lines_valid)) 

213 

214 # Set badge name 

215 clear_left_txt = False 

216 if not withname: 216 ↛ 217line 216 didn't jump to line 217, because the condition on line 216 was never true

217 name = "" # removes left side of badge 

218 elif not name.strip(): 218 ↛ 219line 218 didn't jump to line 219, because the condition on line 218 was never true

219 name = "###" # blank text to replace 

220 clear_left_txt = True # keep left side of badge but remove text 

221 

222 # Generate the badge 

223 badge = get_coverage_badge(cov_stats, name) 

224 badge.write_to( 

225 output_file if is_stdout else output_file_path, 

226 use_shields=webshields, 

227 clear_left_txt=clear_left_txt 

228 ) 

229 

230 if not silent and not is_stdout: 

231 click.echo("SUCCESS - Coverage badge created: %r" % str(output_file_path)) 

232 

233 

234@genbadge.command(name="flake8", 

235 short_help="Generate a badge for the flake8 results (e.g. from a flake8stats.txt file).") 

236@click.option('-i', '--input-file', type=click.File('rt'), help=INFILE_HELP_TMP % "flake8 results TXT") 

237@click.option('-o', '--output-file', type=click.File('wt'), help=OUTFILE_BADGE_HELP) 

238@click.option('-n', '--name', type=str, default="flake8", help=NAME_HELP) 

239@click.option('--withname/--noname', type=bool, default=True, help=WITH_NAME_HELP) 

240@click.option('-w/-l', '--webshields/--local', type=bool, default=True, help=SHIELDS_HELP) 

241@click.option('-v', '--verbose', type=bool, default=False, is_flag=True, help=VERBOSE_HELP) 

242@click.option('-s', '--silent', type=bool, default=False, is_flag=True, help=SILENT_HELP) 

243def gen_flake8_badge( 

244 input_file=None, 

245 output_file=None, 

246 name=None, 

247 withname=None, 

248 webshields=None, 

249 verbose=None, 

250 silent=None 

251): 

252 """ 

253 This command generates a badge for the flake8 results, from a flake8stats.txt 

254 file. Such a file can be generated from python `flake8` using the 

255 --statistics flag. 

256 

257 By default the input file is the relative `./reports/flake8/flake8stats.txt` 

258 and the output file is `./flake8-badge.svg`. You can change these settings 

259 with the `-i/--input_file` and `-o/--output-file` options. 

260 

261 By default the badge will have the name "flake8" as the left-hand side text. 

262 You can change these settings with the `-n/--name` option. The left-hand side 

263 text can be left blank with `-n ""` or have the left-hand side of the badge 

264 completely removed by passing `--noname`. 

265 

266 You can use the verbose flag `-v/--verbose` to display information on the 

267 input file contents, for verification. 

268 

269 The resulting badge will by default look like this: [flake8 | 6 C, 0 W, 5 I] 

270 where 6, 0, 5 denote the number of critical issues, warnings, and 

271 information messages respectively. These severity levels are determined by 

272 the flake8-html plugin so as to match the colors in the HTML report. You can 

273 change the appearance of the badge with the --format option (not 

274 implemented, todo). 

275 """ 

276 # Process i/o files 

277 input_file, input_file_path = _process_infile(input_file, "reports/flake8/flake8stats.txt") 

278 output_file, output_file_path, is_stdout = _process_outfile(output_file, "flake8-badge.svg") 

279 

280 # First retrieve the success percentage from the junit xml 

281 try: 

282 flake8_stats = get_flake8_stats(flake8_stats_file=input_file) 

283 except FileNotFoundError: 

284 raise click.exceptions.FileError(input_file, hint="File not found") 

285 

286 if not silent and verbose and not is_stdout: 

287 click.echo("""Flake8 statistics parsed successfully from %r 

288 - Total (%s) = Critical (%s) + Warning (%s) + Info (%s) 

289""" % (input_file_path, flake8_stats.nb_total, flake8_stats.nb_critical, flake8_stats.nb_warning, flake8_stats.nb_info)) 

290 

291 # Set badge name 

292 clear_left_txt = False 

293 if not withname: 293 ↛ 294line 293 didn't jump to line 294, because the condition on line 293 was never true

294 name = "" # removes left side of badge 

295 elif not name.strip(): 295 ↛ 296line 295 didn't jump to line 296, because the condition on line 295 was never true

296 name = "###" # blank text to replace 

297 clear_left_txt = True # keep left side of badge but remove text 

298 

299 # Generate the badge 

300 badge = get_flake8_badge(flake8_stats, name) 

301 badge.write_to( 

302 output_file if is_stdout else output_file_path, 

303 use_shields=webshields, 

304 clear_left_txt=clear_left_txt 

305 ) 

306 

307 if not silent and not is_stdout: 

308 click.echo("SUCCESS - Flake8 badge created: %r" % str(output_file_path)) 

309 

310 

311def _process_infile(input_file, default_in_file): 

312 """Common in file processor""" 

313 

314 if input_file is None: 

315 input_file = default_in_file 

316 

317 if isinstance(input_file, str): 

318 input_file_path = Path(input_file).absolute().as_posix() 

319 else: 

320 input_file_path = getattr(input_file, "name", "<stdin>") 

321 

322 return input_file, input_file_path 

323 

324 

325def _process_outfile(output_file, default_out_file): 

326 """Common out file processor""" 

327 

328 is_stdout = False 

329 if output_file is None: 

330 output_file_path = Path(default_out_file).absolute() 

331 elif isinstance(output_file, str): 331 ↛ 332line 331 didn't jump to line 332, because the condition on line 331 was never true

332 output_file_path = Path(output_file).absolute() 

333 # special case of a directory 

334 if output_file_path.is_dir(): 

335 output_file_path = output_file_path / default_out_file 

336 else: 

337 output_file_path = getattr(output_file, "name", "<stdout>") 

338 if output_file_path == "<stdout>": 

339 is_stdout = True 

340 else: 

341 output_file_path = Path(output_file_path).absolute() 

342 

343 if not is_stdout: 

344 output_file_path = output_file_path.as_posix() 

345 

346 return output_file, output_file_path, is_stdout 

347 

348 

349if __name__ == '__main__': 349 ↛ 350line 349 didn't jump to line 350, because the condition on line 349 was never true

350 genbadge()