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..dea4c9f4 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', + help='String in format item1,item2,item3', + multiple=True +) 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) @@ -271,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, @@ -287,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 @@ -304,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 8d3b1d6c..d2857c82 100644 --- a/pycobertura/reporters.py +++ b/pycobertura/reporters.py @@ -20,24 +20,22 @@ 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): + 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): - 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) @@ -73,6 +71,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): @@ -120,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): @@ -146,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): @@ -210,6 +214,7 @@ def __init__( cobertura1, cobertura2, ignore_regex=None, + show_columns = None, show_source=True, *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 @@ -342,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: @@ -389,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") @@ -411,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"] @@ -433,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"] @@ -554,4 +557,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..661071cf 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", "--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", "--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", "--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", "--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", "--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", + "--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", + "--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", + "--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", + "--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", + "--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", + "--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", + "--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