diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3536c507..37539678 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,7 +18,7 @@ repos: - id: check-added-large-files - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.6.3 + rev: v0.6.9 hooks: - id: ruff args: [ --fix ] diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bbd6c822..8f817690 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -64,3 +64,16 @@ To run the end-to-end tests, you'll need: - Please be aware that the tests will launch `gh auth setup-git` which might be surprising if you use `https` remotes (sadly, setting `GIT_CONFIG_GLOBAL` seems not to be enough to isolate tests.) + +## Coverage labs + +### Computing the coverage rate + +The coverage rate is `covered_lines / total_lines` (as one would expect). +In case "branch coverage" is enabled, the coverage rate is +`(covered_lines + covered_branches) / (total_lines + total_branches)`. +In order to display coverage rates, we need to round the values. Depending on +the situation, we either round to 0 or 2 decimal places. Rounding rules are: +- We always round down (truncate) the value. +- We don't display the trailing zeros in the decimal part (nor the decimal point + if the decimal part is 0). diff --git a/coverage_comment/coverage.py b/coverage_comment/coverage.py index 564649a5..591e9de9 100644 --- a/coverage_comment/coverage.py +++ b/coverage_comment/coverage.py @@ -13,7 +13,7 @@ # The dataclasses in this module are accessible in the template, which is overridable by the user. # As a coutesy, we should do our best to keep the existing fields for backward compatibility, # and if we really can't and can't add properties, at least bump the major version. -@dataclasses.dataclass +@dataclasses.dataclass(kw_only=True) class CoverageMetadata: version: str timestamp: datetime.datetime @@ -21,26 +21,28 @@ class CoverageMetadata: show_contexts: bool -@dataclasses.dataclass +@dataclasses.dataclass(kw_only=True) class CoverageInfo: covered_lines: int num_statements: int percent_covered: decimal.Decimal missing_lines: int excluded_lines: int - num_branches: int | None - num_partial_branches: int | None - covered_branches: int | None - missing_branches: int | None + num_branches: int = 0 + num_partial_branches: int = 0 + covered_branches: int = 0 + missing_branches: int = 0 -@dataclasses.dataclass +@dataclasses.dataclass(kw_only=True) class FileCoverage: path: pathlib.Path executed_lines: list[int] missing_lines: list[int] excluded_lines: list[int] info: CoverageInfo + executed_branches: list[list[int]] | None = None + missing_branches: list[list[int]] | None = None @dataclasses.dataclass @@ -56,7 +58,7 @@ class Coverage: # Maybe in v4, we can change it to a simpler format. -@dataclasses.dataclass +@dataclasses.dataclass(kw_only=True) class FileDiffCoverage: path: pathlib.Path percent_covered: decimal.Decimal @@ -73,7 +75,7 @@ def violation_lines(self) -> list[int]: return self.missing_statements -@dataclasses.dataclass +@dataclasses.dataclass(kw_only=True) class DiffCoverage: total_num_lines: int total_num_violations: int @@ -82,10 +84,18 @@ class DiffCoverage: files: dict[pathlib.Path, FileDiffCoverage] -def compute_coverage(num_covered: int, num_total: int) -> decimal.Decimal: - if num_total == 0: +def compute_coverage( + num_covered: int, + num_total: int, + num_branches_covered: int = 0, + num_branches_total: int = 0, +) -> decimal.Decimal: + """Compute the coverage percentage, with or without branch coverage.""" + numerator = decimal.Decimal(num_covered + num_branches_covered) + denominator = decimal.Decimal(num_total + num_branches_total) + if denominator == 0: return decimal.Decimal("1") - return decimal.Decimal(num_covered) / decimal.Decimal(num_total) + return numerator / denominator def get_coverage_info( @@ -138,6 +148,26 @@ def generate_coverage_markdown(coverage_path: pathlib.Path) -> str: ) +def _make_coverage_info(data: dict) -> CoverageInfo: + """Build a CoverageInfo object from a "summary" or "totals" key.""" + return CoverageInfo( + covered_lines=data["covered_lines"], + num_statements=data["num_statements"], + percent_covered=compute_coverage( + num_covered=data["covered_lines"], + num_total=data["num_statements"], + num_branches_covered=data.get("covered_branches", 0), + num_branches_total=data.get("num_branches", 0), + ), + missing_lines=data["missing_lines"], + excluded_lines=data["excluded_lines"], + num_branches=data.get("num_branches", 0), + num_partial_branches=data.get("num_partial_branches", 0), + covered_branches=data.get("covered_branches", 0), + missing_branches=data.get("missing_branches", 0), + ) + + def extract_info(data: dict, coverage_path: pathlib.Path) -> Coverage: """ { @@ -191,39 +221,13 @@ def extract_info(data: dict, coverage_path: pathlib.Path) -> Coverage: excluded_lines=file_data["excluded_lines"], executed_lines=file_data["executed_lines"], missing_lines=file_data["missing_lines"], - info=CoverageInfo( - covered_lines=file_data["summary"]["covered_lines"], - num_statements=file_data["summary"]["num_statements"], - percent_covered=compute_coverage( - file_data["summary"]["covered_lines"], - file_data["summary"]["num_statements"], - ), - missing_lines=file_data["summary"]["missing_lines"], - excluded_lines=file_data["summary"]["excluded_lines"], - num_branches=file_data["summary"].get("num_branches"), - num_partial_branches=file_data["summary"].get( - "num_partial_branches" - ), - covered_branches=file_data["summary"].get("covered_branches"), - missing_branches=file_data["summary"].get("missing_branches"), - ), + executed_branches=file_data.get("executed_branches"), + missing_branches=file_data.get("missing_branches"), + info=_make_coverage_info(file_data["summary"]), ) for path, file_data in data["files"].items() }, - info=CoverageInfo( - covered_lines=data["totals"]["covered_lines"], - num_statements=data["totals"]["num_statements"], - percent_covered=compute_coverage( - data["totals"]["covered_lines"], - data["totals"]["num_statements"], - ), - missing_lines=data["totals"]["missing_lines"], - excluded_lines=data["totals"]["excluded_lines"], - num_branches=data["totals"].get("num_branches"), - num_partial_branches=data["totals"].get("num_partial_branches"), - covered_branches=data["totals"].get("covered_branches"), - missing_branches=data["totals"].get("missing_branches"), - ), + info=_make_coverage_info(data["totals"]), ) @@ -256,7 +260,8 @@ def get_diff_coverage_info( total_num_violations += count_missing percent_covered = compute_coverage( - num_covered=count_executed, num_total=count_total + num_covered=count_executed, + num_total=count_total, ) files[path] = FileDiffCoverage( diff --git a/poetry.lock b/poetry.lock index 397ffed1..5dc7e54b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2,13 +2,13 @@ [[package]] name = "anyio" -version = "4.4.0" +version = "4.6.0" description = "High level compatibility layer for multiple asynchronous event loop implementations" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "anyio-4.4.0-py3-none-any.whl", hash = "sha256:c1b2d8f46a8a812513012e1107cb0e68c17159a7a594208005a57dc776e1bdc7"}, - {file = "anyio-4.4.0.tar.gz", hash = "sha256:5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94"}, + {file = "anyio-4.6.0-py3-none-any.whl", hash = "sha256:c7d2e9d63e31599eeb636c8c5c03a7e108d73b345f064f1c19fdc87b79036a9a"}, + {file = "anyio-4.6.0.tar.gz", hash = "sha256:137b4559cbb034c477165047febb6ff83f390fc3b20bf181c1fc0a728cb8beeb"}, ] [package.dependencies] @@ -16,9 +16,9 @@ idna = ">=2.8" sniffio = ">=1.1" [package.extras] -doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] -test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] -trio = ["trio (>=0.23)"] +doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.21.0b1)"] +trio = ["trio (>=0.26.1)"] [[package]] name = "certifi" @@ -223,15 +223,18 @@ files = [ [[package]] name = "idna" -version = "3.8" +version = "3.10" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.6" files = [ - {file = "idna-3.8-py3-none-any.whl", hash = "sha256:050b4e5baadcd44d760cedbd2b8e639f2ff89bbc7a5730fcc662954303377aac"}, - {file = "idna-3.8.tar.gz", hash = "sha256:d838c2c0ed6fced7693d5e8ab8e734d5f8fda53a039c0164afb0b82e771e3603"}, + {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, + {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, ] +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + [[package]] name = "iniconfig" version = "2.0.0" @@ -414,13 +417,13 @@ testing = ["pytest", "pytest-benchmark"] [[package]] name = "pytest" -version = "8.3.2" +version = "8.3.3" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" files = [ - {file = "pytest-8.3.2-py3-none-any.whl", hash = "sha256:4ba08f9ae7dcf84ded419494d229b48d0903ea6407b030eaec46df5e6a73bba5"}, - {file = "pytest-8.3.2.tar.gz", hash = "sha256:c132345d12ce551242c87269de812483f5bcc87cdbb4722e48487ba194f9fdce"}, + {file = "pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2"}, + {file = "pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181"}, ] [package.dependencies] @@ -469,29 +472,29 @@ dev = ["pre-commit", "pytest-asyncio", "tox"] [[package]] name = "ruff" -version = "0.6.3" +version = "0.6.9" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.6.3-py3-none-linux_armv6l.whl", hash = "sha256:97f58fda4e309382ad30ede7f30e2791d70dd29ea17f41970119f55bdb7a45c3"}, - {file = "ruff-0.6.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3b061e49b5cf3a297b4d1c27ac5587954ccb4ff601160d3d6b2f70b1622194dc"}, - {file = "ruff-0.6.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:34e2824a13bb8c668c71c1760a6ac7d795ccbd8d38ff4a0d8471fdb15de910b1"}, - {file = "ruff-0.6.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bddfbb8d63c460f4b4128b6a506e7052bad4d6f3ff607ebbb41b0aa19c2770d1"}, - {file = "ruff-0.6.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ced3eeb44df75353e08ab3b6a9e113b5f3f996bea48d4f7c027bc528ba87b672"}, - {file = "ruff-0.6.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:47021dff5445d549be954eb275156dfd7c37222acc1e8014311badcb9b4ec8c1"}, - {file = "ruff-0.6.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:7d7bd20dc07cebd68cc8bc7b3f5ada6d637f42d947c85264f94b0d1cd9d87384"}, - {file = "ruff-0.6.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:500f166d03fc6d0e61c8e40a3ff853fa8a43d938f5d14c183c612df1b0d6c58a"}, - {file = "ruff-0.6.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:42844ff678f9b976366b262fa2d1d1a3fe76f6e145bd92c84e27d172e3c34500"}, - {file = "ruff-0.6.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70452a10eb2d66549de8e75f89ae82462159855e983ddff91bc0bce6511d0470"}, - {file = "ruff-0.6.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:65a533235ed55f767d1fc62193a21cbf9e3329cf26d427b800fdeacfb77d296f"}, - {file = "ruff-0.6.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d2e2c23cef30dc3cbe9cc5d04f2899e7f5e478c40d2e0a633513ad081f7361b5"}, - {file = "ruff-0.6.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d8a136aa7d228975a6aee3dd8bea9b28e2b43e9444aa678fb62aeb1956ff2351"}, - {file = "ruff-0.6.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:f92fe93bc72e262b7b3f2bba9879897e2d58a989b4714ba6a5a7273e842ad2f8"}, - {file = "ruff-0.6.3-py3-none-win32.whl", hash = "sha256:7a62d3b5b0d7f9143d94893f8ba43aa5a5c51a0ffc4a401aa97a81ed76930521"}, - {file = "ruff-0.6.3-py3-none-win_amd64.whl", hash = "sha256:746af39356fee2b89aada06c7376e1aa274a23493d7016059c3a72e3b296befb"}, - {file = "ruff-0.6.3-py3-none-win_arm64.whl", hash = "sha256:14a9528a8b70ccc7a847637c29e56fd1f9183a9db743bbc5b8e0c4ad60592a82"}, - {file = "ruff-0.6.3.tar.gz", hash = "sha256:183b99e9edd1ef63be34a3b51fee0a9f4ab95add123dbf89a71f7b1f0c991983"}, + {file = "ruff-0.6.9-py3-none-linux_armv6l.whl", hash = "sha256:064df58d84ccc0ac0fcd63bc3090b251d90e2a372558c0f057c3f75ed73e1ccd"}, + {file = "ruff-0.6.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:140d4b5c9f5fc7a7b074908a78ab8d384dd7f6510402267bc76c37195c02a7ec"}, + {file = "ruff-0.6.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:53fd8ca5e82bdee8da7f506d7b03a261f24cd43d090ea9db9a1dc59d9313914c"}, + {file = "ruff-0.6.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645d7d8761f915e48a00d4ecc3686969761df69fb561dd914a773c1a8266e14e"}, + {file = "ruff-0.6.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eae02b700763e3847595b9d2891488989cac00214da7f845f4bcf2989007d577"}, + {file = "ruff-0.6.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d5ccc9e58112441de8ad4b29dcb7a86dc25c5f770e3c06a9d57e0e5eba48829"}, + {file = "ruff-0.6.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:417b81aa1c9b60b2f8edc463c58363075412866ae4e2b9ab0f690dc1e87ac1b5"}, + {file = "ruff-0.6.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3c866b631f5fbce896a74a6e4383407ba7507b815ccc52bcedabb6810fdb3ef7"}, + {file = "ruff-0.6.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7b118afbb3202f5911486ad52da86d1d52305b59e7ef2031cea3425142b97d6f"}, + {file = "ruff-0.6.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a67267654edc23c97335586774790cde402fb6bbdb3c2314f1fc087dee320bfa"}, + {file = "ruff-0.6.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3ef0cc774b00fec123f635ce5c547dac263f6ee9fb9cc83437c5904183b55ceb"}, + {file = "ruff-0.6.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:12edd2af0c60fa61ff31cefb90aef4288ac4d372b4962c2864aeea3a1a2460c0"}, + {file = "ruff-0.6.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:55bb01caeaf3a60b2b2bba07308a02fca6ab56233302406ed5245180a05c5625"}, + {file = "ruff-0.6.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:925d26471fa24b0ce5a6cdfab1bb526fb4159952385f386bdcc643813d472039"}, + {file = "ruff-0.6.9-py3-none-win32.whl", hash = "sha256:eb61ec9bdb2506cffd492e05ac40e5bc6284873aceb605503d8494180d6fc84d"}, + {file = "ruff-0.6.9-py3-none-win_amd64.whl", hash = "sha256:785d31851c1ae91f45b3d8fe23b8ae4b5170089021fbb42402d811135f0b7117"}, + {file = "ruff-0.6.9-py3-none-win_arm64.whl", hash = "sha256:a9641e31476d601f83cd602608739a0840e348bda93fec9f1ee816f8b6798b93"}, + {file = "ruff-0.6.9.tar.gz", hash = "sha256:b076ef717a8e5bc819514ee1d602bbdca5b4420ae13a9cf61a0c0a4f53a2baa2"}, ] [[package]] diff --git a/tests/conftest.py b/tests/conftest.py index 7aead5fe..84ac63cf 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -282,10 +282,10 @@ def _(code: str, has_branches: bool = True) -> coverage_module.Coverage: percent_covered=decimal.Decimal("1.0"), missing_lines=0, excluded_lines=0, - num_branches=0 if has_branches else None, - num_partial_branches=0 if has_branches else None, - covered_branches=0 if has_branches else None, - missing_branches=0 if has_branches else None, + num_branches=0, + num_partial_branches=0, + covered_branches=0, + missing_branches=0, ), files={}, ) @@ -313,10 +313,10 @@ def _(code: str, has_branches: bool = True) -> coverage_module.Coverage: percent_covered=decimal.Decimal("1.0"), missing_lines=0, excluded_lines=0, - num_branches=0 if has_branches else None, - num_partial_branches=0 if has_branches else None, - covered_branches=0 if has_branches else None, - missing_branches=0 if has_branches else None, + num_branches=0, + num_partial_branches=0, + covered_branches=0, + missing_branches=0, ), ) if set(line.split()) & { @@ -340,7 +340,6 @@ def _(code: str, has_branches: bool = True) -> coverage_module.Coverage: coverage_obj.files[current_file].excluded_lines.append(line_number) coverage_obj.files[current_file].info.excluded_lines += 1 coverage_obj.info.excluded_lines += 1 - if has_branches and "branch" in line: coverage_obj.files[current_file].info.num_branches += 1 coverage_obj.info.num_branches += 1 @@ -353,21 +352,22 @@ def _(code: str, has_branches: bool = True) -> coverage_module.Coverage: elif "branch missing" in line: coverage_obj.files[current_file].info.missing_branches += 1 coverage_obj.info.missing_branches += 1 - info = coverage_obj.files[current_file].info coverage_obj.files[ current_file ].info.percent_covered = coverage_module.compute_coverage( num_covered=info.covered_lines, num_total=info.num_statements, + num_branches_covered=info.covered_branches, + num_branches_total=info.num_branches, ) - info = coverage_obj.info coverage_obj.info.percent_covered = coverage_module.compute_coverage( num_covered=info.covered_lines, num_total=info.num_statements, + num_branches_covered=info.covered_branches, + num_branches_total=info.num_branches, ) - return coverage_obj return _ @@ -425,9 +425,19 @@ def coverage_code(): 9 10 branch missing 11 missing - 12 + 12 covered 13 branch covered 14 covered + 15 branch partial + 16 branch covered + 17 branch missing + 18 covered + 19 covered + 20 branch partial + 21 branch missing + 22 branch covered + 23 branch covered + 24 branch covered """ @@ -442,32 +452,48 @@ def coverage_json(): }, "files": { "codebase/code.py": { - "executed_lines": [1, 2, 3, 5, 13, 14], + "executed_lines": [ + 1, + 2, + 3, + 5, + 12, + 13, + 14, + 15, + 16, + 18, + 19, + 20, + 22, + 23, + 24, + ], "summary": { - "covered_lines": 6, - "num_statements": 10, - "percent_covered": 60.0, - "missing_lines": 4, + "covered_lines": 15, + "num_statements": 21, + "percent_covered": 0.625, + "missing_lines": 6, "excluded_lines": 0, - "num_branches": 3, - "num_partial_branches": 1, - "covered_branches": 1, - "missing_branches": 1, + "num_branches": 11, + "num_partial_branches": 3, + "covered_branches": 5, + "missing_branches": 3, }, - "missing_lines": [6, 8, 10, 11], + "missing_lines": [6, 8, 10, 11, 17, 21], "excluded_lines": [], } }, "totals": { - "covered_lines": 6, - "num_statements": 10, - "percent_covered": 60.0, - "missing_lines": 4, + "covered_lines": 15, + "num_statements": 21, + "percent_covered": 0.625, + "missing_lines": 6, "excluded_lines": 0, - "num_branches": 3, - "num_partial_branches": 1, - "covered_branches": 1, - "missing_branches": 1, + "num_branches": 11, + "num_partial_branches": 3, + "covered_branches": 5, + "missing_branches": 3, }, } diff --git a/tests/unit/test_coverage.py b/tests/unit/test_coverage.py index 6ea249a2..621219da 100644 --- a/tests/unit/test_coverage.py +++ b/tests/unit/test_coverage.py @@ -39,6 +39,24 @@ def test_compute_coverage(num_covered, num_total, expected_coverage): ) +@pytest.mark.parametrize( + "num_covered, num_total, branch_covered, branch_total, expected_coverage", + [ + (0, 10, 0, 15, "0"), + (0, 0, 0, 0, "1"), + (5, 0, 5, 0, "1"), + (5, 10, 5, 10, "0.5"), + (1, 50, 1, 50, "0.02"), + ], +) +def test_compute_coverage_with_branches( + num_covered, num_total, branch_covered, branch_total, expected_coverage +): + assert coverage.compute_coverage( + num_covered, num_total, branch_covered, branch_total + ) == decimal.Decimal(expected_coverage) + + def test_get_coverage_info(mocker, coverage_json, coverage_obj): run = mocker.patch( "coverage_comment.subprocess.run", return_value=json.dumps(coverage_json) @@ -132,6 +150,42 @@ def test_generate_coverage_markdown(mocker): assert result == "foo" +def test__make_coverage_info(): + result = coverage._make_coverage_info( + { + "covered_lines": 14, + "num_statements": 20, + "missing_lines": 6, + "excluded_lines": 0, + } + ) + assert isinstance(result, coverage.CoverageInfo) + assert result.percent_covered == decimal.Decimal(14) / decimal.Decimal(20) + assert result.num_branches == 0 + assert result.num_partial_branches == 0 + assert result.covered_branches == 0 + assert result.missing_branches == 0 + + +def test__make_coverage_info__with_branches(): + result = coverage._make_coverage_info( + { + "covered_lines": 4, + "num_statements": 10, + "missing_lines": 1, + "excluded_lines": 0, + "covered_branches": 4, + "num_branches": 6, + "num_partial_branches": 2, + } + ) + assert isinstance(result, coverage.CoverageInfo) + assert result.percent_covered == decimal.Decimal(4 + 4) / decimal.Decimal(10 + 6) + assert result.covered_branches == 4 + assert result.missing_branches == 0 + assert result.excluded_lines == 0 + + @pytest.mark.parametrize( "added_lines, update_obj, expected", [ diff --git a/tests/unit/test_template.py b/tests/unit/test_template.py index 62820423..521b5dc8 100644 --- a/tests/unit/test_template.py +++ b/tests/unit/test_template.py @@ -44,7 +44,7 @@ def test_get_comment_markdown(coverage_obj, diff_coverage_obj): .split(maxsplit=4) ) - expected = ["92%", "60%", "50%", "bar", ""] + expected = ["92%", "62.5%", "60%", "bar", ""] assert result == expected @@ -79,17 +79,17 @@ def test_template(coverage_obj, diff_coverage_obj): expected = """## Coverage report (foo) -
Click to see where and how coverage changed +
Click to see where and how coverage changed
- + - +
FileStatementsMissingCoverageCoverage
(new stmts)
Lines missing
  codebase
  code.py6-8
6-8
Project Total  
@@ -264,17 +264,17 @@ def test_template__no_previous(coverage_obj_no_branch, diff_coverage_obj): expected = """## Coverage report -
Click to see where and how coverage changed +
Click to see where and how coverage changed
- + - +
FileStatementsMissingCoverageCoverage
(new stmts)
Lines missing
  codebase
  code.py6-8
6-8
Project Total