blob: fc43472dd942168f6cfdae6ab3f89820ca2b3559 [file] [log] [blame] [edit]
# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
"""Test json-based summary reporting for coverage.py"""
from __future__ import annotations
import copy
import json
import os
from datetime import datetime
from typing import Any
import coverage
from coverage import Coverage
from tests.coveragetest import UsingModulesMixin, CoverageTest
class JsonReportTest(UsingModulesMixin, CoverageTest):
"""Tests of the JSON reports from coverage.py."""
def _assert_expected_json_report(
self,
cov: Coverage,
expected_result: dict[str, Any],
) -> None:
"""
Helper that creates an example file for most tests.
"""
self.make_file(
"a.py",
"""\
a = {'b': 1}
if a.get('a'):
b = 3
elif a.get('b'):
b = 5
else:
b = 7
if not a:
b = 9
""",
)
self._compare_json_reports(cov, expected_result, "a")
def _assert_expected_json_report_with_regions(
self,
cov: Coverage,
expected_result: dict[str, Any],
) -> None:
"""
Helper that creates an example file for regions tests.
"""
self.make_file(
"b.py",
"""\
a = {"b": 1}
def c():
return 4
class C:
pass
class D:
def e(self):
if a.get("a"):
return 12
return 13
def f(self):
return 15
""",
)
self._compare_json_reports(cov, expected_result, "b")
def _compare_json_reports(
self,
cov: Coverage,
expected_result: dict[str, Any],
mod_name: str,
) -> None:
"""
Helper that handles common ceremonies, comparing JSON reports that
it creates to expected results, so tests can clearly show the
consequences of setting various arguments.
"""
mod = self.start_import_stop(cov, mod_name)
output_path = os.path.join(self.temp_dir, f"{mod_name}.json")
cov.json_report(mod, outfile=output_path)
with open(output_path, encoding="utf-8") as result_file:
parsed_result = json.load(result_file)
self.assert_recent_datetime(
datetime.strptime(parsed_result["meta"]["timestamp"], "%Y-%m-%dT%H:%M:%S.%f"),
)
del parsed_result["meta"]["timestamp"]
expected_result["meta"].update(
{
"version": coverage.__version__,
}
)
assert parsed_result == expected_result
def test_branch_coverage(self) -> None:
cov = coverage.Coverage(branch=True)
a_py_result = {
"executed_lines": [1, 2, 4, 5, 8],
"missing_lines": [3, 7, 9],
"excluded_lines": [],
"executed_branches": [
[2, 4],
[4, 5],
[8, -1],
],
"missing_branches": [
[2, 3],
[4, 7],
[8, 9],
],
"summary": {
"missing_lines": 3,
"covered_lines": 5,
"num_statements": 8,
"num_branches": 6,
"excluded_lines": 0,
"num_partial_branches": 3,
"covered_branches": 3,
"missing_branches": 3,
"percent_covered": 57.142857142857146,
"percent_covered_display": "57",
},
}
expected_result = {
"meta": {
"branch_coverage": True,
"format": 3,
"show_contexts": False,
},
"files": {
"a.py": copy.deepcopy(a_py_result),
},
"totals": {
"missing_lines": 3,
"covered_lines": 5,
"num_statements": 8,
"num_branches": 6,
"excluded_lines": 0,
"num_partial_branches": 3,
"percent_covered": 57.142857142857146,
"percent_covered_display": "57",
"covered_branches": 3,
"missing_branches": 3,
},
}
# With regions, a lot of data is duplicated.
expected_result["files"]["a.py"]["classes"] = {"": a_py_result} # type: ignore[index]
expected_result["files"]["a.py"]["functions"] = {"": a_py_result} # type: ignore[index]
self._assert_expected_json_report(cov, expected_result)
def test_simple_line_coverage(self) -> None:
cov = coverage.Coverage()
a_py_result = {
"executed_lines": [1, 2, 4, 5, 8],
"missing_lines": [3, 7, 9],
"excluded_lines": [],
"summary": {
"excluded_lines": 0,
"missing_lines": 3,
"covered_lines": 5,
"num_statements": 8,
"percent_covered": 62.5,
"percent_covered_display": "62",
},
}
expected_result = {
"meta": {
"branch_coverage": False,
"format": 3,
"show_contexts": False,
},
"files": {
"a.py": copy.deepcopy(a_py_result),
},
"totals": {
"excluded_lines": 0,
"missing_lines": 3,
"covered_lines": 5,
"num_statements": 8,
"percent_covered": 62.5,
"percent_covered_display": "62",
},
}
# With regions, a lot of data is duplicated.
expected_result["files"]["a.py"]["classes"] = {"": a_py_result} # type: ignore[index]
expected_result["files"]["a.py"]["functions"] = {"": a_py_result} # type: ignore[index]
self._assert_expected_json_report(cov, expected_result)
def test_regions_coverage(self) -> None:
cov = coverage.Coverage()
expected_result = {
"files": {
"b.py": {
"classes": {
"": {
"excluded_lines": [],
"executed_lines": [1, 3, 6, 7, 9, 10, 14],
"missing_lines": [4],
"summary": {
"covered_lines": 7,
"excluded_lines": 0,
"missing_lines": 1,
"num_statements": 8,
"percent_covered": 87.5,
"percent_covered_display": "88",
},
},
"C": {
"excluded_lines": [],
"executed_lines": [],
"missing_lines": [],
"summary": {
"covered_lines": 0,
"excluded_lines": 0,
"missing_lines": 0,
"num_statements": 0,
"percent_covered": 100.0,
"percent_covered_display": "100",
},
},
"D": {
"executed_lines": [],
"excluded_lines": [],
"missing_lines": [11, 12, 13, 15],
"summary": {
"covered_lines": 0,
"excluded_lines": 0,
"missing_lines": 4,
"num_statements": 4,
"percent_covered": 0.0,
"percent_covered_display": "0",
},
},
},
"executed_lines": [1, 3, 6, 7, 9, 10, 14],
"excluded_lines": [],
"functions": {
"": {
"excluded_lines": [],
"executed_lines": [1, 3, 6, 7, 9, 10, 14],
"missing_lines": [],
"summary": {
"covered_lines": 7,
"excluded_lines": 0,
"missing_lines": 0,
"num_statements": 7,
"percent_covered": 100.0,
"percent_covered_display": "100",
},
},
"c": {
"executed_lines": [],
"excluded_lines": [],
"missing_lines": [4],
"summary": {
"covered_lines": 0,
"excluded_lines": 0,
"missing_lines": 1,
"num_statements": 1,
"percent_covered": 0.0,
"percent_covered_display": "0",
},
},
"D.e": {
"executed_lines": [],
"excluded_lines": [],
"missing_lines": [11, 12, 13],
"summary": {
"covered_lines": 0,
"excluded_lines": 0,
"missing_lines": 3,
"num_statements": 3,
"percent_covered": 0.0,
"percent_covered_display": "0",
},
},
"D.f": {
"executed_lines": [],
"excluded_lines": [],
"missing_lines": [15],
"summary": {
"covered_lines": 0,
"excluded_lines": 0,
"missing_lines": 1,
"num_statements": 1,
"percent_covered": 0.0,
"percent_covered_display": "0",
},
},
},
"missing_lines": [4, 11, 12, 13, 15],
"summary": {
"covered_lines": 7,
"excluded_lines": 0,
"missing_lines": 5,
"num_statements": 12,
"percent_covered": 58.333333333333336,
"percent_covered_display": "58",
},
},
},
"meta": {
"branch_coverage": False,
"format": 3,
"show_contexts": False,
},
"totals": {
"covered_lines": 7,
"excluded_lines": 0,
"missing_lines": 5,
"num_statements": 12,
"percent_covered": 58.333333333333336,
"percent_covered_display": "58",
},
}
self._assert_expected_json_report_with_regions(cov, expected_result)
def test_branch_regions_coverage(self) -> None:
cov = coverage.Coverage(branch=True)
expected_result = {
"files": {
"b.py": {
"classes": {
"": {
"excluded_lines": [],
"executed_branches": [],
"executed_lines": [1, 3, 6, 7, 9, 10, 14],
"missing_branches": [],
"missing_lines": [4],
"summary": {
"covered_branches": 0,
"covered_lines": 7,
"excluded_lines": 0,
"missing_branches": 0,
"missing_lines": 1,
"num_branches": 0,
"num_partial_branches": 0,
"num_statements": 8,
"percent_covered": 87.5,
"percent_covered_display": "88",
},
},
"C": {
"excluded_lines": [],
"executed_branches": [],
"executed_lines": [],
"missing_branches": [],
"missing_lines": [],
"summary": {
"covered_branches": 0,
"covered_lines": 0,
"excluded_lines": 0,
"missing_branches": 0,
"missing_lines": 0,
"num_branches": 0,
"num_partial_branches": 0,
"num_statements": 0,
"percent_covered": 100.0,
"percent_covered_display": "100",
},
},
"D": {
"excluded_lines": [],
"executed_branches": [],
"executed_lines": [],
"missing_branches": [[11, 12], [11, 13]],
"missing_lines": [11, 12, 13, 15],
"summary": {
"covered_branches": 0,
"covered_lines": 0,
"excluded_lines": 0,
"missing_branches": 2,
"missing_lines": 4,
"num_branches": 2,
"num_partial_branches": 0,
"num_statements": 4,
"percent_covered": 0.0,
"percent_covered_display": "0",
},
},
},
"excluded_lines": [],
"executed_branches": [],
"executed_lines": [1, 3, 6, 7, 9, 10, 14],
"functions": {
"": {
"excluded_lines": [],
"executed_branches": [],
"executed_lines": [1, 3, 6, 7, 9, 10, 14],
"missing_branches": [],
"missing_lines": [],
"summary": {
"covered_branches": 0,
"covered_lines": 7,
"excluded_lines": 0,
"missing_branches": 0,
"missing_lines": 0,
"num_branches": 0,
"num_partial_branches": 0,
"num_statements": 7,
"percent_covered": 100.0,
"percent_covered_display": "100",
},
},
"D.e": {
"excluded_lines": [],
"executed_branches": [],
"executed_lines": [],
"missing_branches": [[11, 12], [11, 13]],
"missing_lines": [11, 12, 13],
"summary": {
"covered_branches": 0,
"covered_lines": 0,
"excluded_lines": 0,
"missing_branches": 2,
"missing_lines": 3,
"num_branches": 2,
"num_partial_branches": 0,
"num_statements": 3,
"percent_covered": 0.0,
"percent_covered_display": "0",
},
},
"D.f": {
"excluded_lines": [],
"executed_branches": [],
"executed_lines": [],
"missing_branches": [],
"missing_lines": [15],
"summary": {
"covered_branches": 0,
"covered_lines": 0,
"excluded_lines": 0,
"missing_branches": 0,
"missing_lines": 1,
"num_branches": 0,
"num_partial_branches": 0,
"num_statements": 1,
"percent_covered": 0.0,
"percent_covered_display": "0",
},
},
"c": {
"excluded_lines": [],
"executed_branches": [],
"executed_lines": [],
"missing_branches": [],
"missing_lines": [4],
"summary": {
"covered_branches": 0,
"covered_lines": 0,
"excluded_lines": 0,
"missing_branches": 0,
"missing_lines": 1,
"num_branches": 0,
"num_partial_branches": 0,
"num_statements": 1,
"percent_covered": 0.0,
"percent_covered_display": "0",
},
},
},
"missing_branches": [[11, 12], [11, 13]],
"missing_lines": [4, 11, 12, 13, 15],
"summary": {
"covered_branches": 0,
"covered_lines": 7,
"excluded_lines": 0,
"missing_branches": 2,
"missing_lines": 5,
"num_branches": 2,
"num_partial_branches": 0,
"num_statements": 12,
"percent_covered": 50.0,
"percent_covered_display": "50",
},
},
},
"meta": {
"branch_coverage": True,
"format": 3,
"show_contexts": False,
},
"totals": {
"covered_branches": 0,
"covered_lines": 7,
"excluded_lines": 0,
"missing_branches": 2,
"missing_lines": 5,
"num_branches": 2,
"num_partial_branches": 0,
"num_statements": 12,
"percent_covered": 50.0,
"percent_covered_display": "50",
},
}
self._assert_expected_json_report_with_regions(cov, expected_result)
def run_context_test(self, relative_files: bool) -> None:
"""A helper for two tests below."""
self.make_file(
"config",
f"""\
[run]
relative_files = {relative_files}
[report]
precision = 2
[json]
show_contexts = True
""",
)
cov = coverage.Coverage(context="cool_test", config_file="config")
a_py_result = {
"executed_lines": [1, 2, 4, 5, 8],
"missing_lines": [3, 7, 9],
"excluded_lines": [],
"contexts": {
"1": ["cool_test"],
"2": ["cool_test"],
"4": ["cool_test"],
"5": ["cool_test"],
"8": ["cool_test"],
},
"summary": {
"excluded_lines": 0,
"missing_lines": 3,
"covered_lines": 5,
"num_statements": 8,
"percent_covered": 62.5,
"percent_covered_display": "62.50",
},
}
expected_result = {
"meta": {
"branch_coverage": False,
"format": 3,
"show_contexts": True,
},
"files": {
"a.py": copy.deepcopy(a_py_result),
},
"totals": {
"excluded_lines": 0,
"missing_lines": 3,
"covered_lines": 5,
"num_statements": 8,
"percent_covered": 62.5,
"percent_covered_display": "62.50",
},
}
# With regions, a lot of data is duplicated.
expected_result["files"]["a.py"]["classes"] = {"": a_py_result} # type: ignore[index]
expected_result["files"]["a.py"]["functions"] = {"": a_py_result} # type: ignore[index]
self._assert_expected_json_report(cov, expected_result)
def test_context_non_relative(self) -> None:
self.run_context_test(relative_files=False)
def test_context_relative(self) -> None:
self.run_context_test(relative_files=True)
def test_l1_equals_l2(self) -> None:
# In results.py, we had a line checking `if l1 == l2` that was never
# true. This test makes it true. The annotations are essential, I
# don't know why.
self.make_file(
"wtf.py",
"""\
def function(
x: int,
y: int,
) -> None:
return x + y
assert function(3, 5) == 8
""",
)
cov = coverage.Coverage(branch=True)
mod = self.start_import_stop(cov, "wtf")
cov.json_report(mod)