Ctrl K
ComunidadGuías…

tests/test_coverage_policy.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""The coverage policy, enforced rather than documented.

Two things keep the gate honest, checked here rather than left to intention:
every exclusion carries a written justification — a bare `# pragma: no cover`
fails, as does an `omit` entry with no "Justification:" above it — and
`fail_under` may only ever go up.

Worth stating plainly, because a percentage invites more confidence than it
earns: line coverage says a line RAN, not that anything checked what it did.
The value in this suite comes from the hand-computed ground truths and the
mutation checks. This gate stops coverage rotting; it does not measure quality.
"""

from __future__ import annotations

import re
import tomllib
from pathlib import Path

import pytest

APP = Path(__file__).parent.parent / "app"
PYPROJECT = Path(__file__).parent.parent / "pyproject.toml"

# The floor. Raise it when coverage improves; never lower it.
#
# What is left uncovered is mostly async loops and the two functions that reach
# the outside world.
MINIMUM_GATE = 92

# A justification has to actually say something. Twenty characters is enough to
# stop "n/a" and "obvious" while not demanding an essay.
MIN_JUSTIFICATION = 20

PRAGMA = re.compile(r"#\s*pragma:\s*no cover(?P<reason>.*)$")


def python_files() -> list[Path]:
    return sorted(p for p in APP.rglob("*.py") if "migrations" not in p.parts)


def config() -> dict:
    return tomllib.loads(PYPROJECT.read_text(encoding="utf-8"))


# --------------------------------------------------------------------------- #
# The gate
# --------------------------------------------------------------------------- #

def test_the_coverage_gate_is_configured():
    report = config()["tool"]["coverage"]["report"]

    assert "fail_under" in report, (
        "Coverage is measured but not enforced — set fail_under in "
        "[tool.coverage.report], or the number is decoration."
    )


def test_the_gate_has_not_been_lowered():
    """The one thing that makes a coverage gate worth having.

    If a change drops coverage, the fix is a test. Editing this number down to
    make a change fit is the exact failure this guards against — so moving it
    requires editing MINIMUM_GATE here too, deliberately, in the same commit.
    """
    gate = config()["tool"]["coverage"]["report"]["fail_under"]

    assert gate >= MINIMUM_GATE, (
        f"The coverage gate is {gate}, below the recorded floor of "
        f"{MINIMUM_GATE}. Raise coverage rather than lowering the gate."
    )


# --------------------------------------------------------------------------- #
# Justifications
# --------------------------------------------------------------------------- #

def test_every_no_cover_pragma_says_why():
    """A bare `# pragma: no cover` is how a coverage target becomes a fiction.

    Write the reason on the same line:
        something_untestable()  # pragma: no cover - needs a real signal
    """
    unjustified: list[str] = []
    for path in python_files():
        for number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1):
            match = PRAGMA.search(line)
            if not match:
                continue
            reason = match.group("reason").strip(" -—:#")
            if len(reason) < MIN_JUSTIFICATION:
                unjustified.append(
                    f"{path.relative_to(APP.parent)}:{number}: {line.strip()}"
                )

    assert not unjustified, (
        "These exclusions have no justification (or too short a one). Say why "
        "the line cannot be tested, on the same line as the pragma:\n  "
        + "\n  ".join(unjustified)
    )


def justifications_in(section: str) -> dict[str, str]:
    """Map each entry of a pyproject list to the comment block above it.

    Walked forwards, line by line: a comment block accumulates and is claimed
    by the next entry it precedes, then resets. Scanning backwards from a
    string index is fiddlier and gets confused by the partial line the index
    lands in.
    """
    found: dict[str, str] = {}
    block: list[str] = []
    inside = False
    for raw in PYPROJECT.read_text(encoding="utf-8").splitlines():
        line = raw.strip()
        if line.startswith(f"{section} = ["):
            inside = True
            continue
        if not inside:
            continue
        if line == "]":
            break
        if line.startswith("#"):
            block.append(line.lstrip("# "))
        elif line.startswith(("'", '"')):
            found[line.strip(",").strip("'\"")] = " ".join(block)
            block = []
    return found


def test_every_omitted_file_says_why():
    """An `omit` entry hides a whole file from the count, which is the largest
    thing anyone can do to this number — so each one is argued in the file."""
    omitted = config()["tool"]["coverage"]["report"].get("omit", [])
    assert omitted, "Nothing omitted — remove this test rather than leaving it vacuous."
    reasons = justifications_in("omit")

    for pattern in omitted:
        justification = reasons.get(pattern, "")
        assert "Justification:" in justification, (
            f"The omit entry {pattern!r} has no 'Justification:' comment above "
            f"it. Omitting a file removes it from the count entirely — say why "
            f"it cannot or should not be tested."
        )
        assert len(justification) > MIN_JUSTIFICATION * 2, (
            f"The justification for omitting {pattern!r} is too thin to review."
        )


def test_every_excluded_pattern_says_why():
    excluded = config()["tool"]["coverage"]["report"].get("exclude_also", [])
    reasons = justifications_in("exclude_also")

    for pattern in excluded:
        justification = reasons.get(pattern, "")
        assert "Justification:" in justification, (
            f"The exclude_also pattern {pattern!r} has no justification above it."
        )


# --------------------------------------------------------------------------- #
# Nothing quietly opts out
# --------------------------------------------------------------------------- #

def test_no_module_disables_coverage_wholesale():
    """A file-level `# coverage: off` would hide a module without appearing in
    the omit list, where it can at least be reviewed."""
    offenders = [
        str(p.relative_to(APP.parent))
        for p in python_files()
        if re.search(r"#\s*coverage:\s*off", p.read_text(encoding="utf-8"))
    ]

    assert not offenders, (
        "These files switch coverage off from the inside, which bypasses the "
        f"reviewable omit list: {offenders}"
    )


@pytest.mark.parametrize("module", ["queries", "fyreport", "charts_build", "exports",
                                    "tenancy", "auth"])
def test_the_modules_that_decide_money_and_access_are_never_omitted(module):
    """A belt on the omit list: whatever else gets excluded, the code that
    computes somebody's tax position or decides what they can see does not."""
    omitted = config()["tool"]["coverage"]["report"].get("omit", [])

    assert f"app/{module}.py" not in omitted