rules_testisolation.py
- Pfad:
/var/www/tools/ki-protokoll/claude-hook/quality/rules_testisolation.py - Namespace: claude-hook.quality
- Zeilen: 272 | Größe: 8,145 Bytes
- Geändert: 2025-12-28 12:54:53 | Gescannt: 2025-12-31 10:22:15
Code Hygiene Score: 95
- Dependencies: 100 (25%)
- LOC: 76 (20%)
- Methods: 100 (20%)
- Secrets: 100 (15%)
- Classes: 100 (10%)
- Magic Numbers: 100 (10%)
Keine Issues gefunden.
Dependencies 4
- extends Rule
- use re
- use typing.List
- use rule_base.Rule
Klassen 7
-
W14_1_MissingTearDownclass Zeile 38 -
W14_2_FileSystemAccessclass Zeile 73 -
W14_3_GlobalStateclass Zeile 116 -
W14_4_HardcodedPathsclass Zeile 145 -
W14_5_SleepInTestsclass Zeile 175 -
W14_6_TestDependenciesclass Zeile 201 -
W14_7_AssertionCountclass Zeile 224
Funktionen 1
-
is_test_file()Zeile 29
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(),
]