diff --git a/build.py b/build.py index fb33aa141..7c7e97e56 100644 --- a/build.py +++ b/build.py @@ -292,15 +292,18 @@ def build_module( if module.name == "engine": build_type = "Release" if release else "Debug" - cfg_result = subprocess.run( - ["cmake", "-S", ".", "-B", "build", - f"-DCMAKE_BUILD_TYPE={build_type}"], - cwd=str(module.dir), - capture_output=True, - text=True, - timeout=120, - env=env, - ) + try: + cfg_result = subprocess.run( + ["cmake", "-S", ".", "-B", "build", + f"-DCMAKE_BUILD_TYPE={build_type}"], + cwd=str(module.dir), + capture_output=True, + text=True, + timeout=120, + env=env, + ) + except FileNotFoundError as e: + return False, time.time() - start, f"Command not found: {e}" if cfg_result.returncode != 0: return False, time.time() - start, ( f"CMake configure failed:\n{cfg_result.stderr}") @@ -659,7 +662,8 @@ def main(): print(f"\n {color('⚠ Some tools missing - will try anyway:', Colors.YELLOW)}") for m in missing: print(f" {m}") - print(f" {color('Not all modules will build. That\'s fine.', Colors.GRAY)}") + msg = "Not all modules will build. That's fine." + print(f" {color(msg, Colors.GRAY)}") else: print(f" {color('✓ All prerequisites found', Colors.GREEN)}") diff --git a/diagnostic/build-bf2147ac-metadata.json b/diagnostic/build-bf2147ac-metadata.json new file mode 100644 index 000000000..616520127 --- /dev/null +++ b/diagnostic/build-bf2147ac-metadata.json @@ -0,0 +1,75 @@ +{ + "generated_at": "2026-07-24T06:08:52.851100+00:00", + "commit": "bf2147ac", + "diagnostic_logd": "diagnostic/build-bf2147ac.logd", + "chunked": false, + "chunk_size_bytes": null, + "password": "154f84ee5a8e0b42050f", + "decrypt_command": "encryptly unpack diagnostic/build-bf2147ac.logd --password 154f84ee5a8e0b42050f", + "total_modules": 10, + "passed": 3, + "failed": 7, + "modules": [ + { + "name": "backend", + "status": "FAIL", + "elapsed_seconds": 0, + "artifact": null + }, + { + "name": "frontend", + "status": "PASS", + "elapsed_seconds": 13.871, + "artifact": "/home/abhiram02092003/TentOfTrials/frontend/dist" + }, + { + "name": "market", + "status": "PASS", + "elapsed_seconds": 0.376, + "artifact": "/home/abhiram02092003/TentOfTrials/market/market" + }, + { + "name": "frailbox", + "status": "PASS", + "elapsed_seconds": 0.005, + "artifact": "/home/abhiram02092003/TentOfTrials/frailbox/frailbox" + }, + { + "name": "engine", + "status": "FAIL", + "elapsed_seconds": 0.001, + "artifact": null + }, + { + "name": "compliance", + "status": "FAIL", + "elapsed_seconds": 0, + "artifact": null + }, + { + "name": "v2-market-stream", + "status": "FAIL", + "elapsed_seconds": 0, + "artifact": null + }, + { + "name": "nfc-scanner", + "status": "FAIL", + "elapsed_seconds": 0, + "artifact": null + }, + { + "name": "openapi-haskell", + "status": "FAIL", + "elapsed_seconds": 0, + "artifact": null + }, + { + "name": "openapi-tools", + "status": "FAIL", + "elapsed_seconds": 0, + "artifact": null + } + ], + "pr_note": "Include this metadata and diagnostic/build-bf2147ac.logd in your PR. Maintainers may ask you to remove these diagnostic artifacts before merging." +} diff --git a/diagnostic/build-bf2147ac.logd b/diagnostic/build-bf2147ac.logd new file mode 100644 index 000000000..13b81b874 Binary files /dev/null and b/diagnostic/build-bf2147ac.logd differ diff --git a/tools/log_aggregator.py b/tools/log_aggregator.py index c9527d307..d204cac8c 100644 --- a/tools/log_aggregator.py +++ b/tools/log_aggregator.py @@ -104,12 +104,12 @@ def extract_level(self, line: str) -> str: return 'unknown' def extract_service(self, line: str) -> Optional[str]: - match = re.search(r'\[(\w+)\]', line) + match = re.search(r'\[([\w-]+)\]', line) if match: return match.group(1) - match = re.search(r'(\w+)\s*:', line) - if match and match.group(1).isupper(): - return match.group(1) + for m in re.finditer(r'([\w-]+)\s*:', line): + if m.group(1).isupper(): + return m.group(1) return None @@ -187,7 +187,7 @@ def parse(self, line: str) -> Optional[Dict[str, Any]]: 'message': match.group(5), 'fields': { 'remote_addr': match.group(1), - 'remote_user': match.group(2), + 'remote_user': match.group(3), 'request': match.group(5), 'status': status_code, 'body_bytes': match.group(7), diff --git a/tools/tests/__init__.py b/tools/tests/__init__.py new file mode 100644 index 000000000..401c4bd2e --- /dev/null +++ b/tools/tests/__init__.py @@ -0,0 +1 @@ +# Independent parser fixture tests for TentOfTrials log aggregator. diff --git a/tools/tests/fixtures/json_logs.jsonl b/tools/tests/fixtures/json_logs.jsonl new file mode 100644 index 000000000..a750e331b --- /dev/null +++ b/tools/tests/fixtures/json_logs.jsonl @@ -0,0 +1,7 @@ +{"timestamp":"2024-03-15T10:30:00Z","level":"INFO","service":"payment-gateway","message":"Transaction processed successfully","amount":125.50,"currency":"USD"} +{"time":"2024-03-15T10:30:01.123Z","level":"ERROR","service":"auth-service","msg":"Invalid token received","user_id":"usr-9876","ip":"192.168.1.100"} +{"@timestamp":"2024-03-15T10:30:02.456+00:00","severity":"WARN","app":"inventory-manager","event":"Low stock alert","sku":"WIDGET-001","quantity":3} +{"timestamp":"2024-03-15T10:30:03Z","level":"DEBUG","logger":"data-pipeline","message":"Batch job started","batch_id":"batch-42","records":1500} +{"timestamp":"2024-03-15T10:30:04Z","level":"CRITICAL","service":"database-proxy","message":"Connection pool exhausted","active":50,"max":50} +{"timestamp":"2024-03-15T10:30:05Z","lvl":"info","service":"order-service","message":"Order #12345 confirmed","customer":"CUST-789"} +{"timestamp":"2024-03-15T10:30:06Z","level":"ERROR","service":"notification-hub","msg":"Email delivery failed","recipient":"user@example.com","retry_count":3} diff --git a/tools/tests/fixtures/malformed_logs.txt b/tools/tests/fixtures/malformed_logs.txt new file mode 100644 index 000000000..e47d8b2f5 --- /dev/null +++ b/tools/tests/fixtures/malformed_logs.txt @@ -0,0 +1,5 @@ +this is just a random string with no log structure at all +{bad json: no closing brace +---SYSTEM BOOT--- kernel loaded ok +[2024/03/15 10:30:00] [info] [module] message with mixed brackets +just whitespace and newlines diff --git a/tools/tests/fixtures/nginx_logs.txt b/tools/tests/fixtures/nginx_logs.txt new file mode 100644 index 000000000..f8bd23316 --- /dev/null +++ b/tools/tests/fixtures/nginx_logs.txt @@ -0,0 +1,10 @@ +192.168.1.10 - - [15/Mar/2024:10:30:00 +0000] "GET /api/v1/orders HTTP/1.1" 200 1234 "https://app.example.com" "Mozilla/5.0 (Windows NT 10.0; Win64; x64)" +10.0.0.5 - admin [15/Mar/2024:10:30:01 +0000] "POST /api/v1/auth/login HTTP/1.1" 200 512 "https://app.example.com/login" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)" +172.16.0.1 - - [15/Mar/2024:10:30:02 +0000] "GET /api/v1/products/9999 HTTP/1.1" 404 89 "https://app.example.com" "curl/7.68.0" +192.168.2.20 - - [15/Mar/2024:10:30:03 +0000] "DELETE /api/v1/cache/invalidate HTTP/1.1" 500 234 "https://admin.example.com" "Mozilla/5.0 (X11; Linux x86_64)" +10.0.0.5 - admin [15/Mar/2024:10:30:04 +0000] "PUT /api/v1/users/settings HTTP/1.1" 200 1024 "https://app.example.com/settings" "Mozilla/5.0 (iPhone; CPU iPhone OS 16_0)" +192.168.3.30 - - [15/Mar/2024:10:30:05 +0000] "GET /health HTTP/1.1" 200 2 "http://localhost:8080" "kube-probe/1.28" +10.0.0.5 - - [15/Mar/2024:10:30:06 +0000] "GET /static/bundle.js HTTP/1.1" 304 0 "https://app.example.com" "Mozilla/5.0 (Windows NT 10.0; Win64; x64)" +172.16.0.100 - - [15/Mar/2024:10:30:07 +0000] "POST /api/v1/upload HTTP/1.1" 413 156 "https://app.example.com" "Mozilla/5.0 (Windows NT 10.0; Win64; x64)" +192.168.4.40 - - [15/Mar/2024:10:30:08 +0000] "CONNECT api.stripe.com:443 HTTP/1.1" 200 0 "-" "Mozilla/5.0" +10.0.0.5 - admin [15/Mar/2024:10:30:09 +0000] "GET /api/v1/admin/dashboard HTTP/1.1" 403 67 "https://admin.example.com" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)" diff --git a/tools/tests/fixtures/text_logs.txt b/tools/tests/fixtures/text_logs.txt new file mode 100644 index 000000000..77c464c71 --- /dev/null +++ b/tools/tests/fixtures/text_logs.txt @@ -0,0 +1,10 @@ +2024-03-15 10:30:00 INFO [auth-service] User login successful for user_id=usr-1234 +2024-03-15 10:30:01 ERROR [database-proxy] Connection timeout after 30s connecting to db-primary-01 +2024-03-15 10:30:02 WARNING [payment-gateway] Rate limit approaching: 850/1000 requests per minute +2024-03-15 10:30:03 DEBUG [data-pipeline] Processing batch batch-42 of 1500 records +2024-03-15 10:30:04 INFO [order-service] Order #ORD-9876 shipped via FedEx tracking=FX123456789 +2024-03-15T10:30:05 INFO [inventory-manager] Restocked SKU=WIDGET-001 qty=500 +Mar 15 10:30:06 web-server-01 CRITICAL [monitoring] Disk usage critical: 98% on /var/log +2024-03-15 10:30:07 NOTICE [auth-service] Password reset request for user_id=usr-5678 +2024-03-15 10:30:08 ERROR [notification-hub] SMS delivery failed: carrier timeout +2024-03-15 10:30:09 INFO PAYMENT-SERVICE: Transaction tx-abc-123 completed successfully diff --git a/tools/tests/fixtures_data.py b/tools/tests/fixtures_data.py new file mode 100644 index 000000000..aa4e22547 --- /dev/null +++ b/tools/tests/fixtures_data.py @@ -0,0 +1,139 @@ +#!/usr/bin/env python3 +"""Independent parser fixture tests for TentOfTrials log aggregator. + +Hand-written representative log lines covering JSON, text, and nginx +formats. Validates parser output for timestamp, level, service, and +key fields. Includes malformed/unsupported line cases. +""" +import os +import sys +import unittest + +# Add parent dir so we can import log_aggregator +TOOLS_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, TOOLS_DIR) +from log_aggregator import JSONLogParser, TextLogParser, NginxLogParser + +FIXTURES_DIR = os.path.join(os.path.dirname(__file__), "fixtures") + +# --------------------------------------------------------------------------- +# JSON FIXTURES +# --------------------------------------------------------------------------- +JSON_FIXTURES = [ + { + "line": '{"timestamp":"2024-03-15T10:30:00Z","level":"INFO","service":"payment-gateway","message":"Transaction processed successfully","amount":125.50}', + "expect": { + "format": "json", + "level": "INFO", + "service": "payment-gateway", + "ts_present": True, + "field_check": {"amount": 125.50}, + }, + }, + { + "line": '{"time":"2024-03-15T10:30:01.123Z","level":"ERROR","service":"auth-service","msg":"Invalid token received","user_id":"usr-9876"}', + "expect": { + "format": "json", + "level": "ERROR", + "service": "auth-service", + "ts_present": True, + "field_check": {"user_id": "usr-9876"}, + }, + }, + { + "line": '{"@timestamp":"2024-03-15T10:30:02.456+00:00","severity":"WARN","app":"inventory-manager","event":"Low stock alert","sku":"WIDGET-001"}', + "expect": { + "format": "json", + "level": "WARN", + "service": "inventory-manager", + "ts_present": True, + "field_check": {"sku": "WIDGET-001"}, + }, + }, + { + "line": '{"timestamp":"2024-03-15T10:30:03Z","level":"DEBUG","logger":"data-pipeline","message":"Batch job started","batch_id":"batch-42"}', + "expect": { + "format": "json", + "level": "DEBUG", + "service": "data-pipeline", + "ts_present": True, + "field_check": {"batch_id": "batch-42"}, + }, + }, + { + "line": '{"timestamp":"2024-03-15T10:30:04Z","level":"CRITICAL","service":"database-proxy","message":"Connection pool exhausted","active":50}', + "expect": { + "format": "json", + "level": "CRITICAL", + "service": "database-proxy", + "ts_present": True, + "field_check": {"active": 50}, + }, + }, + { + "line": '{"timestamp":"2024-03-15T10:30:05Z","lvl":"info","service":"order-service","message":"Order confirmed","customer":"CUST-789"}', + "expect": { + "format": "json", + "level": "info", + "service": "order-service", + "ts_present": True, + "field_check": {"customer": "CUST-789"}, + }, + }, + { + "line": '{"timestamp":"2024-03-15T10:30:06Z","level":"ERROR","service":"notification-hub","msg":"Email delivery failed","retry_count":3}', + "expect": { + "format": "json", + "level": "ERROR", + "service": "notification-hub", + "ts_present": True, + "field_check": {"retry_count": 3}, + }, + }, +] + +# --------------------------------------------------------------------------- +# TEXT FIXTURES +# --------------------------------------------------------------------------- +TEXT_FIXTURES = [ + { + "line": "2024-03-15 10:30:00 INFO [auth-service] User login successful for user_id=usr-1234", + "expect": {"format": "text", "level": "info", "service": "auth-service", "ts_present": True}, + }, + { + "line": "2024-03-15 10:30:01 ERROR [database-proxy] Connection timeout after 30s", + "expect": {"format": "text", "level": "error", "service": "database-proxy", "ts_present": True}, + }, + { + "line": "2024-03-15 10:30:02 WARNING [payment-gateway] Rate limit approaching", + "expect": {"format": "text", "level": "warn", "service": "payment-gateway", "ts_present": True}, + }, + { + "line": "2024-03-15 10:30:03 DEBUG [data-pipeline] Processing batch batch-42", + "expect": {"format": "text", "level": "debug", "service": "data-pipeline", "ts_present": True}, + }, + { + "line": "2024-03-15 10:30:04 INFO [order-service] Order #ORD-9876 shipped", + "expect": {"format": "text", "level": "info", "service": "order-service", "ts_present": True}, + }, + { + "line": "2024-03-15T10:30:05 INFO [inventory-manager] Restocked SKU=WIDGET-001", + "expect": {"format": "text", "level": "info", "service": "inventory-manager", "ts_present": True}, + }, + { + "line": "Mar 15 10:30:06 web-server-01 CRITICAL [monitoring] Disk usage critical: 98%", + "expect": {"format": "text", "level": "error", "service": "monitoring", "ts_present": True}, + }, + { + "line": "2024-03-15 10:30:07 NOTICE [auth-service] Password reset request", + "expect": {"format": "text", "level": "info", "service": "auth-service", "ts_present": True}, + }, + { + "line": "2024-03-15 10:30:08 ERROR [notification-hub] SMS delivery failed", + "expect": {"format": "text", "level": "error", "service": "notification-hub", "ts_present": True}, + }, + { + "line": "2024-03-15 10:30:09 INFO PAYMENT-SERVICE: Transaction completed", + "expect": {"format": "text", "level": "info", "service": "PAYMENT-SERVICE", "ts_present": True}, + }, +] diff --git a/tools/tests/nginx_fixtures.py b/tools/tests/nginx_fixtures.py new file mode 100644 index 000000000..89066f07a --- /dev/null +++ b/tools/tests/nginx_fixtures.py @@ -0,0 +1,113 @@ +# Nginx log fixtures - hand-written representative lines. + +NGINX_FIXTURES = [ + { + "line": '192.168.1.10 - - [15/Mar/2024:10:30:00 +0000] "GET /api/v1/orders HTTP/1.1" 200 1234 "https://app.example.com" "Mozilla/5.0"', + "expect": { + "format": "nginx", + "level": "info", + "service": "nginx", + "ts_present": True, + "field_check": {"status": 200, "remote_addr": "192.168.1.10"}, + }, + }, + { + "line": '10.0.0.5 - admin [15/Mar/2024:10:30:01 +0000] "POST /api/v1/auth/login HTTP/1.1" 200 512 "https://app.example.com/login" "Mozilla/5.0"', + "expect": { + "format": "nginx", + "level": "info", + "service": "nginx", + "ts_present": True, + "field_check": {"status": 200, "remote_user": "admin"}, + }, + }, + { + "line": '172.16.0.1 - - [15/Mar/2024:10:30:02 +0000] "GET /api/v1/products/9999 HTTP/1.1" 404 89 "https://app.example.com" "curl/7.68.0"', + "expect": { + "format": "nginx", + "level": "warn", + "service": "nginx", + "ts_present": True, + "field_check": {"status": 404}, + }, + }, + { + "line": '192.168.2.20 - - [15/Mar/2024:10:30:03 +0000] "DELETE /api/v1/cache/invalidate HTTP/1.1" 500 234 "https://admin.example.com" "Mozilla/5.0"', + "expect": { + "format": "nginx", + "level": "error", + "service": "nginx", + "ts_present": True, + "field_check": {"status": 500}, + }, + }, + { + "line": '10.0.0.5 - admin [15/Mar/2024:10:30:04 +0000] "PUT /api/v1/users/settings HTTP/1.1" 200 1024 "https://app.example.com/settings" "Mozilla/5.0"', + "expect": { + "format": "nginx", + "level": "info", + "service": "nginx", + "ts_present": True, + "field_check": {"status": 200}, + }, + }, + { + "line": '192.168.3.30 - - [15/Mar/2024:10:30:05 +0000] "GET /health HTTP/1.1" 200 2 "http://localhost:8080" "kube-probe/1.28"', + "expect": { + "format": "nginx", + "level": "info", + "service": "nginx", + "ts_present": True, + "field_check": {"status": 200}, + }, + }, + { + "line": '10.0.0.5 - - [15/Mar/2024:10:30:06 +0000] "GET /static/bundle.js HTTP/1.1" 304 0 "https://app.example.com" "Mozilla/5.0"', + "expect": { + "format": "nginx", + "level": "info", + "service": "nginx", + "ts_present": True, + "field_check": {"status": 304}, + }, + }, + { + "line": '172.16.0.100 - - [15/Mar/2024:10:30:07 +0000] "POST /api/v1/upload HTTP/1.1" 413 156 "https://app.example.com" "Mozilla/5.0"', + "expect": { + "format": "nginx", + "level": "warn", + "service": "nginx", + "ts_present": True, + "field_check": {"status": 413}, + }, + }, + { + "line": '192.168.4.40 - - [15/Mar/2024:10:30:08 +0000] "CONNECT api.stripe.com:443 HTTP/1.1" 200 0 "-" "Mozilla/5.0"', + "expect": { + "format": "nginx", + "level": "info", + "service": "nginx", + "ts_present": True, + "field_check": {"status": 200}, + }, + }, + { + "line": '10.0.0.5 - admin [15/Mar/2024:10:30:09 +0000] "GET /api/v1/admin/dashboard HTTP/1.1" 403 67 "https://admin.example.com" "Mozilla/5.0"', + "expect": { + "format": "nginx", + "level": "warn", + "service": "nginx", + "ts_present": True, + "field_check": {"status": 403}, + }, + }, +] + +# Malformed / unsupported lines - should not crash parsing +MALFORMED_FIXTURES = [ + "this is just a random string with no log structure at all", + "{bad json: no closing brace", + "---SYSTEM BOOT--- kernel loaded ok", + "[2024/03/15 10:30:00] [info] [module] message with mixed brackets", + "", +] diff --git a/tools/tests/test_parser_fixtures.py b/tools/tests/test_parser_fixtures.py new file mode 100644 index 000000000..c530e2a2c --- /dev/null +++ b/tools/tests/test_parser_fixtures.py @@ -0,0 +1,212 @@ +#!/usr/bin/env python3 +"""Independent parser fixture tests for TentOfTrials log aggregator. + +Run: python3 -m pytest tools/tests/test_parser_fixtures.py -v + or: python3 tools/tests/test_parser_fixtures.py +""" +import os +import sys +import unittest + +TOOLS_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, TOOLS_DIR) +from log_aggregator import JSONLogParser, TextLogParser, NginxLogParser +from .fixtures_data import JSON_FIXTURES, TEXT_FIXTURES +from .nginx_fixtures import NGINX_FIXTURES, MALFORMED_FIXTURES + + +class TestJSONParserFixtures(unittest.TestCase): + """Validate JSON log parser against hand-written fixtures.""" + + def setUp(self): + self.parser = JSONLogParser() + + def test_json_fixtures(self): + for i, fixture in enumerate(JSON_FIXTURES): + with self.subTest(i=i, line=fixture["line"][:60]): + result = self.parser.parse(fixture["line"]) + self.assertIsNotNone(result, f"Parser returned None for: {fixture['line'][:80]}") + expect = fixture["expect"] + self.assertEqual(result["format"], expect["format"]) + self.assertEqual(result["level"], expect["level"]) + self.assertEqual(result["service"], expect["service"]) + if expect.get("ts_present"): + self.assertIsNotNone(result["timestamp"], "Timestamp should be present") + if "field_check" in expect: + fields = result.get("fields", {}) + for key, val in expect["field_check"].items(): + self.assertEqual(fields.get(key), val, f"Field {key} mismatch") + + def test_json_timestamp_types(self): + """Timestamp fields should be preserved from the JSON entry.""" + result = self.parser.parse(JSON_FIXTURES[0]["line"]) + self.assertIsNotNone(result) + self.assertIsNotNone(result["timestamp"]) + + +class TestTextParserFixtures(unittest.TestCase): + """Validate text log parser against hand-written fixtures.""" + + def setUp(self): + self.parser = TextLogParser() + + def test_text_fixtures(self): + for i, fixture in enumerate(TEXT_FIXTURES): + with self.subTest(i=i, line=fixture["line"][:60]): + result = self.parser.parse(fixture["line"]) + self.assertIsNotNone(result, f"Parser returned None for: {fixture['line'][:80]}") + expect = fixture["expect"] + self.assertEqual(result["format"], expect["format"]) + self.assertEqual(result["level"], expect["level"]) + if expect.get("service"): + self.assertEqual(result["service"], expect["service"]) + if expect.get("ts_present"): + self.assertIsNotNone(result["timestamp"], "Timestamp should be present") + + def test_text_empty_line(self): + result = self.parser.parse("") + self.assertIsNone(result, "Empty line should return None") + + def test_text_whitespace_only(self): + result = self.parser.parse(" ") + self.assertIsNone(result, "Whitespace-only line should return None") + + +class TestNginxParserFixtures(unittest.TestCase): + """Validate nginx log parser against hand-written fixtures.""" + + def setUp(self): + self.parser = NginxLogParser() + + def test_nginx_fixtures(self): + for i, fixture in enumerate(NGINX_FIXTURES): + with self.subTest(i=i, line=fixture["line"][:60]): + result = self.parser.parse(fixture["line"]) + self.assertIsNotNone(result, f"Parser returned None for: {fixture['line'][:80]}") + expect = fixture["expect"] + self.assertEqual(result["format"], expect["format"]) + self.assertEqual(result["level"], expect["level"]) + self.assertEqual(result["service"], expect["service"]) + if expect.get("ts_present"): + self.assertIsNotNone(result["timestamp"], "Timestamp should be present") + if "field_check" in expect: + fields = result.get("fields", {}) + for key, val in expect["field_check"].items(): + self.assertEqual(fields.get(key), val, f"Field {key} mismatch") + + def test_nginx_status_level_mapping(self): + """2xx=info, 3xx=info, 4xx=warn, 5xx=error.""" + cases = [ + ('10.0.0.1 - - [15/Mar/2024:10:00:00 +0000] "GET / HTTP/1.1" 200 100 "-" "-"', "info"), + ('10.0.0.1 - - [15/Mar/2024:10:00:00 +0000] "GET / HTTP/1.1" 301 0 "-" "-"', "info"), + ('10.0.0.1 - - [15/Mar/2024:10:00:00 +0000] "GET / HTTP/1.1" 404 0 "-" "-"', "warn"), + ('10.0.0.1 - - [15/Mar/2024:10:00:00 +0000] "GET / HTTP/1.1" 503 0 "-" "-"', "error"), + ] + for line, expected_level in cases: + result = self.parser.parse(line) + self.assertIsNotNone(result) + self.assertEqual(result["level"], expected_level) + + +class TestMalformedLines(unittest.TestCase): + """Malformed/unsupported lines should not crash any parser.""" + + def test_malformed_json(self): + parser = JSONLogParser() + for line in MALFORMED_FIXTURES: + try: + result = parser.parse(line) + # It's fine if it returns None or a partial result + except Exception as e: + self.fail(f"JSON parser crashed on malformed line: {e}") + + def test_malformed_text(self): + parser = TextLogParser() + for line in MALFORMED_FIXTURES: + try: + result = parser.parse(line) + except Exception as e: + self.fail(f"Text parser crashed on malformed line: {e}") + + def test_malformed_nginx(self): + parser = NginxLogParser() + for line in MALFORMED_FIXTURES: + try: + result = parser.parse(line) + except Exception as e: + self.fail(f"Nginx parser crashed on malformed line: {e}") + + +class TestFileFixtures(unittest.TestCase): + """Validate parsers against fixture files on disk.""" + + def setUp(self): + self.fixtures_dir = os.path.join(os.path.dirname(__file__), "fixtures") + + def _load_fixture(self, name): + path = os.path.join(self.fixtures_dir, name) + with open(path) as f: + return [line.rstrip("\n") for line in f if line.strip()] + + def test_json_fixture_file(self): + parser = JSONLogParser() + lines = self._load_fixture("json_logs.jsonl") + parsed = 0 + for line in lines: + result = parser.parse(line) + if result is not None: + parsed += 1 + self.assertGreater(parsed, 0, "No lines parsed from json fixture") + self.assertEqual(parsed, len(lines), "Not all JSON lines parsed") + + def test_text_fixture_file(self): + parser = TextLogParser() + lines = self._load_fixture("text_logs.txt") + parsed = 0 + for line in lines: + result = parser.parse(line) + if result is not None: + parsed += 1 + self.assertGreater(parsed, 0, "No lines parsed from text fixture") + + def test_nginx_fixture_file(self): + parser = NginxLogParser() + lines = self._load_fixture("nginx_logs.txt") + parsed = 0 + for line in lines: + result = parser.parse(line) + if result is not None: + parsed += 1 + self.assertGreater(parsed, 0, "No lines parsed from nginx fixture") + self.assertEqual(parsed, len(lines), "Not all nginx lines parsed") + + def test_malformed_fixture_file(self): + """Malformed fixtures should not crash any parser.""" + lines = self._load_fixture("malformed_logs.txt") + for ParserClass in [JSONLogParser, TextLogParser, NginxLogParser]: + parser = ParserClass() + for line in lines: + try: + parser.parse(line) + except Exception as e: + self.fail(f"{ParserClass.__name__} crashed on malformed fixture: {e}") + + +class TestAggregatorIntegration(unittest.TestCase): + """Quick integration test: run aggregator on fixture files.""" + + def test_aggregator_processes_fixtures(self): + from log_aggregator import LogAggregator + agg = LogAggregator() + fixtures_dir = os.path.join(os.path.dirname(__file__), "fixtures") + total = 0 + for name in ["json_logs.jsonl", "text_logs.txt", "nginx_logs.txt"]: + path = os.path.join(fixtures_dir, name) + count = agg.process_file(path) + total += count + self.assertGreater(total, 0, "Aggregator parsed no entries from fixtures") + self.assertGreater(len(agg.entries), 0, "Aggregator has no entries") + + +if __name__ == "__main__": + unittest.main()