⬅ genbadge/main.py source

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>
5 try:
6 from pathlib import Path
7 except ImportError: # pragma: no cover
8 from pathlib2 import Path # python 2
9  
10 import click
11  
12  
13 from .utils_junit import get_test_stats, get_tests_badge
14 from .utils_coverage import get_coverage_badge, get_coverage_stats
15 from .utils_flake8 import get_flake8_stats, get_flake8_badge
16  
17 try:
18 FileNotFoundError
19 except NameError:
20 FileNotFoundError = IOError
21  
22  
23 INFILE_HELP_TMP = "An alternate %s file to read. '-' is supported and means <stdin>."
24 OUTFILE_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.")
27 NAME_HELP = ("An alternate SVG badge text name to display on the left-hand side of the badge.")
28 WITH_NAME_HELP = ("Indicates if a badge should be generated with or without the left-hand side of the badge.")
29 SHIELDS_HELP = ("Indicates if badges should be generated using the shields.io HTTP API (default) or the local SVG file "
30 "template included.")
31 VERBOSE_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.")
34 SILENT_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(invoke_without_command=True)
39 @click.pass_context
40 def genbadge(ctx):
41 """
42 Commandline utility to generate badges.
43 To get help on each command use:
44  
45 genbadge <cmd> --help
46  
47 """
48 if ctx.invoked_subcommand is None:
49 click.echo(ctx.get_help())
50 ctx.exit(0)
51  
52  
53 @genbadge.command(name="tests",
54 short_help="Generate a badge for the test results (e.g. from a junit.xml).")
55 @click.option('-i', '--input-file', type=click.File('rt'), help=INFILE_HELP_TMP % "test results XML")
56 @click.option('-o', '--output-file', type=click.File('wt'), help=OUTFILE_BADGE_HELP)
57 @click.option('-n', '--name', type=str, default="tests", help=NAME_HELP)
58 @click.option('-t', '--threshold', type=float,
59 help="An optional success percentage threshold to use. The command will fail with exit code 1 if the"
60 "actual success percentage is strictly less than the provided value.")
61 @click.option('--withname/--noname', type=bool, default=True, help=WITH_NAME_HELP)
62 @click.option('-w/-l', '--webshields/--local', type=bool, default=True, help=SHIELDS_HELP)
63 @click.option('-v', '--verbose', type=bool, default=False, is_flag=True, help=VERBOSE_HELP)
64 @click.option('-s', '--silent', type=bool, default=False, is_flag=True, help=SILENT_HELP)
65 def gen_tests_badge(
66 input_file=None,
67 output_file=None,
68 name=None,
69 threshold=None,
70 withname=None,
71 webshields=None,
72 verbose=None,
73 silent=None
74 ):
75 """
76 This command generates a badge for the test results, from an XML file in the
77 junit format. Such a file can be for example generated from python pytest
78 using the --junitxml flag, or from java junit.
79  
80 By default the input file is the relative `./reports/junit/junit.xml` and
81 the output file is `./tests-badge.svg`. You can change these settings with
82 the `-i/--input_file` and `-o/--output-file` options.
83  
84 By default the badge will have the name "tests" as the left-hand side text.
85 You can change these settings with the `-n/--name` option. The left-hand side
86 text can be left blank with `-n ""` or have the left-hand side of the badge
87 completely removed by passing `--noname`.
88  
89 You can use the verbose flag `-v/--verbose` to display information on the
90 input file contents, for verification.
91  
92 The resulting badge will by default look like this: [tests | 6/12]
93 where 6 is the number of tests that have run successfully, and 12 is the
94 total number of tests minus the number of skipped tests. When all tests
95 pass with success, the badge simply shows the number of tests [tests | 12].
96 You can change the appearance of the badge with the --format option (not
97 implemented, todo).
98  
99 The success percentage is defined as 6/12 = 50.0%. You can use the
100 `-t/--threshold` flag to setup a minimum success percentage required. If the
101 success percentage is below the threshold, an error will be raised and the
102 badge will not be generated.
103 """
104 # Process i/o files
105 input_file, input_file_path = _process_infile(input_file, "reports/junit/junit.xml")
106 output_file, output_file_path, is_stdout = _process_outfile(output_file, "tests-badge.svg")
107  
108 # First retrieve the success percentage from the junit xml
109 try:
110 test_stats = get_test_stats(junit_xml_file=input_file)
111 except FileNotFoundError:
112 raise click.exceptions.FileError(input_file, hint="File not found")
113  
114 if not silent and verbose and not is_stdout:
115 click.echo("""Test statistics parsed successfully from %r
116 - Nb tests: Total (%s) = Success (%s) + Skipped (%s) + Failed (%s) + Errors (%s)
117 - Success percentage: %.2f%% (%s / %s) (Skipped tests are excluded)
118 """ % (input_file_path, test_stats.total_with_skipped, test_stats.success, test_stats.skipped, test_stats.failed,
  • E122 Continuation line missing indentation or outdented
119 test_stats.errors, test_stats.success_percentage, test_stats.success, test_stats.total_without_skipped))
120  
121 # sanity check
122 if test_stats.total_with_skipped != test_stats.success + test_stats.skipped + test_stats.failed + test_stats.errors:
123 raise click.exceptions.ClickException(
124 "Inconsistent junit results: the sum of all kind of tests is not equal to the total. Please report this "
125 "issue if you think your file is correct. Details: %r" % test_stats
126 )
127  
128 # Validate against the threshold
129 if threshold is not None and test_stats.success_percentage < threshold:
130 raise click.exceptions.ClickException(
131 "Success percentage %s%% is strictly lower than required threshold %s%%"
132 % (float(test_stats.success_percentage), threshold)
133 )
  • W293 Blank line contains whitespace
134
135 # Set badge name
136 clear_left_txt = False
137 if not withname:
  • E261 At least two spaces before inline comment
138 name = "" # removes left side of badge
139 elif not name.strip():
  • E261 At least two spaces before inline comment
140 name = "###" # blank text to replace
  • E261 At least two spaces before inline comment
141 clear_left_txt = True # keep left side of badge but remove text
142  
143 # Generate the badge
144 badge = get_tests_badge(test_stats, name)
145 badge.write_to(
  • W291 Trailing whitespace
146 output_file if is_stdout else output_file_path,
  • W291 Trailing whitespace
147 use_shields=webshields,
148 clear_left_txt=clear_left_txt
149 )
150  
151 if not silent and not is_stdout:
152 click.echo("SUCCESS - Tests badge created: %r" % str(output_file_path))
153  
154  
155 @genbadge.command(name="coverage",
156 short_help="Generate a badge for the coverage results (e.g. from a coverage.xml).")
157 @click.option('-i', '--input-file', type=click.File('rt'), help=INFILE_HELP_TMP % "coverage results XML")
158 @click.option('-o', '--output-file', type=click.File('wt'), help=OUTFILE_BADGE_HELP)
159 @click.option('-n', '--name', type=str, default="coverage", help=NAME_HELP)
160 @click.option('--withname/--noname', type=bool, default=True, help=WITH_NAME_HELP)
161 @click.option('-w/-l', '--webshields/--local', type=bool, default=True, help=SHIELDS_HELP)
162 @click.option('-v', '--verbose', type=bool, default=False, is_flag=True, help=VERBOSE_HELP)
163 @click.option('-s', '--silent', type=bool, default=False, is_flag=True, help=SILENT_HELP)
164 def gen_coverage_badge(
165 input_file=None,
166 output_file=None,
167 name=None,
168 withname=None,
169 webshields=None,
170 verbose=None,
171 silent=None
172 ):
173 """
174 This command generates a badge for the coverage results, from an XML file in
175 the 'coverage' format. Such a file can be for example generated using the
176 python `coverage` tool, or java `cobertura`.
177  
178 By default the input file is the relative `./reports/coverage/coverage.xml`
179 and the output file is `./coverage-badge.svg`. You can change these settings
180 with the `-i/--input_file` and `-o/--output-file` options.
  • W293 Blank line contains whitespace
181
182 By default the badge will have the name "coverage" as the left-hand side text.
183 You can change these settings with the `-n/--name` option. The left-hand side
184 text can be left blank with `-n ""` or have the left-hand side of the badge
185 completely removed by passing `--noname`.
186  
187 You can use the verbose flag `-v/--verbose` to display information on the
188 input file contents, for verification.
189  
190 The resulting badge will by default look like this: [coverage | 98.1%] where
191 98.1 is the total coverage, obtained from the branch and line coverages
192 using the formula
193  
194 (nb_lines_covered + nb_branches_covered) / (nb_lines / nb_branches)
195  
196 and multiplying this by 100.
197 """
198 # Process i/o files
199 input_file, input_file_path = _process_infile(input_file, "reports/coverage/coverage.xml")
200 output_file, output_file_path, is_stdout = _process_outfile(output_file, "coverage-badge.svg")
201  
202 # First retrieve the coverage info from the coverage xml
203 try:
204 cov_stats = get_coverage_stats(coverage_xml_file=input_file)
205 except FileNotFoundError:
206 raise click.exceptions.FileError(input_file, hint="File not found")
207  
208 if not silent and verbose and not is_stdout:
209 click.echo("""Coverage results parsed successfully from %(ifp)r
210 - Branch coverage: %(bcp).2f%% (%(bc)s/%(bv)s)
211 - Line coverage: %(lcp).2f%% (%(lc)s/%(lv)s)
212 - Total coverage: %(tcp).2f%% ((%(bc)s+%(lc)s)/(%(bv)s+%(lv)s))
213 """ % dict(ifp=input_file_path, tcp=cov_stats.total_coverage,
214 bcp=cov_stats.branch_coverage, bc=cov_stats.branches_covered, bv=cov_stats.branches_valid,
215 lcp=cov_stats.line_coverage, lc=cov_stats.lines_covered, lv=cov_stats.lines_valid))
216  
217 # Set badge name
218 clear_left_txt = False
219 if not withname:
  • E261 At least two spaces before inline comment
220 name = "" # removes left side of badge
221 elif not name.strip():
  • E261 At least two spaces before inline comment
222 name = "###" # blank text to replace
  • E261 At least two spaces before inline comment
223 clear_left_txt = True # keep left side of badge but remove text
  • W293 Blank line contains whitespace
224
225 # Generate the badge
  • W291 Trailing whitespace
226 badge = get_coverage_badge(cov_stats, name)
227 badge.write_to(
  • W291 Trailing whitespace
228 output_file if is_stdout else output_file_path,
  • W291 Trailing whitespace
229 use_shields=webshields,
230 clear_left_txt=clear_left_txt
231 )
232  
233 if not silent and not is_stdout:
234 click.echo("SUCCESS - Coverage badge created: %r" % str(output_file_path))
235  
236  
237 @genbadge.command(name="flake8",
238 short_help="Generate a badge for the flake8 results (e.g. from a flake8stats.txt file).")
239 @click.option('-i', '--input-file', type=click.File('rt'), help=INFILE_HELP_TMP % "flake8 results TXT")
240 @click.option('-o', '--output-file', type=click.File('wt'), help=OUTFILE_BADGE_HELP)
241 @click.option('-n', '--name', type=str, default="flake8", help=NAME_HELP)
242 @click.option('--withname/--noname', type=bool, default=True, help=WITH_NAME_HELP)
243 @click.option('-w/-l', '--webshields/--local', type=bool, default=True, help=SHIELDS_HELP)
244 @click.option('-v', '--verbose', type=bool, default=False, is_flag=True, help=VERBOSE_HELP)
245 @click.option('-s', '--silent', type=bool, default=False, is_flag=True, help=SILENT_HELP)
246 def gen_flake8_badge(
247 input_file=None,
248 output_file=None,
249 name=None,
250 withname=None,
251 webshields=None,
252 verbose=None,
253 silent=None
254 ):
255 """
256 This command generates a badge for the flake8 results, from a flake8stats.txt
257 file. Such a file can be generated from python `flake8` using the
258 --statistics flag.
259  
260 By default the input file is the relative `./reports/flake8/flake8stats.txt`
261 and the output file is `./flake8-badge.svg`. You can change these settings
262 with the `-i/--input_file` and `-o/--output-file` options.
263  
264 By default the badge will have the name "flake8" as the left-hand side text.
265 You can change these settings with the `-n/--name` option. The left-hand side
266 text can be left blank with `-n ""` or have the left-hand side of the badge
267 completely removed by passing `--noname`.
268  
269 You can use the verbose flag `-v/--verbose` to display information on the
270 input file contents, for verification.
271  
272 The resulting badge will by default look like this: [flake8 | 6 C, 0 W, 5 I]
273 where 6, 0, 5 denote the number of critical issues, warnings, and
274 information messages respectively. These severity levels are determined by
275 the flake8-html plugin so as to match the colors in the HTML report. You can
276 change the appearance of the badge with the --format option (not
277 implemented, todo).
278 """
279 # Process i/o files
280 input_file, input_file_path = _process_infile(input_file, "reports/flake8/flake8stats.txt")
281 output_file, output_file_path, is_stdout = _process_outfile(output_file, "flake8-badge.svg")
282  
283 # First retrieve the success percentage from the junit xml
284 try:
285 flake8_stats = get_flake8_stats(flake8_stats_file=input_file)
286 except FileNotFoundError:
287 raise click.exceptions.FileError(input_file, hint="File not found")
288  
289 if not silent and verbose and not is_stdout:
290 click.echo("""Flake8 statistics parsed successfully from %r
291 - Total (%s) = Critical (%s) + Warning (%s) + Info (%s)
292 """ % (input_file_path, flake8_stats.nb_total, flake8_stats.nb_critical, flake8_stats.nb_warning, flake8_stats.nb_info))
293  
294 # Set badge name
295 clear_left_txt = False
296 if not withname:
  • E261 At least two spaces before inline comment
297 name = "" # removes left side of badge
298 elif not name.strip():
  • E261 At least two spaces before inline comment
299 name = "###" # blank text to replace
  • E261 At least two spaces before inline comment
300 clear_left_txt = True # keep left side of badge but remove text
301  
302 # Generate the badge
303 badge = get_flake8_badge(flake8_stats, name)
304 badge.write_to(
  • W291 Trailing whitespace
305 output_file if is_stdout else output_file_path,
  • W291 Trailing whitespace
306 use_shields=webshields,
307 clear_left_txt=clear_left_txt
308 )
309  
310 if not silent and not is_stdout:
311 click.echo("SUCCESS - Flake8 badge created: %r" % str(output_file_path))
312  
313  
314 def _process_infile(input_file, default_in_file):
315 """Common in file processor"""
316  
317 if input_file is None:
318 input_file = default_in_file
319  
320 if isinstance(input_file, str):
321 input_file_path = Path(input_file).absolute().as_posix()
322 else:
323 input_file_path = getattr(input_file, "name", "<stdin>")
324  
325 return input_file, input_file_path
326  
327  
328 def _process_outfile(output_file, default_out_file):
329 """Common out file processor"""
330  
331 is_stdout = False
332 if output_file is None:
333 output_file_path = Path(default_out_file).absolute()
334 elif isinstance(output_file, str):
335 output_file_path = Path(output_file).absolute()
336 # special case of a directory
337 if output_file_path.is_dir():
338 output_file_path = output_file_path / default_out_file
339 else:
340 output_file_path = getattr(output_file, "name", "<stdout>")
341 if output_file_path == "<stdout>":
342 is_stdout = True
343 else:
344 output_file_path = Path(output_file_path).absolute()
345  
346 if not is_stdout:
347 output_file_path = output_file_path.as_posix()
348  
349 return output_file, output_file_path, is_stdout
350  
351  
352 if __name__ == '__main__':
353 genbadge()