Files
wehub-resource-sync 4b6817381b
Benchmark image — build + push to ECR (any adapter) / build + push (push) Waiting to run
CI / quality (ubuntu-latest) (push) Waiting to run
CI / test (tools-runtime) (push) Waiting to run
CI / test (e2e-general) (push) Waiting to run
CI / test (cli-runtime) (push) Waiting to run
CI / test (e2e-provider-and-openclaw) (push) Waiting to run
CI / test (integrations-and-misc) (push) Waiting to run
CI / coverage-report (push) Blocked by required conditions
CI / test-kubernetes (push) Waiting to run
CI / should-run-thorough (push) Waiting to run
CI / test-thorough (cloudwatch-demo) (push) Blocked by required conditions
CI / test-thorough (flink-ecs) (push) Blocked by required conditions
CI / test-thorough (upstream-lambda) (push) Blocked by required conditions
CI / test-thorough (prefect-ecs-fargate) (push) Blocked by required conditions
CodeQL / Analyze (python) (push) Waiting to run
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Blocked by required conditions
Release / publish-release (push) Blocked by required conditions
Release / publish-main-release (push) Blocked by required conditions
Release / prepare (push) Waiting to run
Release / verify (push) Blocked by required conditions
Release / build-python-dist (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Blocked by required conditions
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Waiting to run
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Waiting to run
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Waiting to run
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:10:45 +08:00

354 lines
13 KiB
Python

"""Tests for log compaction utilities (deduplication + taxonomy)."""
from __future__ import annotations
from platform.common.log_compaction import (
_classify_error_type,
_extract_components,
_normalize_message,
build_error_taxonomy,
compact_logs,
deduplicate_logs,
)
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _make_log(message: str, level: str = "ERROR", ts: str = "") -> dict:
return {"message": message, "log_level": level, "timestamp": ts}
# ===========================================================================
# Phase 1: deduplicate_logs
# ===========================================================================
class TestDeduplicateLogs:
"""Phase 1 — deduplication + count grouping."""
def test_empty_input(self):
assert deduplicate_logs([]) == []
def test_single_log_unchanged(self):
logs = [_make_log("Something happened", "INFO", "2024-01-15T10:00:00Z")]
result = deduplicate_logs(logs)
assert len(result) == 1
assert result[0]["message"] == "Something happened"
assert result[0]["count"] == 1
assert result[0]["first_seen"] == "2024-01-15T10:00:00Z"
assert result[0]["last_seen"] == "2024-01-15T10:00:00Z"
def test_exact_duplicates_grouped(self):
logs = [
_make_log("Connection timeout to upstream service", "ERROR", "2024-01-15T10:00:01Z"),
_make_log("Connection timeout to upstream service", "ERROR", "2024-01-15T10:02:00Z"),
_make_log("Connection timeout to upstream service", "ERROR", "2024-01-15T10:04:58Z"),
]
result = deduplicate_logs(logs)
assert len(result) == 1
assert result[0]["count"] == 3
assert result[0]["first_seen"] == "2024-01-15T10:00:01Z"
assert result[0]["last_seen"] == "2024-01-15T10:04:58Z"
def test_near_duplicates_with_variable_tokens(self):
"""Messages differing only by UUID / timestamp / IP should be grouped."""
logs = [
_make_log(
"Request abc12345-1234-1234-1234-123456789abc failed after 30s", "ERROR", "t1"
),
_make_log(
"Request def12345-1234-1234-1234-123456789def failed after 45s", "ERROR", "t2"
),
]
result = deduplicate_logs(logs)
assert len(result) == 1
assert result[0]["count"] == 2
def test_different_levels_not_grouped(self):
"""Same message at different log levels should stay separate."""
logs = [
_make_log("Disk usage high", "WARN", "t1"),
_make_log("Disk usage high", "ERROR", "t2"),
]
result = deduplicate_logs(logs)
assert len(result) == 2
def test_different_messages_stay_separate(self):
logs = [
_make_log("Connection timeout", "ERROR", "t1"),
_make_log("Schema validation failed", "ERROR", "t2"),
_make_log("Out of memory", "ERROR", "t3"),
]
result = deduplicate_logs(logs)
assert len(result) == 3
def test_max_output_cap(self):
logs = [_make_log(f"Unique error {i}", "ERROR", f"t{i}") for i in range(100)]
result = deduplicate_logs(logs, max_output=10)
assert len(result) == 10
def test_burst_scenario_from_issue(self):
"""The exact scenario from issue #226: 48 identical timeouts + 2 distinct errors."""
logs = (
[
_make_log(
"Connection timeout to upstream service", "ERROR", f"2024-01-15T10:00:{i:02d}Z"
)
for i in range(48)
]
+ [
_make_log(
"Schema validation failed: missing field 'user_id'",
"ERROR",
"2024-01-15T10:01:00Z",
)
]
+ [
_make_log(
"NullPointerException in DataProcessor.transform",
"ERROR",
"2024-01-15T10:01:30Z",
)
]
)
assert len(logs) == 50
result = deduplicate_logs(logs, max_output=50)
# 48 timeouts → 1 group, plus 2 distinct errors = 3 entries
assert len(result) == 3
# The timeout group should have count=48
timeout_group = next(r for r in result if "timeout" in r["message"].lower())
assert timeout_group["count"] == 48
def test_preserves_log_level_case(self):
logs = [_make_log("Oops", "error")]
result = deduplicate_logs(logs)
assert result[0]["log_level"] == "ERROR"
def test_missing_timestamp_handled(self):
logs = [_make_log("No timestamp"), _make_log("No timestamp")]
result = deduplicate_logs(logs)
assert len(result) == 1
assert result[0]["count"] == 2
assert result[0]["first_seen"] == ""
def test_ip_address_normalization(self):
logs = [
_make_log("Connection refused to 10.0.0.1:5432", "ERROR", "t1"),
_make_log("Connection refused to 192.168.1.100:5432", "ERROR", "t2"),
]
result = deduplicate_logs(logs)
assert len(result) == 1
assert result[0]["count"] == 2
def test_preserves_source_metadata_for_downstream_evidence_mapping(self):
logs = [
{
"message": "FailedScheduling: 0/3 nodes are available",
"log_level": "WARN",
"timestamp": "2026-04-10T02:00:00Z",
"source_type": "k8s_events",
"namespace": "billing",
"cluster": "prod-1",
"service": "billing-worker",
},
{
"message": "FailedScheduling: 0/3 nodes are available",
"log_level": "WARN",
"timestamp": "2026-04-10T02:01:00Z",
"source_type": "k8s_events",
"namespace": "billing",
"cluster": "prod-1",
"service": "billing-worker",
},
]
result = deduplicate_logs(logs)
assert len(result) == 1
assert result[0]["count"] == 2
assert result[0]["source_type"] == "k8s_events"
assert result[0]["namespace"] == "billing"
assert result[0]["cluster"] == "prod-1"
assert result[0]["service"] == "billing-worker"
# ===========================================================================
# Phase 2: build_error_taxonomy
# ===========================================================================
class TestBuildErrorTaxonomy:
"""Phase 2 — structured error taxonomy."""
def test_empty_input(self):
result = build_error_taxonomy([])
assert result["total_logs_fetched"] == 0
assert result["distinct_error_types"] == 0
assert result["error_taxonomy"] == []
def test_single_error_type(self):
logs = [
_make_log("Connection timeout to upstream after 30s", ts="t1"),
_make_log("Connection timeout to upstream after 45s", ts="t2"),
_make_log("Connection timeout to upstream after 60s", ts="t3"),
]
result = build_error_taxonomy(logs)
assert result["total_logs_fetched"] == 3
assert result["distinct_error_types"] == 1
assert result["error_taxonomy"][0]["error_type"] == "ConnectionTimeout"
assert result["error_taxonomy"][0]["count"] == 3
def test_multiple_error_types(self):
logs = [
_make_log("Connection timeout to upstream"),
_make_log("Connection timeout to upstream"),
_make_log("Out of memory error on worker-3"),
_make_log("Schema validation failed: missing field 'id'"),
]
result = build_error_taxonomy(logs)
assert result["distinct_error_types"] == 3
# Sorted by count desc — timeout should be first
assert result["error_taxonomy"][0]["error_type"] == "ConnectionTimeout"
assert result["error_taxonomy"][0]["count"] == 2
def test_affected_components_extracted(self):
logs = [
_make_log("Timeout connecting to service=upstream-api host=db-primary"),
]
result = build_error_taxonomy(logs)
components = result["error_taxonomy"][0]["affected_components"]
assert "upstream-api" in components
assert "db-primary" in components
def test_sample_messages_deduped(self):
"""Near-identical samples should not duplicate."""
logs = [
_make_log("Timeout after 30s connecting to 10.0.0.1"),
_make_log("Timeout after 45s connecting to 10.0.0.2"),
_make_log("Timeout after 60s connecting to 10.0.0.3"),
]
result = build_error_taxonomy(logs, max_samples=5)
samples = result["error_taxonomy"][0]["sample_messages"]
# Near-identical after normalization → only 1 unique sample
assert len(samples) == 1
def test_sample_messages_cap(self):
logs = [_make_log(f"Timeout error in component-{i}") for i in range(20)]
result = build_error_taxonomy(logs, max_samples=3)
samples = result["error_taxonomy"][0]["sample_messages"]
assert len(samples) <= 3
def test_raw_samples_across_types(self):
logs = [
_make_log("Connection timeout"),
_make_log("Out of memory"),
_make_log("Schema validation failed"),
]
result = build_error_taxonomy(logs)
assert len(result["raw_samples"]) >= 3
def test_timestamp_range_tracked(self):
logs = [
_make_log("Timeout", ts="2024-01-15T10:00:01Z"),
_make_log("Timeout", ts="2024-01-15T10:04:58Z"),
]
result = build_error_taxonomy(logs)
entry = result["error_taxonomy"][0]
assert entry["first_seen"] == "2024-01-15T10:00:01Z"
assert entry["last_seen"] == "2024-01-15T10:04:58Z"
# ===========================================================================
# classify / extract helpers
# ===========================================================================
class TestClassifyErrorType:
def test_timeout(self):
assert _classify_error_type("Connection timed out after 30s") == "ConnectionTimeout"
def test_oom(self):
assert _classify_error_type("Container killed by OOM killer") == "OutOfMemory"
def test_auth(self):
assert _classify_error_type("Authentication failed for user admin") == "AuthenticationError"
def test_schema(self):
assert _classify_error_type("Schema validation error: missing field") == "SchemaValidation"
def test_unknown(self):
assert _classify_error_type("Something completely unexpected") == "Unknown"
def test_rate_limit(self):
assert _classify_error_type("429 Too Many Requests") == "RateLimited"
def test_not_found(self):
assert _classify_error_type("404 Not Found: /api/v2/resource") == "ResourceNotFound"
def test_import_error(self):
assert _classify_error_type("ImportError: No module named 'pandas'") == "ImportError"
class TestExtractComponents:
def test_key_value_pattern(self):
assert "my-svc" in _extract_components("error in service=my-svc")
def test_quoted_identifiers(self):
assert "upstream-api" in _extract_components("failed to reach 'upstream-api'")
def test_no_components(self):
assert _extract_components("simple error") == []
# ===========================================================================
# normalize helper
# ===========================================================================
class TestNormalizeMessage:
def test_uuid_replaced(self):
result = _normalize_message("Request abc12345-1234-1234-1234-123456789abc failed")
assert "abc12345" not in result
assert "<*>" in result
def test_ip_replaced(self):
result = _normalize_message("Connecting to 10.0.0.1:5432")
assert "10.0.0.1" not in result
def test_timestamp_replaced(self):
result = _normalize_message("Error at 2024-01-15T10:00:01Z in worker")
assert "2024-01-15" not in result
# ===========================================================================
# compact_logs (convenience wrapper)
# ===========================================================================
class TestCompactLogs:
def test_combines_both_phases(self):
logs = [
_make_log("Connection timeout", "ERROR", "t1"),
_make_log("Connection timeout", "ERROR", "t2"),
_make_log("Connection timeout", "ERROR", "t3"),
_make_log("Schema validation failed", "ERROR", "t4"),
_make_log("All good", "INFO", "t5"),
]
result = compact_logs(logs, max_output=50)
# Phase 1: deduplicated list
assert len(result["compacted_logs"]) == 3 # 3 timeout→1, 1 schema, 1 info
assert result["total_raw"] == 5
# Phase 2: taxonomy (errors only)
taxonomy = result["error_taxonomy"]
assert taxonomy["total_logs_fetched"] == 4 # 4 error logs
assert taxonomy["distinct_error_types"] == 2
def test_no_errors_produces_empty_taxonomy(self):
logs = [_make_log("All good", "INFO")]
result = compact_logs(logs)
assert result["error_taxonomy"]["distinct_error_types"] == 0
assert len(result["compacted_logs"]) == 1