rules_testisolation.py

Code Hygiene Score: 95

Keine Issues gefunden.

Dependencies 4

Klassen 7

Funktionen 1

Code

#!/usr/bin/env python3
"""
Post-Hook Test Isolation Regeln (WARN) - Test Isolation.

W14.x Regeln: Warnt bei potenziellen Test-Isolation-Problemen.

Prinzip: "Tests beeinflussen sich nicht gegenseitig. Globaler Zustand ist kontrolliert."

WICHTIG: Diese Regeln gelten NUR für Test-Dateien (/tests/, /Test/).
"""

import re
from typing import List
from .rule_base import Rule


# =============================================================================
# KONFIGURATION
# =============================================================================

# Pfade die als Test-Dateien gelten
TEST_PATHS = ["/tests/", "/Test/"]


# =============================================================================
# HELPER
# =============================================================================

def is_test_file(file_path: str) -> bool:
    """Prüft ob die Datei eine Test-Datei ist."""
    return any(test_path in file_path for test_path in TEST_PATHS)


# =============================================================================
# W14: TEST ISOLATION
# =============================================================================

class W14_1_MissingTearDown(Rule):
    """W14.1: Test-Klasse ohne tearDown-Methode bei Setup."""

    def __init__(self):
        # Leere Allowlist - Tests werden explizit geprüft
        super().__init__(allowlist=[])

    def should_skip(self, file_path: str) -> bool:
        # Nur Test-Dateien prüfen
        return not is_test_file(file_path)

    def check(self, file_path: str, content: str) -> List[str]:
        warnings = []

        # Hat die Klasse setUp?
        has_setup = bool(re.search(
            r"(?:protected|public)\s+function\s+setUp\s*\(",
            content
        ))

        # Hat die Klasse tearDown?
        has_teardown = bool(re.search(
            r"(?:protected|public)\s+function\s+tearDown\s*\(",
            content
        ))

        if has_setup and not has_teardown:
            warnings.append(
                "W14.1: Test has setUp() but no tearDown(). "
                "Consider adding tearDown() to clean up test state."
            )

        return warnings


class W14_2_FileSystemAccess(Rule):
    """W14.2: Direkter Dateisystem-Zugriff in Tests ohne Cleanup."""

    def __init__(self):
        super().__init__(allowlist=[])

    def should_skip(self, file_path: str) -> bool:
        return not is_test_file(file_path)

    def check(self, file_path: str, content: str) -> List[str]:
        warnings = []

        # Dateisystem-Schreiboperationen
        fs_write_patterns = [
            r"file_put_contents\s*\(",
            r"fwrite\s*\(",
            r"mkdir\s*\(",
            r"touch\s*\(",
            r"copy\s*\(",
            r"rename\s*\(",
        ]

        has_fs_write = any(
            re.search(pattern, content)
            for pattern in fs_write_patterns
        )

        if has_fs_write:
            # Prüfe ob Cleanup vorhanden
            has_cleanup = bool(re.search(
                r"unlink\s*\(|rmdir\s*\(|tearDown",
                content
            ))

            if not has_cleanup:
                warnings.append(
                    "W14.2: Test writes to filesystem without visible cleanup. "
                    "Ensure files are removed in tearDown() or after test."
                )

        return warnings


class W14_3_GlobalState(Rule):
    """W14.3: Modifikation von globalem Zustand in Tests."""

    def __init__(self):
        super().__init__(allowlist=[])

    def should_skip(self, file_path: str) -> bool:
        return not is_test_file(file_path)

    def check(self, file_path: str, content: str) -> List[str]:
        warnings = []

        global_state_patterns = [
            (r"ini_set\s*\(", "ini_set() modifies PHP configuration"),
            (r"putenv\s*\(", "putenv() modifies environment variables"),
            (r"define\s*\(", "define() creates global constants"),
            (r"date_default_timezone_set\s*\(", "Timezone setting is global"),
        ]

        for pattern, description in global_state_patterns:
            if re.search(pattern, content):
                warnings.append(
                    f"W14.3: {description}. "
                    "Ensure original value is restored in tearDown()."
                )

        return warnings


class W14_4_HardcodedPaths(Rule):
    """W14.4: Hardcoded absolute Pfade in Tests."""

    def __init__(self):
        super().__init__(allowlist=[])

    def should_skip(self, file_path: str) -> bool:
        return not is_test_file(file_path)

    def check(self, file_path: str, content: str) -> List[str]:
        warnings = []

        # Absolute Pfade die problematisch sein könnten
        hardcoded_paths = re.findall(
            r"['\"]/(var|tmp|home|Users)/[^'\"]+['\"]",
            content
        )

        # Erlaube /tmp für Temp-Dateien
        problematic = [p for p in hardcoded_paths if "/tmp/" not in p]

        if problematic:
            warnings.append(
                f"W14.4: Hardcoded absolute paths in test ({len(problematic)}x). "
                "Use sys_get_temp_dir() or __DIR__ for portable paths."
            )

        return warnings


class W14_5_SleepInTests(Rule):
    """W14.5: sleep() in Tests - verlangsamt Test-Suite."""

    def __init__(self):
        super().__init__(allowlist=[])

    def should_skip(self, file_path: str) -> bool:
        return not is_test_file(file_path)

    def check(self, file_path: str, content: str) -> List[str]:
        warnings = []

        sleep_calls = re.findall(r"\bsleep\s*\(\s*(\d+)\s*\)", content)
        usleep_calls = re.findall(r"\busleep\s*\(", content)

        total_sleep = sum(int(s) for s in sleep_calls)

        if sleep_calls or usleep_calls:
            warnings.append(
                f"W14.5: sleep()/usleep() in test ({len(sleep_calls) + len(usleep_calls)}x, "
                f"~{total_sleep}s total). Consider mocking time-dependent behavior."
            )

        return warnings


class W14_6_TestDependencies(Rule):
    """W14.6: Tests mit @depends ohne klare Isolation."""

    def __init__(self):
        super().__init__(allowlist=[])

    def should_skip(self, file_path: str) -> bool:
        return not is_test_file(file_path)

    def check(self, file_path: str, content: str) -> List[str]:
        warnings = []

        depends_annotations = re.findall(r"@depends\s+(\w+)", content)

        if len(depends_annotations) > 2:
            warnings.append(
                f"W14.6: Test has {len(depends_annotations)} @depends annotations. "
                "Complex test dependencies can cause cascading failures. Consider isolation."
            )

        return warnings


class W14_7_AssertionCount(Rule):
    """W14.7: Test-Methode ohne Assertions."""

    def __init__(self):
        super().__init__(allowlist=[])

    def should_skip(self, file_path: str) -> bool:
        return not is_test_file(file_path)

    def check(self, file_path: str, content: str) -> List[str]:
        warnings = []

        # Finde Test-Methoden
        test_methods = re.findall(
            r"public\s+function\s+(test\w+)\s*\([^)]*\)\s*(?::\s*void)?\s*\{([^}]+)\}",
            content,
            re.DOTALL
        )

        for method_name, method_body in test_methods:
            # Zähle Assertions
            assertions = len(re.findall(
                r"\$this->assert|\$this->expect|self::assert|self::expect",
                method_body
            ))

            if assertions == 0:
                warnings.append(
                    f"W14.7: Test method '{method_name}' has no assertions. "
                    "Tests should verify expected behavior."
                )

        return warnings


# =============================================================================
# RULE COLLECTION
# =============================================================================

RULES = [
    W14_1_MissingTearDown(),
    W14_2_FileSystemAccess(),
    W14_3_GlobalState(),
    W14_4_HardcodedPaths(),
    W14_5_SleepInTests(),
    W14_6_TestDependencies(),
    W14_7_AssertionCount(),
]
← Übersicht Graph