Skip to content

Pyramid level

pyramid_level

Pyramid-level soft-signal rule — R1+R2+R3 core.

Classifies every tests/**/test_*.py function into unit / integration / e2e based on attr-IO, fixture arguments, taint of tmp_path, and import provenance (public vs internal). Mismatches between the classified level and the folder the test lives in are reported as findings.

R4 (conftest fixture-IO resolution) and R5 (mock neutralisation) are stubbed here as identities — ticket #4b replaces the two placeholders.

Finding

Bases: BaseModel

One classification verdict for a single test function.

Source code in packages/axm-audit/src/axm_audit/core/rules/test_quality/pyramid_level.py
Python
class Finding(BaseModel):
    """One classification verdict for a single test function."""

    path: str
    function: str
    level: str
    reason: str
    current_level: str
    has_real_io: bool
    has_subprocess: bool
    io_signals: list[str] = Field(default_factory=list)
    imports_public: list[str] = Field(default_factory=list)
    imports_internal: list[str] = Field(default_factory=list)
    suggested_file: str = ""
    severity: Severity = Severity.WARNING

    model_config = {"extra": "forbid"}

PyramidCheckResult

Bases: CheckResult

:class:CheckResult subclass exposing findings and score.

Source code in packages/axm-audit/src/axm_audit/core/rules/test_quality/pyramid_level.py
Python
class PyramidCheckResult(CheckResult):
    """:class:`CheckResult` subclass exposing ``findings`` and ``score``."""

    findings: list[Finding] = Field(default_factory=list)
    score: int = 100

    model_config = {"extra": "forbid"}

PyramidLevelRule dataclass

Bases: ProjectRule

Report tests whose classified pyramid level mismatches their folder.

Source code in packages/axm-audit/src/axm_audit/core/rules/test_quality/pyramid_level.py
Python
@register_rule("test_quality")
@dataclass
class PyramidLevelRule(ProjectRule):
    """Report tests whose classified pyramid level mismatches their folder."""

    strict_mismatches: bool = True

    @property
    def rule_id(self) -> str:
        """Stable identifier for this rule."""
        return "TEST_QUALITY_PYRAMID_LEVEL"

    def check(self, project_path: Path) -> PyramidCheckResult:
        """Classify tests in ``project_path`` against their pyramid folder."""
        tests_dir = project_path / "tests"
        if not tests_dir.exists():
            return PyramidCheckResult(
                rule_id=self.rule_id,
                passed=True,
                message="no tests/ directory",
                severity=Severity.INFO,
                score=100,
            )

        all_findings = scan_package(project_path)
        mismatches = [
            f for f in all_findings if f.current_level not in ("root", f.level)
        ]
        count = len(mismatches) if self.strict_mismatches else 0
        score = max(0, 100 - count * _SCORE_PENALTY)
        passed = count == 0
        message = (
            "pyramid levels match folder layout"
            if passed
            else f"{count} test(s) mis-located vs. classified pyramid level"
        )
        return PyramidCheckResult(
            rule_id=self.rule_id,
            passed=passed,
            message=message,
            severity=Severity.WARNING,
            findings=all_findings,
            score=score,
        )
rule_id property

Stable identifier for this rule.

check(project_path)

Classify tests in project_path against their pyramid folder.

Source code in packages/axm-audit/src/axm_audit/core/rules/test_quality/pyramid_level.py
Python
def check(self, project_path: Path) -> PyramidCheckResult:
    """Classify tests in ``project_path`` against their pyramid folder."""
    tests_dir = project_path / "tests"
    if not tests_dir.exists():
        return PyramidCheckResult(
            rule_id=self.rule_id,
            passed=True,
            message="no tests/ directory",
            severity=Severity.INFO,
            score=100,
        )

    all_findings = scan_package(project_path)
    mismatches = [
        f for f in all_findings if f.current_level not in ("root", f.level)
    ]
    count = len(mismatches) if self.strict_mismatches else 0
    score = max(0, 100 - count * _SCORE_PENALTY)
    passed = count == 0
    message = (
        "pyramid levels match folder layout"
        if passed
        else f"{count} test(s) mis-located vs. classified pyramid level"
    )
    return PyramidCheckResult(
        rule_id=self.rule_id,
        passed=passed,
        message=message,
        severity=Severity.WARNING,
        findings=all_findings,
        score=score,
    )

scan_package(pkg_root)

Scan every test file under <pkg_root>/tests and return findings.

Source code in packages/axm-audit/src/axm_audit/core/rules/test_quality/pyramid_level.py
Python
def scan_package(pkg_root: Path) -> list[Finding]:
    """Scan every test file under ``<pkg_root>/tests`` and return findings."""
    tests_dir = pkg_root / "tests"
    if not tests_dir.exists():
        return []
    pkg_prefixes = get_pkg_prefixes(pkg_root)
    init_all = get_init_all(pkg_root)
    findings: list[Finding] = []
    for test_file, tree in iter_test_files(pkg_root):
        if tree is None:
            continue
        findings.extend(
            scan_test_file(test_file, tree, pkg_root, pkg_prefixes, init_all, tests_dir)
        )
    return findings

scan_test_file(test_file, tree, pkg_root, pkg_prefixes, init_all, tests_dir)

Classify every test_* function in tree and return findings.

Emits one :class:Finding per function whose classified level differs from its folder-derived current level.

Source code in packages/axm-audit/src/axm_audit/core/rules/test_quality/pyramid_level.py
Python
def scan_test_file(  # noqa: PLR0913
    test_file: Path,
    tree: ast.Module,
    pkg_root: Path,
    pkg_prefixes: set[str],
    init_all: set[str] | None,
    tests_dir: Path,
) -> list[Finding]:
    """Classify every ``test_*`` function in *tree* and return findings.

    Emits one :class:`Finding` per function whose classified level differs
    from its folder-derived current level.
    """
    ctx = _build_scan_context(
        test_file, tree, pkg_root, pkg_prefixes, init_all, tests_dir
    )
    return [
        _classify_test_function(ctx, node)
        for node in ast.walk(tree)
        if isinstance(node, ast.FunctionDef) and node.name.startswith("test_")
    ]