From 44c9229864d5260d5fdf211af73d6574d792d9da Mon Sep 17 00:00:00 2001 From: Michael Gross Date: Sun, 15 Jun 2025 16:51:26 +0200 Subject: [PATCH 1/2] simple way of only showing specified columns --- .gitignore | 2 + pycobertura/cli.py | 9 +- pycobertura/reporters.py | 14 ++- tests/test_cli.py | 258 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 280 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 77779038..eb06457c 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,7 @@ var/ .installed.cfg *.egg .eggs +.venv # Installer logs pip-log.txt @@ -32,6 +33,7 @@ htmlcov/ .tox/ .coverage .cache +.history nosetests.xml coverage.xml junit.xml diff --git a/pycobertura/cli.py b/pycobertura/cli.py index f3d8f82e..d8779dcc 100644 --- a/pycobertura/cli.py +++ b/pycobertura/cli.py @@ -116,6 +116,12 @@ def get_exit_code(differ: CoberturaDiff, source): "the --source is a zip archive and the files were zipped under " "a directory prefix that is not part of the source.", ) +@click.option( + '-cols', + '--show-columns', + required=True, + help='List in format [item1,item2,item3]' +) def show( cobertura_file, ignore_regex, @@ -127,6 +133,7 @@ def show( annotation_level, annotation_title, annotation_message, + show_columns, ): """show coverage summary of a Cobertura report""" @@ -138,7 +145,7 @@ def show( filesystem=filesystem_factory(source, source_prefix=source_prefix), ) Reporter = reporters[format] - reporter = Reporter(cobertura, ignore_regex) + reporter = Reporter(cobertura, ignore_regex, show_columns) if format == "csv": report = reporter.generate(delimiter) diff --git a/pycobertura/reporters.py b/pycobertura/reporters.py index 8d3b1d6c..ff99de48 100644 --- a/pycobertura/reporters.py +++ b/pycobertura/reporters.py @@ -25,9 +25,10 @@ class Reporter: - def __init__(self, cobertura, ignore_regex=None): + def __init__(self, cobertura, ignore_regex=None, show_columns=None): self.cobertura: Cobertura = cobertura self.ignore_regex = ignore_regex + self.show_columns = show_columns @staticmethod def format_line_rates(summary_lines): @@ -73,6 +74,9 @@ def get_summary_lines(self): summary_lines["Cover"].append(total_rate) summary_lines["Missing"].append([]) + if self.show_columns: + summary_lines = {k:v for k, v in summary_lines.items() if k in self.show_columns} + return summary_lines def per_file_stats(self, summary_lines): @@ -211,6 +215,7 @@ def __init__( cobertura2, ignore_regex=None, show_source=True, + show_columns = None, *args, **kwargs, ): @@ -218,6 +223,7 @@ def __init__( self.show_source = show_source self.color = kwargs.pop("color", False) self.ignore_regex = ignore_regex + self.show_columns = show_columns def format_line_rate(self, line_rate): return f"{line_rate:+.2%}" if line_rate else "+100.00%" @@ -319,6 +325,10 @@ def get_summary_lines(self): diff_total_missing[i] for i in indexes_of_files_with_changes ] summary_lines["Missing"].append("") # for total line + + + if self.show_columns: + summary_lines = {k:v for k, v in summary_lines.items() if k in self.show_columns} return summary_lines @@ -554,4 +564,4 @@ def generate( ) ) result = "\n".join(result_strs) - return result + return result \ No newline at end of file diff --git a/tests/test_cli.py b/tests/test_cli.py index d879a179..3c152790 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -30,6 +30,264 @@ def test_show__format_default(): assert result.exit_code == ExitCodes.OK +def test_show__format_default_show_columns_combinations_1(): + from pycobertura.cli import show, ExitCodes + + runner = CliRunner() + result = runner.invoke( + show, + ["tests/dummy.original.xml", "--only-show-columns", '["Filename"]'], + catch_exceptions=False, + ) + assert ( + result.output + == """\ +Filename +----------------- +dummy/__init__.py +dummy/dummy.py +TOTAL +""" + ) + assert result.exit_code == ExitCodes.OK + + +def test_show__format_default_show_columns_combinations_2(): + from pycobertura.cli import show, ExitCodes + + runner = CliRunner() + result = runner.invoke( + show, + ["tests/dummy.original.xml", "--only-show-columns", '["Filename","Miss"]'], + catch_exceptions=False, + ) + assert ( + result.output + == """\ +Filename Miss +----------------- ------ +dummy/__init__.py 0 +dummy/dummy.py 2 +TOTAL 2 +""" + ) + assert result.exit_code == ExitCodes.OK + + result = runner.invoke( + show, + ["tests/dummy.original.xml", "--only-show-columns", '["Filename","Cover"]'], + catch_exceptions=False, + ) + assert ( + result.output + == """\ +Filename Cover +----------------- ------- +dummy/__init__.py 0.00% +dummy/dummy.py 50.00% +TOTAL 50.00% +""" + ) + + assert result.exit_code == ExitCodes.OK + + result = runner.invoke( + show, + ["tests/dummy.original.xml", "--only-show-columns", '["Cover","Missing"]'], + catch_exceptions=False, + ) + assert ( + result.output + == """\ +Cover Missing +------- --------- +0.00% +50.00% 2, 5 +50.00% +""" + ) + + assert result.exit_code == ExitCodes.OK + + result = runner.invoke( + show, + ["tests/dummy.original.xml", "--only-show-columns", '["Filename","Stmts"]'], + catch_exceptions=False, + ) + assert ( + result.output + == """\ +Filename Stmts +----------------- ------- +dummy/__init__.py 0 +dummy/dummy.py 4 +TOTAL 4 +""" + ) + + assert result.exit_code == ExitCodes.OK + + +def test_show__format_default_show_columns_combinations_3(): + from pycobertura.cli import show, ExitCodes + + runner = CliRunner() + result = runner.invoke( + show, + [ + "tests/dummy.original.xml", + "--only-show-columns", + '["Filename", "Miss", "Missing"]', + ], + catch_exceptions=False, + ) + assert ( + result.output + == """\ +Filename Miss Missing +----------------- ------ --------- +dummy/__init__.py 0 +dummy/dummy.py 2 2, 5 +TOTAL 2 +""" + ) + assert result.exit_code == ExitCodes.OK + + result = runner.invoke( + show, + [ + "tests/dummy.original.xml", + "--only-show-columns", + '["Filename","Miss","Cover"]', + ], + catch_exceptions=False, + ) + assert ( + result.output + == """\ +Filename Miss Cover +----------------- ------ ------- +dummy/__init__.py 0 0.00% +dummy/dummy.py 2 50.00% +TOTAL 2 50.00% +""" + ) + + assert result.exit_code == ExitCodes.OK + + result = runner.invoke( + show, + [ + "tests/dummy.original.xml", + "--only-show-columns", + '["Filename","Cover","Missing"]', + ], + catch_exceptions=False, + ) + assert ( + result.output + == """\ +Filename Cover Missing +----------------- ------- --------- +dummy/__init__.py 0.00% +dummy/dummy.py 50.00% 2, 5 +TOTAL 50.00% +""" + ) + + assert result.exit_code == ExitCodes.OK + + +def test_show__format_default_show_columns_combinations_4(): + from pycobertura.cli import show, ExitCodes + + runner = CliRunner() + result = runner.invoke( + show, + [ + "tests/dummy.original.xml", + "--only-show-columns", + '["Filename", "Stmts", "Miss", "Missing"]', + ], + catch_exceptions=False, + ) + assert ( + result.output + == """\ +Filename Stmts Miss Missing +----------------- ------- ------ --------- +dummy/__init__.py 0 0 +dummy/dummy.py 4 2 2, 5 +TOTAL 4 2 +""" + ) + assert result.exit_code == ExitCodes.OK + + result = runner.invoke( + show, + [ + "tests/dummy.original.xml", + "--only-show-columns", + '["Filename", "Stmts", "Cover", "Missing"]', + ], + catch_exceptions=False, + ) + assert ( + result.output + == """\ +Filename Stmts Cover Missing +----------------- ------- ------- --------- +dummy/__init__.py 0 0.00% +dummy/dummy.py 4 50.00% 2, 5 +TOTAL 4 50.00% +""" + ) + assert result.exit_code == ExitCodes.OK + + result = runner.invoke( + show, + [ + "tests/dummy.original.xml", + "--only-show-columns", + '["Filename", "Stmts", "Miss", "Cover"]', + ], + catch_exceptions=False, + ) + assert ( + result.output + == """\ +Filename Stmts Miss Cover +----------------- ------- ------ ------- +dummy/__init__.py 0 0 0.00% +dummy/dummy.py 4 2 50.00% +TOTAL 4 2 50.00% +""" + ) + assert result.exit_code == ExitCodes.OK + + result = runner.invoke( + show, + [ + "tests/dummy.original.xml", + "--only-show-columns", + '["Stmts", "Miss", "Cover", "Missing"]', + ], + catch_exceptions=False, + ) + assert ( + result.output + == """\ + Stmts Miss Cover Missing +------- ------ ------- --------- + 0 0 0.00% + 4 2 50.00% 2, 5 + 4 2 50.00% +""" + ) + assert result.exit_code == ExitCodes.OK + + + def test_show__format_text(): from pycobertura.cli import show, ExitCodes From 97890a67d24c01b2a91a34b7e8c0b30976a9c602 Mon Sep 17 00:00:00 2001 From: Michael Gross Date: Sun, 29 Jun 2025 15:03:54 +0200 Subject: [PATCH 2/2] started test implementation and fixes. --- pycobertura/cli.py | 13 +++++-- pycobertura/reporters.py | 75 ++++++++++++++++++---------------------- tests/test_cli.py | 38 ++++++++++---------- 3 files changed, 63 insertions(+), 63 deletions(-) diff --git a/pycobertura/cli.py b/pycobertura/cli.py index d8779dcc..dea4c9f4 100644 --- a/pycobertura/cli.py +++ b/pycobertura/cli.py @@ -119,8 +119,8 @@ def get_exit_code(differ: CoberturaDiff, source): @click.option( '-cols', '--show-columns', - required=True, - help='List in format [item1,item2,item3]' + help='String in format item1,item2,item3', + multiple=True ) def show( cobertura_file, @@ -278,6 +278,12 @@ def show( type=str, help="annotation message for github annotation format", ) +@click.option( + '-cols', + '--show-columns', + help='String in format item1,item2,item3', + multiple=True +) def diff( cobertura_file1, cobertura_file2, @@ -294,6 +300,7 @@ def diff( annotation_level, annotation_title, annotation_message, + show_columns ): """compare coverage of two Cobertura reports""" # Assume that the source is located in the same directory as the provided @@ -311,7 +318,7 @@ def diff( cobertura2 = Cobertura(cobertura_file2, filesystem=filesystem2) Reporter = delta_reporters[format] - reporter_args = [cobertura1, cobertura2, ignore_regex] + reporter_args = [cobertura1, cobertura2, ignore_regex, show_columns] reporter_kwargs = {"show_source": source} isatty = True if output is None else output.isatty() diff --git a/pycobertura/reporters.py b/pycobertura/reporters.py index ff99de48..d2857c82 100644 --- a/pycobertura/reporters.py +++ b/pycobertura/reporters.py @@ -20,9 +20,6 @@ env.filters["is_not_equal_to_dash"] = filters.is_not_equal_to_dash env.filters["misses_color"] = filters.misses_color -headers_with_missing = ["Filename", "Stmts", "Miss", "Cover", "Missing"] -headers_without_missing = ["Filename", "Stmts", "Miss", "Cover"] - class Reporter: def __init__(self, cobertura, ignore_regex=None, show_columns=None): @@ -30,15 +27,15 @@ def __init__(self, cobertura, ignore_regex=None, show_columns=None): self.ignore_regex = ignore_regex self.show_columns = show_columns - @staticmethod - def format_line_rates(summary_lines): - for i, line_rate in enumerate(summary_lines["Cover"]): - summary_lines["Cover"][i] = f"{line_rate:.2%}" + def format_line_rates(self, summary_lines): + if (not self.show_columns or "Cover" in self.show_columns): + for i, line_rate in enumerate(summary_lines["Cover"]): + summary_lines["Cover"][i] = f"{line_rate:.2%}" - @staticmethod - def format_missing_lines(summary_lines): - for i, missing_lines in enumerate(summary_lines["Missing"]): - summary_lines["Missing"][i] = stringify(missing_lines) + def format_missing_lines(self, summary_lines): + if (not self.show_columns or "Missing" in self.show_columns): + for i, missing_lines in enumerate(summary_lines["Missing"]): + summary_lines["Missing"][i] = stringify(missing_lines) def get_summary_lines(self): filenames = self.cobertura.files(ignore_regex=self.ignore_regex) @@ -124,25 +121,28 @@ def generate(self): summary_lines = self.get_summary_lines() self.format_line_rates(summary_lines) self.format_missing_lines(summary_lines) - return tabulate(summary_lines, headers=headers_with_missing) + headers = list(summary_lines.keys()) + return tabulate(summary_lines, headers=headers) + class CsvReporter(Reporter): def generate(self, delimiter): summary_lines = self.get_summary_lines() + headers = list(summary_lines.keys()) + values = list(summary_lines.values()) + self.format_line_rates(summary_lines) self.format_missing_lines(summary_lines) - list_of_lines = [headers_with_missing] - list_of_lines.extend( - [[f"{item}" for item in row] for row in zip(*summary_lines.values())] - ) + list_of_lines = [headers] + # Transpose the columns into rows + rows = list(zip(*values)) + list_of_lines.extend(rows) - # Explanation here: - # https://stackoverflow.com/a/55889036/9698518 delimiter = delimiter.encode().decode("unicode_escape") - return "\n".join([delimiter.join(line) for line in list_of_lines]) + return "\n".join([delimiter.join(map(str, line)) for line in list_of_lines]) class MarkdownReporter(Reporter): @@ -150,7 +150,7 @@ def generate(self): summary_lines = self.get_summary_lines() self.format_line_rates(summary_lines) self.format_missing_lines(summary_lines) - return tabulate(summary_lines, headers=headers_with_missing, tablefmt="github") + return tabulate(summary_lines, headers=list(summary_lines.keys()), tablefmt="github") class JsonReporter(Reporter): @@ -214,8 +214,8 @@ def __init__( cobertura1, cobertura2, ignore_regex=None, - show_source=True, show_columns = None, + show_source=True, *args, **kwargs, ): @@ -352,37 +352,31 @@ def per_file_stats(self, summary_lines): class TextReporterDelta(DeltaReporter): def generate(self): summary_lines = self.get_summary_lines() - headers = headers_without_missing + headers = list(summary_lines.keys()) - if self.show_source: + if self.show_source and "Missing" in headers: missed_lines_colored = [ self.color_number([str(m[0]) for m in missing]) for missing in summary_lines["Missing"] ] summary_lines["Missing"] = missed_lines_colored - headers = headers_with_missing return tabulate(summary_lines, headers=headers) class CsvReporterDelta(DeltaReporter): def generate(self, delimiter): summary_lines = self.get_summary_lines() - - # lines_values: List of lines dictionary values arranged in - # tuples of Table row values - lines_values = list(zip(*summary_lines.values())) + headers = list(summary_lines.keys()) + values = list(summary_lines.values()) # Stringify every item in Table row values without using the Missing column # and store in the list list_of_lines - list_of_lines = [headers_without_missing] - list_of_lines.extend([[f"{item}" for item in row[:-1]] for row in lines_values]) + list_of_lines = [headers] + rows = list(zip(*values)) + list_of_lines.extend(rows) - if self.show_source: - # Add the Missing header to list_of_lines first inner list - # This is a direct assignment to avoid appending an additional "Missing" - # header in every iteration of the tests which would fail them - list_of_lines[0] = headers_with_missing + if self.show_source and "Missing" in list_of_lines: # Add to every list inside the list_of_lines the Missing column value for line_index, missing_line in enumerate(summary_lines["Missing"]): # for colors, explanation see here: @@ -399,21 +393,20 @@ def generate(self, delimiter): # https://stackoverflow.com/a/55889036/9698518 delimiter = delimiter.encode().decode("unicode_escape") - return "\n".join([delimiter.join(line) for line in list_of_lines]) + return "\n".join([delimiter.join(map(str, line)) for line in list_of_lines]) class MarkdownReporterDelta(DeltaReporter): def generate(self): summary_lines = self.get_summary_lines() - headers = headers_without_missing + headers = list(summary_lines.keys()) - if self.show_source: + if self.show_source and "Missing" in headers: missed_lines_colored = [ self.color_number([str(m[0]) for m in missing]) for missing in summary_lines["Missing"] ] summary_lines["Missing"] = missed_lines_colored - headers = headers_with_missing return tabulate(summary_lines, headers=headers, tablefmt="github") @@ -421,7 +414,7 @@ class JsonReporterDelta(DeltaReporter): def generate(self): summary_lines = self.get_summary_lines() - if self.show_source: + if self.show_source and "Missing" in summary_lines.keys(): missed_lines_colored = [ self.color_number([str(m[0]) for m in missing]) for missing in summary_lines["Missing"] @@ -443,7 +436,7 @@ class YamlReporterDelta(DeltaReporter): def generate(self): summary_lines = self.get_summary_lines() - if self.show_source: + if self.show_source and "Missing" in summary_lines.keys(): missed_lines_colored = [ self.color_number([str(m[0]) for m in missing]) for missing in summary_lines["Missing"] diff --git a/tests/test_cli.py b/tests/test_cli.py index 3c152790..661071cf 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -36,7 +36,7 @@ def test_show__format_default_show_columns_combinations_1(): runner = CliRunner() result = runner.invoke( show, - ["tests/dummy.original.xml", "--only-show-columns", '["Filename"]'], + ["tests/dummy.original.xml", "--show-columns", "Filename"], catch_exceptions=False, ) assert ( @@ -58,7 +58,7 @@ def test_show__format_default_show_columns_combinations_2(): runner = CliRunner() result = runner.invoke( show, - ["tests/dummy.original.xml", "--only-show-columns", '["Filename","Miss"]'], + ["tests/dummy.original.xml", "--show-columns", "Filename,Miss"], catch_exceptions=False, ) assert ( @@ -75,7 +75,7 @@ def test_show__format_default_show_columns_combinations_2(): result = runner.invoke( show, - ["tests/dummy.original.xml", "--only-show-columns", '["Filename","Cover"]'], + ["tests/dummy.original.xml", "--show-columns", "Filename,Cover"], catch_exceptions=False, ) assert ( @@ -93,7 +93,7 @@ def test_show__format_default_show_columns_combinations_2(): result = runner.invoke( show, - ["tests/dummy.original.xml", "--only-show-columns", '["Cover","Missing"]'], + ["tests/dummy.original.xml", "--show-columns", "Cover,Missing"], catch_exceptions=False, ) assert ( @@ -111,7 +111,7 @@ def test_show__format_default_show_columns_combinations_2(): result = runner.invoke( show, - ["tests/dummy.original.xml", "--only-show-columns", '["Filename","Stmts"]'], + ["tests/dummy.original.xml", "--show-columns", "Filename,Stmts"], catch_exceptions=False, ) assert ( @@ -136,8 +136,8 @@ def test_show__format_default_show_columns_combinations_3(): show, [ "tests/dummy.original.xml", - "--only-show-columns", - '["Filename", "Miss", "Missing"]', + "--show-columns", + "Filename,Miss,Missing", ], catch_exceptions=False, ) @@ -157,8 +157,8 @@ def test_show__format_default_show_columns_combinations_3(): show, [ "tests/dummy.original.xml", - "--only-show-columns", - '["Filename","Miss","Cover"]', + "--show-columns", + "Filename,Miss,Cover", ], catch_exceptions=False, ) @@ -179,8 +179,8 @@ def test_show__format_default_show_columns_combinations_3(): show, [ "tests/dummy.original.xml", - "--only-show-columns", - '["Filename","Cover","Missing"]', + "--show-columns", + "Filename,Cover,Missing", ], catch_exceptions=False, ) @@ -206,8 +206,8 @@ def test_show__format_default_show_columns_combinations_4(): show, [ "tests/dummy.original.xml", - "--only-show-columns", - '["Filename", "Stmts", "Miss", "Missing"]', + "--show-columns", + "Filename,Stmts,Miss,Missing", ], catch_exceptions=False, ) @@ -227,8 +227,8 @@ def test_show__format_default_show_columns_combinations_4(): show, [ "tests/dummy.original.xml", - "--only-show-columns", - '["Filename", "Stmts", "Cover", "Missing"]', + "--show-columns", + "Filename,Stmts,Cover,Missing", ], catch_exceptions=False, ) @@ -248,8 +248,8 @@ def test_show__format_default_show_columns_combinations_4(): show, [ "tests/dummy.original.xml", - "--only-show-columns", - '["Filename", "Stmts", "Miss", "Cover"]', + "--show-columns", + "Filename,Stmts,Miss,Cover", ], catch_exceptions=False, ) @@ -269,8 +269,8 @@ def test_show__format_default_show_columns_combinations_4(): show, [ "tests/dummy.original.xml", - "--only-show-columns", - '["Stmts", "Miss", "Cover", "Missing"]', + "--show-columns", + "Stmts,Miss,Cover,Missing", ], catch_exceptions=False, )