""" Context Gatherer Unit Tests ============================ Phase 5.2.1: 日誌清洗模組測試 Gate 2 Checkpoint: 驗證 ERROR Only 過濾邏輯 - 確保餵給 Ollama 的是純淨的戰訊,不含雜訊 """ import pytest from src.services.context_gatherer import LogLevelFilter class TestLogLevelFilter: """LogLevelFilter 單元測試 - ERROR Only 原則驗證""" # ========================================================================= # 測試案例 1: 禁止的日誌等級 (必須過濾) # ========================================================================= @pytest.mark.parametrize("line", [ "[DEBUG] Starting application initialization", "[INFO] Server listening on port 8080", "[TRACE] Request ID: abc123 processing", "[VERBOSE] Memory allocation details", "DEBUG: Connection pool initialized", "INFO: Health check passed", "TRACE: Stack trace dump", 'level=DEBUG msg="Processing request"', 'level="INFO" service=api status=healthy', 'level=info component="scheduler"', ]) def test_forbidden_levels_are_filtered(self, line: str): """禁止等級 (DEBUG/INFO/TRACE/VERBOSE) 必須被過濾""" assert LogLevelFilter.is_allowed(line) is False, f"Should filter: {line}" # ========================================================================= # 測試案例 2: 允許的日誌等級 (必須保留) # ========================================================================= @pytest.mark.parametrize("line", [ "[ERROR] Database connection failed", "[FATAL] Out of memory, shutting down", "[CRITICAL] SSL certificate expired", "[WARN] High CPU usage detected (95%)", "[WARNING] Disk space low on /var/log", "ERROR: Unable to connect to Redis", "FATAL: Unrecoverable state", "CRITICAL: Data corruption detected", "WARN: Response time degraded", "WARNING: Connection pool exhausted", 'level=ERROR msg="Request failed"', 'level="CRITICAL" service=db error="timeout"', 'level=warning component="cache" status=degraded', ]) def test_allowed_levels_are_preserved(self, line: str): """允許等級 (ERROR/FATAL/CRITICAL/WARN/WARNING) 必須保留""" assert LogLevelFilter.is_allowed(line) is True, f"Should preserve: {line}" # ========================================================================= # 測試案例 3: Stacktrace 保留 # ========================================================================= @pytest.mark.parametrize("line", [ "Traceback (most recent call last):", ' File "/app/main.py", line 42, in handle_request', " at com.example.Service.process(Service.java:123)", " at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)", "panic: runtime error: index out of range", " 0: 0x7fff5fbff8c0 main.main+0x20", ]) def test_stacktrace_lines_are_preserved(self, line: str): """Stacktrace 行必須保留 (包括 Python/Java/Go)""" assert LogLevelFilter.is_allowed(line) is True, f"Should preserve stacktrace: {line}" # ========================================================================= # 測試案例 4: K8s 事件格式 # ========================================================================= @pytest.mark.parametrize("line", [ "Warning BackOff 2m30s kubelet Back-off restarting failed container", "Error Failed 5m kubelet Error: ImagePullBackOff", ]) def test_k8s_warning_error_events_preserved(self, line: str): """K8s Warning/Error 事件必須保留""" assert LogLevelFilter.is_allowed(line) is True, f"Should preserve K8s event: {line}" @pytest.mark.parametrize("line", [ "Normal Scheduled 10m default-scheduler Successfully assigned", "Normal Pulled 8m kubelet Container image pulled", ]) def test_k8s_normal_events_filtered(self, line: str): """K8s Normal 事件應該被過濾""" assert LogLevelFilter.is_allowed(line) is False, f"Should filter K8s Normal: {line}" # ========================================================================= # 測試案例 5: 空行與邊界情況 # ========================================================================= @pytest.mark.parametrize("line", [ "", " ", "\t\t", ]) def test_empty_lines_are_filtered(self, line: str): """空行必須被過濾""" assert LogLevelFilter.is_allowed(line) is False # ========================================================================= # 測試案例 6: 完整日誌過濾 (多行) # ========================================================================= def test_filter_logs_multiline(self): """測試多行日誌過濾 - ERROR Only 原則""" raw_logs = """ [INFO] Application started successfully [DEBUG] Loading configuration from /etc/app/config.yaml [INFO] Connected to database [ERROR] Failed to connect to Redis: Connection refused [INFO] Retrying connection... [ERROR] Redis connection failed after 3 retries Traceback (most recent call last): File "/app/redis_client.py", line 45, in connect raise ConnectionError("Unable to connect") [DEBUG] Cleanup initiated [WARN] Memory usage high: 85% [INFO] Health check passed [CRITICAL] Service degraded, entering maintenance mode """.strip() filtered = LogLevelFilter.filter_logs(raw_logs) _lines = [line for line in filtered.split("\n") if line.strip()] # 驗證: 只有 ERROR/WARN/CRITICAL 和 Stacktrace 被保留 assert "[INFO]" not in filtered, "INFO should be filtered" assert "[DEBUG]" not in filtered, "DEBUG should be filtered" assert "[ERROR] Failed to connect to Redis" in filtered assert "[ERROR] Redis connection failed" in filtered assert "Traceback (most recent call last):" in filtered assert "[WARN] Memory usage high" in filtered assert "[CRITICAL] Service degraded" in filtered # 計算過濾效果 stats = LogLevelFilter.get_filter_stats(raw_logs, filtered) assert stats["filtered_lines"] < stats["original_lines"] assert stats["removal_rate_percent"] > 0 def test_filter_stats_calculation(self): """測試過濾統計計算""" original = "[INFO] line1\n[ERROR] line2\n[DEBUG] line3" filtered = "[ERROR] line2" stats = LogLevelFilter.get_filter_stats(original, filtered) assert stats["original_lines"] == 3 assert stats["filtered_lines"] == 1 assert stats["removed_lines"] == 2 assert stats["removal_rate_percent"] == pytest.approx(66.7, rel=0.1) # ========================================================================= # 測試案例 7: 真實 K8s Pod 日誌模擬 # ========================================================================= def test_real_world_k8s_pod_logs(self): """模擬真實 K8s Pod 日誌 - 驗證雜訊過濾效果""" # 模擬 Harbor Core Pod 崩潰日誌 k8s_logs = """ 2024-03-21T10:15:23.456Z INFO [harbor.core] Starting Harbor Core v2.9.0 2024-03-21T10:15:24.789Z DEBUG [harbor.core.db] Initializing database connection pool 2024-03-21T10:15:25.123Z INFO [harbor.core.db] Connected to PostgreSQL 2024-03-21T10:15:26.456Z DEBUG [harbor.core.cache] Redis client initialized 2024-03-21T10:15:27.789Z INFO [harbor.core.api] HTTP server listening on :8080 2024-03-21T10:16:45.123Z ERROR [harbor.core.db] Connection lost to PostgreSQL 2024-03-21T10:16:45.456Z FATAL [harbor.core] Database connection unrecoverable Traceback (most recent call last): File "/harbor/core/db.py", line 234, in connect raise DatabaseConnectionError("Max retries exceeded") 2024-03-21T10:16:46.789Z INFO [harbor.core] Graceful shutdown initiated 2024-03-21T10:16:47.123Z DEBUG [harbor.core] Cleanup completed """.strip() filtered = LogLevelFilter.filter_logs(k8s_logs) stats = LogLevelFilter.get_filter_stats(k8s_logs, filtered) # 驗證: 只保留 ERROR, FATAL 和 Stacktrace assert "ERROR" in filtered assert "FATAL" in filtered assert "Traceback" in filtered assert "INFO" not in filtered.replace("Co", "") # 避免誤判 assert "DEBUG" not in filtered # 驗證: 過濾率應該很高 (約 60-70%) assert stats["removal_rate_percent"] > 50, f"Should filter >50%, got {stats['removal_rate_percent']}%" print("\n📊 K8s Log Filter Stats:") print(f" Original: {stats['original_lines']} lines") print(f" Filtered: {stats['filtered_lines']} lines") print(f" Removed: {stats['removed_lines']} lines ({stats['removal_rate_percent']}%)") print(f"\n✅ 純淨戰訊 (ERROR Only):\n{filtered}") # ============================================================================= # CLI 測試入口 # ============================================================================= if __name__ == "__main__": # 快速驗證測試 print("=" * 60) print("Phase 5.2.1 - Context Gatherer Unit Tests") print("Gate 2 Checkpoint: ERROR Only 過濾邏輯驗證") print("=" * 60) test = TestLogLevelFilter() # 執行關鍵測試 print("\n🔍 測試 1: 禁止等級過濾...") for line in [ "[DEBUG] test", "[INFO] test", "[TRACE] test", "level=DEBUG msg=test", "INFO: application started", ]: result = LogLevelFilter.is_allowed(line) status = "❌ 過濾" if not result else "⚠️ 錯誤保留" print(f" {status}: {line[:50]}") print("\n🔍 測試 2: 允許等級保留...") for line in [ "[ERROR] Database connection failed", "[FATAL] Out of memory", "[CRITICAL] SSL expired", "[WARN] High CPU", "[WARNING] Disk low", ]: result = LogLevelFilter.is_allowed(line) status = "✅ 保留" if result else "⚠️ 錯誤過濾" print(f" {status}: {line[:50]}") print("\n🔍 測試 3: 多行日誌過濾效果...") test.test_real_world_k8s_pod_logs() print("\n" + "=" * 60) print("✅ Gate 2 Checkpoint: ERROR Only 過濾邏輯驗證完成") print("=" * 60)