⬅ 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()
39 def 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)
62 def 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,
  • E122 Continuation line missing indentation or outdented
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:
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 )
  • W293 Blank line contains whitespace
131
132 # Set badge name
133 clear_left_txt = False
134 if not withname:
  • E261 At least two spaces before inline comment
135 name = "" # removes left side of badge
136 elif not name.strip():
  • E261 At least two spaces before inline comment
137 name = "###" # blank text to replace
  • E261 At least two spaces before inline comment
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(
  • W291 Trailing whitespace
143 output_file if is_stdout else output_file_path,
  • W291 Trailing whitespace
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)
161 def 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.
  • W293 Blank line contains whitespace
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:
  • E261 At least two spaces before inline comment
217 name = "" # removes left side of badge
218 elif not name.strip():
  • E261 At least two spaces before inline comment
219 name = "###" # blank text to replace
  • E261 At least two spaces before inline comment
220 clear_left_txt = True # keep left side of badge but remove text
  • W293 Blank line contains whitespace
221
222 # Generate the badge
  • W291 Trailing whitespace
223 badge = get_coverage_badge(cov_stats, name)
224 badge.write_to(
  • W291 Trailing whitespace
225 output_file if is_stdout else output_file_path,
  • W291 Trailing whitespace
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)
243 def 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:
  • E261 At least two spaces before inline comment
294 name = "" # removes left side of badge
295 elif not name.strip():
  • E261 At least two spaces before inline comment
296 name = "###" # blank text to replace
  • E261 At least two spaces before inline comment
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(
  • W291 Trailing whitespace
302 output_file if is_stdout else output_file_path,
  • W291 Trailing whitespace
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  
311 def _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  
325 def _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):
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  
349 if __name__ == '__main__':
350 genbadge()