{
"event": "PreToolUse",
"tool_name": "Write",
"tool_input": {
"file_path": "\/var\/www\/tools\/ki-protokoll\/claude-hook\/quality\/rules_testisolation.py",
"content": "#!\/usr\/bin\/env python3\n\"\"\"\nPost-Hook Test Isolation Regeln (WARN) - Test Isolation.\n\nW14.x Regeln: Warnt bei potenziellen Test-Isolation-Problemen.\n\nPrinzip: \"Tests beeinflussen sich nicht gegenseitig. Globaler Zustand ist kontrolliert.\"\n\nWICHTIG: Diese Regeln gelten NUR für Test-Dateien (\/tests\/, \/Test\/).\n\"\"\"\n\nimport re\nfrom typing import List\nfrom .rule_base import Rule\n\n\n# =============================================================================\n# KONFIGURATION\n# =============================================================================\n\n# Pfade die als Test-Dateien gelten\nTEST_PATHS = [\"\/tests\/\", \"\/Test\/\"]\n\n\n# =============================================================================\n# HELPER\n# =============================================================================\n\ndef is_test_file(file_path: str) -> bool:\n \"\"\"Prüft ob die Datei eine Test-Datei ist.\"\"\"\n return any(test_path in file_path for test_path in TEST_PATHS)\n\n\n# =============================================================================\n# W14: TEST ISOLATION\n# =============================================================================\n\nclass W14_1_MissingTearDown(Rule):\n \"\"\"W14.1: Test-Klasse ohne tearDown-Methode bei Setup.\"\"\"\n\n def __init__(self):\n # Leere Allowlist - Tests werden explizit geprüft\n super().__init__(allowlist=[])\n\n def should_skip(self, file_path: str) -> bool:\n # Nur Test-Dateien prüfen\n return not is_test_file(file_path)\n\n def check(self, file_path: str, content: str) -> List[str]:\n warnings = []\n\n # Hat die Klasse setUp?\n has_setup = bool(re.search(\n r\"(?:protected|public)\\s+function\\s+setUp\\s*\\(\",\n content\n ))\n\n # Hat die Klasse tearDown?\n has_teardown = bool(re.search(\n r\"(?:protected|public)\\s+function\\s+tearDown\\s*\\(\",\n content\n ))\n\n if has_setup and not has_teardown:\n warnings.append(\n \"W14.1: Test has setUp() but no tearDown(). \"\n \"Consider adding tearDown() to clean up test state.\"\n )\n\n return warnings\n\n\nclass W14_2_FileSystemAccess(Rule):\n \"\"\"W14.2: Direkter Dateisystem-Zugriff in Tests ohne Cleanup.\"\"\"\n\n def __init__(self):\n super().__init__(allowlist=[])\n\n def should_skip(self, file_path: str) -> bool:\n return not is_test_file(file_path)\n\n def check(self, file_path: str, content: str) -> List[str]:\n warnings = []\n\n # Dateisystem-Schreiboperationen\n fs_write_patterns = [\n r\"file_put_contents\\s*\\(\",\n r\"fwrite\\s*\\(\",\n r\"mkdir\\s*\\(\",\n r\"touch\\s*\\(\",\n r\"copy\\s*\\(\",\n r\"rename\\s*\\(\",\n ]\n\n has_fs_write = any(\n re.search(pattern, content)\n for pattern in fs_write_patterns\n )\n\n if has_fs_write:\n # Prüfe ob Cleanup vorhanden\n has_cleanup = bool(re.search(\n r\"unlink\\s*\\(|rmdir\\s*\\(|tearDown\",\n content\n ))\n\n if not has_cleanup:\n warnings.append(\n \"W14.2: Test writes to filesystem without visible cleanup. \"\n \"Ensure files are removed in tearDown() or after test.\"\n )\n\n return warnings\n\n\nclass W14_3_GlobalState(Rule):\n \"\"\"W14.3: Modifikation von globalem Zustand in Tests.\"\"\"\n\n def __init__(self):\n super().__init__(allowlist=[])\n\n def should_skip(self, file_path: str) -> bool:\n return not is_test_file(file_path)\n\n def check(self, file_path: str, content: str) -> List[str]:\n warnings = []\n\n global_state_patterns = [\n (r\"ini_set\\s*\\(\", \"ini_set() modifies PHP configuration\"),\n (r\"putenv\\s*\\(\", \"putenv() modifies environment variables\"),\n (r\"define\\s*\\(\", \"define() creates global constants\"),\n (r\"date_default_timezone_set\\s*\\(\", \"Timezone setting is global\"),\n ]\n\n for pattern, description in global_state_patterns:\n if re.search(pattern, content):\n warnings.append(\n f\"W14.3: {description}. \"\n \"Ensure original value is restored in tearDown().\"\n )\n\n return warnings\n\n\nclass W14_4_HardcodedPaths(Rule):\n \"\"\"W14.4: Hardcoded absolute Pfade in Tests.\"\"\"\n\n def __init__(self):\n super().__init__(allowlist=[])\n\n def should_skip(self, file_path: str) -> bool:\n return not is_test_file(file_path)\n\n def check(self, file_path: str, content: str) -> List[str]:\n warnings = []\n\n # Absolute Pfade die problematisch sein könnten\n hardcoded_paths = re.findall(\n r\"['\\\"]\/(var|tmp|home|Users)\/[^'\\\"]+['\\\"]\",\n content\n )\n\n # Erlaube \/tmp für Temp-Dateien\n problematic = [p for p in hardcoded_paths if \"\/tmp\/\" not in p]\n\n if problematic:\n warnings.append(\n f\"W14.4: Hardcoded absolute paths in test ({len(problematic)}x). \"\n \"Use sys_get_temp_dir() or __DIR__ for portable paths.\"\n )\n\n return warnings\n\n\nclass W14_5_SleepInTests(Rule):\n \"\"\"W14.5: sleep() in Tests - verlangsamt Test-Suite.\"\"\"\n\n def __init__(self):\n super().__init__(allowlist=[])\n\n def should_skip(self, file_path: str) -> bool:\n return not is_test_file(file_path)\n\n def check(self, file_path: str, content: str) -> List[str]:\n warnings = []\n\n sleep_calls = re.findall(r\"\\bsleep\\s*\\(\\s*(\\d+)\\s*\\)\", content)\n usleep_calls = re.findall(r\"\\busleep\\s*\\(\", content)\n\n total_sleep = sum(int(s) for s in sleep_calls)\n\n if sleep_calls or usleep_calls:\n warnings.append(\n f\"W14.5: sleep()\/usleep() in test ({len(sleep_calls) + len(usleep_calls)}x, \"\n f\"~{total_sleep}s total). Consider mocking time-dependent behavior.\"\n )\n\n return warnings\n\n\nclass W14_6_TestDependencies(Rule):\n \"\"\"W14.6: Tests mit @depends ohne klare Isolation.\"\"\"\n\n def __init__(self):\n super().__init__(allowlist=[])\n\n def should_skip(self, file_path: str) -> bool:\n return not is_test_file(file_path)\n\n def check(self, file_path: str, content: str) -> List[str]:\n warnings = []\n\n depends_annotations = re.findall(r\"@depends\\s+(\\w+)\", content)\n\n if len(depends_annotations) > 2:\n warnings.append(\n f\"W14.6: Test has {len(depends_annotations)} @depends annotations. \"\n \"Complex test dependencies can cause cascading failures. Consider isolation.\"\n )\n\n return warnings\n\n\nclass W14_7_AssertionCount(Rule):\n \"\"\"W14.7: Test-Methode ohne Assertions.\"\"\"\n\n def __init__(self):\n super().__init__(allowlist=[])\n\n def should_skip(self, file_path: str) -> bool:\n return not is_test_file(file_path)\n\n def check(self, file_path: str, content: str) -> List[str]:\n warnings = []\n\n # Finde Test-Methoden\n test_methods = re.findall(\n r\"public\\s+function\\s+(test\\w+)\\s*\\([^)]*\\)\\s*(?::\\s*void)?\\s*\\{([^}]+)\\}\",\n content,\n re.DOTALL\n )\n\n for method_name, method_body in test_methods:\n # Zähle Assertions\n assertions = len(re.findall(\n r\"\\$this->assert|\\$this->expect|self::assert|self::expect\",\n method_body\n ))\n\n if assertions == 0:\n warnings.append(\n f\"W14.7: Test method '{method_name}' has no assertions. \"\n \"Tests should verify expected behavior.\"\n )\n\n return warnings\n\n\n# =============================================================================\n# RULE COLLECTION\n# =============================================================================\n\nRULES = [\n W14_1_MissingTearDown(),\n W14_2_FileSystemAccess(),\n W14_3_GlobalState(),\n W14_4_HardcodedPaths(),\n W14_5_SleepInTests(),\n W14_6_TestDependencies(),\n W14_7_AssertionCount(),\n]\n"
}
}
{
"tool_response": {
"type": "create",
"filePath": "\/var\/www\/tools\/ki-protokoll\/claude-hook\/quality\/rules_testisolation.py",
"content": "#!\/usr\/bin\/env python3\n\"\"\"\nPost-Hook Test Isolation Regeln (WARN) - Test Isolation.\n\nW14.x Regeln: Warnt bei potenziellen Test-Isolation-Problemen.\n\nPrinzip: \"Tests beeinflussen sich nicht gegenseitig. Globaler Zustand ist kontrolliert.\"\n\nWICHTIG: Diese Regeln gelten NUR für Test-Dateien (\/tests\/, \/Test\/).\n\"\"\"\n\nimport re\nfrom typing import List\nfrom .rule_base import Rule\n\n\n# =============================================================================\n# KONFIGURATION\n# =============================================================================\n\n# Pfade die als Test-Dateien gelten\nTEST_PATHS = [\"\/tests\/\", \"\/Test\/\"]\n\n\n# =============================================================================\n# HELPER\n# =============================================================================\n\ndef is_test_file(file_path: str) -> bool:\n \"\"\"Prüft ob die Datei eine Test-Datei ist.\"\"\"\n return any(test_path in file_path for test_path in TEST_PATHS)\n\n\n# =============================================================================\n# W14: TEST ISOLATION\n# =============================================================================\n\nclass W14_1_MissingTearDown(Rule):\n \"\"\"W14.1: Test-Klasse ohne tearDown-Methode bei Setup.\"\"\"\n\n def __init__(self):\n # Leere Allowlist - Tests werden explizit geprüft\n super().__init__(allowlist=[])\n\n def should_skip(self, file_path: str) -> bool:\n # Nur Test-Dateien prüfen\n return not is_test_file(file_path)\n\n def check(self, file_path: str, content: str) -> List[str]:\n warnings = []\n\n # Hat die Klasse setUp?\n has_setup = bool(re.search(\n r\"(?:protected|public)\\s+function\\s+setUp\\s*\\(\",\n content\n ))\n\n # Hat die Klasse tearDown?\n has_teardown = bool(re.search(\n r\"(?:protected|public)\\s+function\\s+tearDown\\s*\\(\",\n content\n ))\n\n if has_setup and not has_teardown:\n warnings.append(\n \"W14.1: Test has setUp() but no tearDown(). \"\n \"Consider adding tearDown() to clean up test state.\"\n )\n\n return warnings\n\n\nclass W14_2_FileSystemAccess(Rule):\n \"\"\"W14.2: Direkter Dateisystem-Zugriff in Tests ohne Cleanup.\"\"\"\n\n def __init__(self):\n super().__init__(allowlist=[])\n\n def should_skip(self, file_path: str) -> bool:\n return not is_test_file(file_path)\n\n def check(self, file_path: str, content: str) -> List[str]:\n warnings = []\n\n # Dateisystem-Schreiboperationen\n fs_write_patterns = [\n r\"file_put_contents\\s*\\(\",\n r\"fwrite\\s*\\(\",\n r\"mkdir\\s*\\(\",\n r\"touch\\s*\\(\",\n r\"copy\\s*\\(\",\n r\"rename\\s*\\(\",\n ]\n\n has_fs_write = any(\n re.search(pattern, content)\n for pattern in fs_write_patterns\n )\n\n if has_fs_write:\n # Prüfe ob Cleanup vorhanden\n has_cleanup = bool(re.search(\n r\"unlink\\s*\\(|rmdir\\s*\\(|tearDown\",\n content\n ))\n\n if not has_cleanup:\n warnings.append(\n \"W14.2: Test writes to filesystem without visible cleanup. \"\n \"Ensure files are removed in tearDown() or after test.\"\n )\n\n return warnings\n\n\nclass W14_3_GlobalState(Rule):\n \"\"\"W14.3: Modifikation von globalem Zustand in Tests.\"\"\"\n\n def __init__(self):\n super().__init__(allowlist=[])\n\n def should_skip(self, file_path: str) -> bool:\n return not is_test_file(file_path)\n\n def check(self, file_path: str, content: str) -> List[str]:\n warnings = []\n\n global_state_patterns = [\n (r\"ini_set\\s*\\(\", \"ini_set() modifies PHP configuration\"),\n (r\"putenv\\s*\\(\", \"putenv() modifies environment variables\"),\n (r\"define\\s*\\(\", \"define() creates global constants\"),\n (r\"date_default_timezone_set\\s*\\(\", \"Timezone setting is global\"),\n ]\n\n for pattern, description in global_state_patterns:\n if re.search(pattern, content):\n warnings.append(\n f\"W14.3: {description}. \"\n \"Ensure original value is restored in tearDown().\"\n )\n\n return warnings\n\n\nclass W14_4_HardcodedPaths(Rule):\n \"\"\"W14.4: Hardcoded absolute Pfade in Tests.\"\"\"\n\n def __init__(self):\n super().__init__(allowlist=[])\n\n def should_skip(self, file_path: str) -> bool:\n return not is_test_file(file_path)\n\n def check(self, file_path: str, content: str) -> List[str]:\n warnings = []\n\n # Absolute Pfade die problematisch sein könnten\n hardcoded_paths = re.findall(\n r\"['\\\"]\/(var|tmp|home|Users)\/[^'\\\"]+['\\\"]\",\n content\n )\n\n # Erlaube \/tmp für Temp-Dateien\n problematic = [p for p in hardcoded_paths if \"\/tmp\/\" not in p]\n\n if problematic:\n warnings.append(\n f\"W14.4: Hardcoded absolute paths in test ({len(problematic)}x). \"\n \"Use sys_get_temp_dir() or __DIR__ for portable paths.\"\n )\n\n return warnings\n\n\nclass W14_5_SleepInTests(Rule):\n \"\"\"W14.5: sleep() in Tests - verlangsamt Test-Suite.\"\"\"\n\n def __init__(self):\n super().__init__(allowlist=[])\n\n def should_skip(self, file_path: str) -> bool:\n return not is_test_file(file_path)\n\n def check(self, file_path: str, content: str) -> List[str]:\n warnings = []\n\n sleep_calls = re.findall(r\"\\bsleep\\s*\\(\\s*(\\d+)\\s*\\)\", content)\n usleep_calls = re.findall(r\"\\busleep\\s*\\(\", content)\n\n total_sleep = sum(int(s) for s in sleep_calls)\n\n if sleep_calls or usleep_calls:\n warnings.append(\n f\"W14.5: sleep()\/usleep() in test ({len(sleep_calls) + len(usleep_calls)}x, \"\n f\"~{total_sleep}s total). Consider mocking time-dependent behavior.\"\n )\n\n return warnings\n\n\nclass W14_6_TestDependencies(Rule):\n \"\"\"W14.6: Tests mit @depends ohne klare Isolation.\"\"\"\n\n def __init__(self):\n super().__init__(allowlist=[])\n\n def should_skip(self, file_path: str) -> bool:\n return not is_test_file(file_path)\n\n def check(self, file_path: str, content: str) -> List[str]:\n warnings = []\n\n depends_annotations = re.findall(r\"@depends\\s+(\\w+)\", content)\n\n if len(depends_annotations) > 2:\n warnings.append(\n f\"W14.6: Test has {len(depends_annotations)} @depends annotations. \"\n \"Complex test dependencies can cause cascading failures. Consider isolation.\"\n )\n\n return warnings\n\n\nclass W14_7_AssertionCount(Rule):\n \"\"\"W14.7: Test-Methode ohne Assertions.\"\"\"\n\n def __init__(self):\n super().__init__(allowlist=[])\n\n def should_skip(self, file_path: str) -> bool:\n return not is_test_file(file_path)\n\n def check(self, file_path: str, content: str) -> List[str]:\n warnings = []\n\n # Finde Test-Methoden\n test_methods = re.findall(\n r\"public\\s+function\\s+(test\\w+)\\s*\\([^)]*\\)\\s*(?::\\s*void)?\\s*\\{([^}]+)\\}\",\n content,\n re.DOTALL\n )\n\n for method_name, method_body in test_methods:\n # Zähle Assertions\n assertions = len(re.findall(\n r\"\\$this->assert|\\$this->expect|self::assert|self::expect\",\n method_body\n ))\n\n if assertions == 0:\n warnings.append(\n f\"W14.7: Test method '{method_name}' has no assertions. \"\n \"Tests should verify expected behavior.\"\n )\n\n return warnings\n\n\n# =============================================================================\n# RULE COLLECTION\n# =============================================================================\n\nRULES = [\n W14_1_MissingTearDown(),\n W14_2_FileSystemAccess(),\n W14_3_GlobalState(),\n W14_4_HardcodedPaths(),\n W14_5_SleepInTests(),\n W14_6_TestDependencies(),\n W14_7_AssertionCount(),\n]\n",
"structuredPatch": [],
"originalFile": null
}
}