Skip to content

Tautology

tautology

Tautology rule — mechanical detection + delete-side triage.

Detects six tautology patterns in test bodies and classifies each finding via :func:tautology_triage.triage. Verdicts land in CheckResult.metadata["verdicts"] so downstream tooling can filter by DELETE/STRENGTHEN/UNKNOWN.

Finding dataclass

One tautology finding for a single test function.

Source code in packages/axm-audit/src/axm_audit/core/rules/test_quality/tautology.py
Python
@dataclass
class Finding:
    """One tautology finding for a single test function."""

    test: str
    line: int
    pattern: str
    detail: str
    path: str = ""

TautologyCheckResult

Bases: CheckResult

:class:CheckResult with metadata carrying the verdict list.

Source code in packages/axm-audit/src/axm_audit/core/rules/test_quality/tautology.py
Python
class TautologyCheckResult(CheckResult):
    """:class:`CheckResult` with ``metadata`` carrying the verdict list."""

    metadata: dict[str, Any] = Field(default_factory=dict)
    score: int = 100

    model_config = {"extra": "forbid"}

TautologyRule dataclass

Bases: ProjectRule

Detect tautological test assertions and triage each finding.

Source code in packages/axm-audit/src/axm_audit/core/rules/test_quality/tautology.py
Python
@register_rule("test_quality")
@dataclass
class TautologyRule(ProjectRule):
    """Detect tautological test assertions and triage each finding."""

    _verdicts: list[dict[str, Any]] = field(
        default_factory=list, init=False, repr=False
    )

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

    def check(self, project_path: Path) -> TautologyCheckResult:
        """Scan test files in ``project_path`` and return tautology verdicts."""
        pkg_symbols = collect_pkg_public_symbols(project_path)
        contracts = collect_pkg_contract_classes(project_path)

        all_verdicts: list[dict[str, Any]] = []
        for test_file, tree in self._iter_test_files_with_fallback(project_path):
            if tree is None:
                continue
            all_verdicts.extend(
                self._verdicts_for_file(
                    test_file, tree, project_path, pkg_symbols, contracts
                )
            )

        n = len(all_verdicts)
        score = max(0, 100 - n * _SCORE_PENALTY)
        passed = n == 0
        message = "no tautologies found" if passed else f"{n} tautology finding(s)"
        return TautologyCheckResult(
            rule_id=self.rule_id,
            passed=passed,
            message=message,
            severity=Severity.WARNING,
            metadata={"verdicts": all_verdicts},
            score=score,
        )

    @staticmethod
    def _iter_test_files_with_fallback(
        project_path: Path,
    ) -> Iterator[tuple[Path, ast.Module | None]]:
        tests_dir = project_path / "tests"
        if tests_dir.exists():
            yield from iter_test_files(project_path)
            return
        for p in sorted(project_path.rglob("test_*.py")):
            try:
                yield p, ast.parse(p.read_text(), filename=str(p))
            except (OSError, SyntaxError, UnicodeDecodeError):
                yield p, None

    @staticmethod
    def _verdicts_for_file(
        test_file: Path,
        tree: ast.Module,
        project_path: Path,
        pkg_symbols: Any,
        contracts: Any,
    ) -> list[dict[str, Any]]:
        try:
            source = test_file.read_text()
        except (OSError, UnicodeDecodeError):
            source = ""
        try:
            rel = str(test_file.relative_to(project_path))
        except ValueError:
            rel = str(test_file)
        findings = detect_tautologies(tree, path=rel)
        if not findings:
            return []
        helpers = _collect_helpers(tree)
        verdicts: list[dict[str, Any]] = []
        for f in findings:
            loc = _find_func(tree, f.test)
            if loc is None:
                continue
            v = triage(
                f,
                tree=tree,
                func=loc.func,
                enclosing_class=loc.enclosing_class,
                helpers=helpers,
                pkg_symbols=pkg_symbols,
                contracts=contracts,
                test_file=test_file,
                source_text=source,
            )
            verdicts.append(
                {
                    "file": rel,
                    "test": f.test,
                    "line": f.line,
                    "pattern": f.pattern,
                    "rule": v.rule,
                    "verdict": v.action,
                    "reason": v.reason,
                }
            )
        return verdicts
rule_id property

Stable identifier for this rule.

check(project_path)

Scan test files in project_path and return tautology verdicts.

Source code in packages/axm-audit/src/axm_audit/core/rules/test_quality/tautology.py
Python
def check(self, project_path: Path) -> TautologyCheckResult:
    """Scan test files in ``project_path`` and return tautology verdicts."""
    pkg_symbols = collect_pkg_public_symbols(project_path)
    contracts = collect_pkg_contract_classes(project_path)

    all_verdicts: list[dict[str, Any]] = []
    for test_file, tree in self._iter_test_files_with_fallback(project_path):
        if tree is None:
            continue
        all_verdicts.extend(
            self._verdicts_for_file(
                test_file, tree, project_path, pkg_symbols, contracts
            )
        )

    n = len(all_verdicts)
    score = max(0, 100 - n * _SCORE_PENALTY)
    passed = n == 0
    message = "no tautologies found" if passed else f"{n} tautology finding(s)"
    return TautologyCheckResult(
        rule_id=self.rule_id,
        passed=passed,
        message=message,
        severity=Severity.WARNING,
        metadata={"verdicts": all_verdicts},
        score=score,
    )

detect_tautologies(tree, *, path='')

Return :class:Finding entries for every tautological test in tree.

Source code in packages/axm-audit/src/axm_audit/core/rules/test_quality/tautology.py
Python
def detect_tautologies(tree: ast.Module, *, path: str = "") -> list[Finding]:
    """Return :class:`Finding` entries for every tautological test in *tree*."""
    findings: list[Finding] = []
    module_mocks = _extract_mock_setups(tree.body)
    for node in ast.walk(tree):
        if not (isinstance(node, ast.FunctionDef) and node.name.startswith("test_")):
            continue
        for f in _analyze_test_function(node, module_mocks):
            f.path = path
            findings.append(f)
    return findings