diff --git a/CHANGES.md b/CHANGES.md index 24a72a4..bbe08ca 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,6 +5,7 @@ * Add `--sort-by-uncovered-lines` to sort output, so the file with the most missing lines is at the top. Thanks @guettli * Fix: JSON and YAML output when using `--ignore-regex` in `show` command. Thanks @OidaTiftla +* Add `--fail-threshold` to return a non-zero exit code when the total number of uncovered lines exceeds the specified threshold ## 4.1.0 (2025-04-13) diff --git a/README.md b/README.md index 71a4393..70ed5c5 100644 --- a/README.md +++ b/README.md @@ -219,6 +219,12 @@ $ pycobertura show --format github-annotation tests/cobertura.xml ::notice file=dummy/dummy4.py,line=1,endLine=6,title=pycobertura::not covered ``` +The following shows how to return a non-zero exit code when the total number of uncovered lines exceeds the specified threshold. + +```shell +$ pycobertura show --fail-threshold=123 cobertura.xml +``` + If you run it in GitHub Actions/Apps, the above log generates check annotations. ![Example output of github-annotation formatted pycobertura show command](images/example_github_annotation_show.png) diff --git a/pycobertura/cli.py b/pycobertura/cli.py index b5e9295..163368c 100644 --- a/pycobertura/cli.py +++ b/pycobertura/cli.py @@ -39,6 +39,7 @@ class ExitCodes: EXCEPTION = 1 COVERAGE_WORSENED = 2 NOT_ALL_CHANGES_COVERED = 3 + TOTAL_MISSES_ABOVE_THRESHOLD = 4 def get_exit_code(differ: CoberturaDiff, source): @@ -121,6 +122,13 @@ def get_exit_code(differ: CoberturaDiff, source): default=False, help="Sort the summary so files with the most uncovered lines appear first.", ) +@click.option( + "--fail-threshold", + metavar="", + type=click.IntRange(min=1), + help="Return a non-zero code if the total number of uncovered statements " + "exceeds the threshold.", +) def show( cobertura_file, ignore_regex, @@ -133,6 +141,7 @@ def show( annotation_level, annotation_title, annotation_message, + fail_threshold, ): """show coverage summary of a Cobertura report""" @@ -146,7 +155,6 @@ def show( Reporter = reporters[format] reporter_kwargs = {} reporter_kwargs["sort_by_uncovered_lines"] = sort_by_uncovered_lines - reporter = Reporter(cobertura, ignore_regex, **reporter_kwargs) if format == "csv": @@ -166,6 +174,10 @@ def show( isatty = True if output is None else output.isatty() click.echo(report, file=output, nl=isatty) + if fail_threshold is not None: + if cobertura.total_misses() > fail_threshold: + raise SystemExit(ExitCodes.TOTAL_MISSES_ABOVE_THRESHOLD) + delta_reporters = { "text": TextReporterDelta, @@ -178,8 +190,7 @@ def show( } -@pycobertura.command( - help="""\ +@pycobertura.command(help="""\ The diff command compares and shows the changes between two Cobertura reports. NOTE: Reporting missing lines or showing the source code with the diff command @@ -190,8 +201,7 @@ def show( options `--source1` and `--source2` are necessary to point to the source code directories (or zip archives). If the source is not available at all, pass `--no-source` but missing lines and source code will not be reported. -""" -) +""") @click.argument("cobertura_file1") @click.argument("cobertura_file2") @click.option( diff --git a/pycobertura/reporters.py b/pycobertura/reporters.py index eef7a2f..29a8c05 100644 --- a/pycobertura/reporters.py +++ b/pycobertura/reporters.py @@ -13,7 +13,6 @@ import json import io - env = Environment(loader=PackageLoader("pycobertura", "templates")) env.filters["line_status"] = filters.line_status env.filters["line_reason"] = filters.line_reason_icon diff --git a/tests/test_cli.py b/tests/test_cli.py index 67a2284..e699e82 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,5 +1,6 @@ import json import os +from pycobertura.cli import ExitCodes import pytest from click.testing import CliRunner @@ -12,6 +13,55 @@ def test_exit_codes(): assert ExitCodes.EXCEPTION == 1 assert ExitCodes.COVERAGE_WORSENED == 2 assert ExitCodes.NOT_ALL_CHANGES_COVERED == 3 + assert ExitCodes.TOTAL_MISSES_ABOVE_THRESHOLD == 4 + + +@pytest.mark.parametrize("fail_threshold, exit_code", ((1000, ExitCodes.OK), (1, ExitCodes.TOTAL_MISSES_ABOVE_THRESHOLD))) +def test_show__fail_threshold__exit_status(fail_threshold, exit_code): + from pycobertura.cli import show + + runner = CliRunner() + result = runner.invoke(show, [ + 'tests/dummy.original.xml', + f'--fail-threshold={fail_threshold}', + ], catch_exceptions=False) + assert result.exit_code == exit_code + + +@pytest.mark.parametrize('fail_threshold', (-1, 0)) +def test_show__fail_threshold__invalid_value(fail_threshold): + from pycobertura.cli import show + + runner = CliRunner() + result = runner.invoke( + show, + ['tests/dummy.original.xml', f'--fail-threshold={fail_threshold}'], + catch_exceptions=False, + ) + assert result.output == f"""\ +Usage: show [OPTIONS] COBERTURA_FILE +Try 'show --help' for help. + +Error: Invalid value for '--fail-threshold': {fail_threshold} is not in the range x>=1. +""" + + +@pytest.mark.parametrize('fail_threshold', (42.0, True, False, None)) +def test_show__fail_threshold__invalid_type(fail_threshold): + from pycobertura.cli import show + + runner = CliRunner() + result = runner.invoke( + show, + ['tests/dummy.original.xml', f'--fail-threshold={fail_threshold}'], + catch_exceptions=False, + ) + assert result.output == f"""\ +Usage: show [OPTIONS] COBERTURA_FILE +Try 'show --help' for help. + +Error: Invalid value for '--fail-threshold': '{fail_threshold}' is not a valid integer range. +""" def test_show__format_default():