feat: deterministic PZ log parser module + unit tests

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-04 15:18:41 +00:00
parent 511583035b
commit 4fec3a58f6
21 changed files with 1217 additions and 0 deletions

View File

View File

@@ -0,0 +1,7 @@
[16-04-26 00:00:42.314] LOG : General f:0, t:1776297642254, st:48,648,157,434> server starting.
[16-04-26 00:04:00.000] ERROR: General f:0, t:1776297840000, st:48,648,355,178> Lua((MOD:Test Mod Alpha)) wrapper failure
java.lang.RuntimeException: outer wrapper at zombie.Foo(Foo.java:10)
Caused by: java.lang.IllegalStateException: middle layer
Caused by: java.lang.NullPointerException: deepest cause
at zombie.Bar(Bar.java:99)
[16-04-26 00:04:01.000] LOG : General f:0, t:1776297841000, st:48,648,356,178> after.

View File

@@ -0,0 +1,8 @@
[16-04-26 00:00:42.314] LOG : General f:0, t:1776297642254, st:48,648,157,434> server starting.
[16-04-26 00:01:00.000] ERROR: General f:0, t:1776297660000, st:48,648,175,178> Lua((MOD:Test Mod Alpha)) crash 1
at media/lua/client/A.lua:11
[16-04-26 00:01:01.000] ERROR: General f:0, t:1776297661000, st:48,648,176,178> Lua((MOD:Test Mod Alpha)) crash 1
at media/lua/client/A.lua:11
[16-04-26 00:01:02.000] ERROR: General f:0, t:1776297662000, st:48,648,177,178> Lua((MOD:Test Mod Alpha)) crash 1
at media/lua/client/A.lua:11
[16-04-26 00:01:03.000] LOG : General f:0, t:1776297663000, st:48,648,178,178> ok.

View File

View File

@@ -0,0 +1,4 @@
[16-04-26 00:00:42.314] LOG : General f:0, t:1776297642254, st:48,648,157,434> server starting.
[16-04-26 00:03:00.000] ERROR: General f:0, t:1776297780000, st:48,648,295,178> KahluaThread.flusherrormessage> dumping lua stack trace
at media/lua/client/Foo.lua:1
[16-04-26 00:03:01.000] LOG : General f:0, t:1776297781000, st:48,648,296,178> after.

View File

@@ -0,0 +1,10 @@
[16-04-26 00:00:42.314] LOG : General f:0, t:1776297642254, st:48,648,157,434> server starting.
[16-04-26 00:01:00.000] ERROR: General f:0, t:1776297660000, st:48,648,175,178> Lua((MOD:Test Mod A)) format1
at media/lua/client/F1.lua:11
[16-04-26 00:01:01.000] ERROR: General f:0, t:1776297661000, st:48,648,176,178> Lua((MOD:Test Mod B)) format2
function: doStuff -- file: media/lua/client/F2.lua line # 22
[16-04-26 00:01:02.000] ERROR: General f:0, t:1776297662000, st:48,648,177,178> Lua((MOD:Test Mod C)) format3
[string "media/lua/client/F3.lua"]:33: bang
[16-04-26 00:01:03.000] ERROR: General f:0, t:1776297663000, st:48,648,178,178> Lua((MOD:Test Mod D)) format4 about "media/lua/client/F4.lua" failure
[16-04-26 00:01:04.000] ERROR: General f:0, t:1776297664000, st:48,648,179,178> Lua((MOD:Test Mod E)) format5 path media/lua/client/F5.lua mention
[16-04-26 00:01:05.000] LOG : General f:0, t:1776297665000, st:48,648,180,178> ok.

View File

@@ -0,0 +1,7 @@
[16-04-26 00:00:42.314] LOG : General f:0, t:1776297642254, st:48,648,157,434> server starting.
[16-04-26 00:01:00.000] LOG : General f:0, t:1776297660000, st:48,648,175,178> Lua((MOD:Spongies Clothing)) initialised.
[16-04-26 00:01:01.000] LOG : General f:0, t:1776297661000, st:48,648,176,178> ordinary log line.
[16-04-26 00:01:02.000] LOG : General f:0, t:1776297662000, st:48,648,177,178> another log line.
[16-04-26 00:01:03.000] ERROR: General f:0, t:1776297663000, st:48,648,178,178> LuaManager.GetFunctionObject> no such function: doStuff
at media/lua/client/Spongie.lua:7
[16-04-26 00:01:04.000] LOG : General f:0, t:1776297664000, st:48,648,179,178> ok.

View File

@@ -0,0 +1,8 @@
[16-04-26 00:00:42.314] LOG : General f:0, t:1776297642254, st:48,648,157,434> server starting.
[16-04-26 00:01:19.080] ERROR: General f:0, t:1776297679080, st:48,648,194,258> DebugFileWatcher.registerDir> Exception thrown
java.nio.file.NoSuchFileException: /placeholder/config/mods at UnixException.translateToIOException(null:-1).
Stack trace:
at java.base/sun.nio.fs.UnixException.translateToIOException(Unknown Source)
at java.base/sun.nio.fs.UnixException.asIOException(Unknown Source)
at java.base/sun.nio.fs.LinuxWatchService$Poller.implRegister(Unknown Source)
[16-04-26 00:01:19.090] LOG : General f:0, t:1776297679090, st:48,648,194,268> after.

View File

@@ -0,0 +1,45 @@
[16-04-26 00:00:42.314] LOG : General f:0, t:1776297642254, st:48,648,157,434> server starting.
[16-04-26 00:01:00.000] LOG : General f:0, t:1776297660000, st:48,648,175,178> Lua((MOD:Test Mod Distant)) initialised.
[16-04-26 00:01:01.000] LOG : General f:0, t:1776297661000, st:48,648,176,178> filler 1.
[16-04-26 00:01:02.000] LOG : General f:0, t:1776297662000, st:48,648,177,178> filler 2.
[16-04-26 00:01:03.000] LOG : General f:0, t:1776297663000, st:48,648,178,178> filler 3.
[16-04-26 00:01:04.000] LOG : General f:0, t:1776297664000, st:48,648,179,178> filler 4.
[16-04-26 00:01:05.000] LOG : General f:0, t:1776297665000, st:48,648,180,178> filler 5.
[16-04-26 00:01:06.000] LOG : General f:0, t:1776297666000, st:48,648,181,178> filler 6.
[16-04-26 00:01:07.000] LOG : General f:0, t:1776297667000, st:48,648,182,178> filler 7.
[16-04-26 00:01:08.000] LOG : General f:0, t:1776297668000, st:48,648,183,178> filler 8.
[16-04-26 00:01:09.000] LOG : General f:0, t:1776297669000, st:48,648,184,178> filler 9.
[16-04-26 00:01:10.000] LOG : General f:0, t:1776297670000, st:48,648,185,178> filler 10.
[16-04-26 00:01:11.000] LOG : General f:0, t:1776297671000, st:48,648,186,178> filler 11.
[16-04-26 00:01:12.000] LOG : General f:0, t:1776297672000, st:48,648,187,178> filler 12.
[16-04-26 00:01:13.000] LOG : General f:0, t:1776297673000, st:48,648,188,178> filler 13.
[16-04-26 00:01:14.000] LOG : General f:0, t:1776297674000, st:48,648,189,178> filler 14.
[16-04-26 00:01:15.000] LOG : General f:0, t:1776297675000, st:48,648,190,178> filler 15.
[16-04-26 00:01:16.000] LOG : General f:0, t:1776297676000, st:48,648,191,178> filler 16.
[16-04-26 00:01:17.000] LOG : General f:0, t:1776297677000, st:48,648,192,178> filler 17.
[16-04-26 00:01:18.000] LOG : General f:0, t:1776297678000, st:48,648,193,178> filler 18.
[16-04-26 00:01:19.000] LOG : General f:0, t:1776297679000, st:48,648,194,178> filler 19.
[16-04-26 00:01:20.000] LOG : General f:0, t:1776297680000, st:48,648,195,178> filler 20.
[16-04-26 00:01:21.000] LOG : General f:0, t:1776297681000, st:48,648,196,178> filler 21.
[16-04-26 00:01:22.000] LOG : General f:0, t:1776297682000, st:48,648,197,178> filler 22.
[16-04-26 00:01:23.000] LOG : General f:0, t:1776297683000, st:48,648,198,178> filler 23.
[16-04-26 00:01:24.000] LOG : General f:0, t:1776297684000, st:48,648,199,178> filler 24.
[16-04-26 00:01:25.000] LOG : General f:0, t:1776297685000, st:48,648,200,178> filler 25.
[16-04-26 00:01:26.000] LOG : General f:0, t:1776297686000, st:48,648,201,178> filler 26.
[16-04-26 00:01:27.000] LOG : General f:0, t:1776297687000, st:48,648,202,178> filler 27.
[16-04-26 00:01:28.000] LOG : General f:0, t:1776297688000, st:48,648,203,178> filler 28.
[16-04-26 00:01:29.000] LOG : General f:0, t:1776297689000, st:48,648,204,178> filler 29.
[16-04-26 00:01:30.000] LOG : General f:0, t:1776297690000, st:48,648,205,178> filler 30.
[16-04-26 00:01:31.000] LOG : General f:0, t:1776297691000, st:48,648,206,178> filler 31.
[16-04-26 00:01:32.000] LOG : General f:0, t:1776297692000, st:48,648,207,178> filler 32.
[16-04-26 00:01:33.000] LOG : General f:0, t:1776297693000, st:48,648,208,178> filler 33.
[16-04-26 00:01:34.000] LOG : General f:0, t:1776297694000, st:48,648,209,178> filler 34.
[16-04-26 00:01:35.000] LOG : General f:0, t:1776297695000, st:48,648,210,178> filler 35.
[16-04-26 00:01:36.000] LOG : General f:0, t:1776297696000, st:48,648,211,178> filler 36.
[16-04-26 00:01:37.000] LOG : General f:0, t:1776297697000, st:48,648,212,178> filler 37.
[16-04-26 00:01:38.000] LOG : General f:0, t:1776297698000, st:48,648,213,178> filler 38.
[16-04-26 00:01:39.000] LOG : General f:0, t:1776297699000, st:48,648,214,178> filler 39.
[16-04-26 00:01:40.000] LOG : General f:0, t:1776297700000, st:48,648,215,178> filler 40.
[16-04-26 00:01:41.000] LOG : General f:0, t:1776297701000, st:48,648,216,178> filler 41.
[16-04-26 00:01:42.000] ERROR: General f:0, t:1776297702000, st:48,648,217,178> LuaManager.GetFunctionObject> no such function (way past lookback)
[16-04-26 00:01:43.000] LOG : General f:0, t:1776297703000, st:48,648,218,178> ok.

View File

@@ -0,0 +1,6 @@
[16-04-26 00:00:42.314] LOG : General f:0, t:1776297642254, st:48,648,157,434> server starting.
[16-04-26 00:01:19.131] LOG : Mod f:0, t:1776297679131, st:48,648,194,309> loading example_mod_alpha.
[16-04-26 00:05:00.000] ERROR: General f:0, t:1776297900000, st:48,648,415,178> Lua((MOD:Test Mod Alpha)) something broke
at media/lua/client/Foo.lua:42
function: doStuff -- file: media/lua/client/Foo.lua line # 42
[16-04-26 00:05:01.000] LOG : General f:0, t:1776297901000, st:48,648,416,178> after the error.

View File

@@ -0,0 +1,3 @@
[16-04-26 00:00:42.314] LOG : General f:0, t:1776297642254, st:48,648,157,434> server starting.
[16-04-26 00:01:00.000] LOG : General f:0, t:1776297660000, st:48,648,175,178> ordinary line.
[16-04-26 00:02:00.000] LOG : General f:0, t:1776297720000, st:48,648,235,178> nothing wrong.

View File

@@ -0,0 +1,5 @@
[16-04-26 00:00:42.314] LOG : General f:0, t:1776297642254, st:48,648,157,434> server starting.
[16-04-26 00:01:00.000] LOG : General f:0, t:1776297660000, st:48,648,175,178> Lua((MOD:Spongies Clothing)) initialised.
[16-04-26 00:01:01.000] LOG : General f:0, t:1776297661000, st:48,648,176,178> ordinary log line.
[16-04-26 00:01:03.000] ERROR: General f:0, t:1776297663000, st:48,648,178,178> Disk full while writing chunk data
[16-04-26 00:01:04.000] LOG : General f:0, t:1776297664000, st:48,648,179,178> ok.

View File

@@ -0,0 +1,6 @@
[16-04-26 00:00:42.314] LOG : General f:0, t:1776297642254, st:48,648,157,434> server starting.
[16-04-26 00:01:00.000] ERROR: General f:0, t:1776297660000, st:48,648,175,178> Lua((MOD:Test Mod Alpha)) crash now
at media/lua/client/X.lua:11
at media/lua/client/Y.lua:22
[string "media/lua/client/Z.lua"]:33: oops
[16-04-26 00:01:04.000] LOG : General f:0, t:1776297664000, st:48,648,179,178> ok.

View File

@@ -0,0 +1,6 @@
[16-04-26 00:00:42.314] LOG : General f:0, t:1776297642254, st:48,648,157,434> server starting.
[16-04-26 00:01:00.000] LOG : General f:0, t:1776297660000, st:48,648,175,178> at media/lua/client/A.lua:11
[16-04-26 00:01:01.000] LOG : General f:0, t:1776297661000, st:48,648,176,178> at media/lua/client/B.lua:22
[16-04-26 00:01:02.000] LOG : General f:0, t:1776297662000, st:48,648,177,178> [string "media/lua/client/C.lua"]:33: oops
[16-04-26 00:01:03.000] ERROR: General f:0, t:1776297663000, st:48,648,178,178> Lua((MOD:Test Mod Alpha)) crash
[16-04-26 00:01:04.000] LOG : General f:0, t:1776297664000, st:48,648,179,178> ok.

View File

@@ -0,0 +1,3 @@
[16-04-26 00:00:42.314] LOG : General f:0, t:1776297642254, st:48,648,157,434> server starting.
[16-04-26 00:01:00.000] ERROR: General f:0, t:1776297660000, st:48,648,175,178> require("DependencyMod/Foo") failed: needed by Test Mod Alpha
[16-04-26 00:01:01.000] LOG : General f:0, t:1776297661000, st:48,648,176,178> ok.

View File

@@ -0,0 +1,5 @@
[16-04-26 00:00:42.314] LOG : General f:0, t:1776297642254, st:48,648,157,434> server starting.
[16-04-26 00:01:00.000] ERROR: General f:0, t:1776297660000, st:48,648,175,178> ERROR: top-level error message
[16-04-26 00:01:01.000] WARN : General f:0, t:1776297661000, st:48,648,176,178> WARN: top-level warn message
[16-04-26 00:01:02.000] ERROR: General f:0, t:1776297662000, st:48,648,177,178> SEVERE: java-style severe message at zombie.Foo(Foo.java:5)
[16-04-26 00:01:03.000] LOG : General f:0, t:1776297663000, st:48,648,178,178> ok.

View File

@@ -0,0 +1,3 @@
[16-04-26 00:00:42.314] LOG : General f:0, t:1776297642254, st:48,648,157,434> server starting.
[16-04-26 00:02:00.000] WARN : General f:0, t:1776297720000, st:48,648,235,178> ZomboidFileSystem.loadModAndRequired> required mod "absent_mod" not found.
[16-04-26 00:02:01.000] LOG : General f:0, t:1776297721000, st:48,648,236,178> after.

View File

@@ -0,0 +1,95 @@
"""Tests for pz_parser phase 3 — mod attribution."""
from __future__ import annotations
import pathlib
import sys
import unittest
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1]))
import pz_parser # noqa: E402
FIXTURE_DIR = pathlib.Path(__file__).resolve().parent / "fixtures"
def fixture(name: str) -> pathlib.Path:
return FIXTURE_DIR / name
class AttributionBucketTests(unittest.TestCase):
"""Three confidence buckets: direct (high), inferred (medium),
unattributed (low)."""
def test_direct_attribution_when_lua_marker_on_entry(self) -> None:
entries = pz_parser.parse_file(fixture("fixture_lua_attributed.txt"))
records = pz_parser.classify_entries(entries, source_file="la.txt")
self.assertEqual(len(records), 1)
rec = records[0]
self.assertEqual(rec.attribution, "direct")
self.assertEqual(rec.confidence, "high")
# mod_id is normalised: lowercase, no spaces / apostrophes / hyphens.
self.assertEqual(rec.mod_id, "testmodalpha")
self.assertEqual(rec.mod_name, "Test Mod Alpha")
def test_inferred_attribution_within_lookback_window(self) -> None:
entries = pz_parser.parse_file(fixture("fixture_inferred.txt"))
records = pz_parser.classify_entries(entries, source_file="in.txt")
self.assertEqual(len(records), 1)
rec = records[0]
self.assertEqual(rec.attribution, "inferred")
self.assertEqual(rec.confidence, "medium")
self.assertEqual(rec.mod_id, "spongiesclothing")
def test_unattributed_when_no_marker_and_not_lua_shaped(self) -> None:
entries = pz_parser.parse_file(fixture("fixture_unattributed.txt"))
records = pz_parser.classify_entries(entries, source_file="ua.txt")
self.assertEqual(len(records), 1)
rec = records[0]
self.assertEqual(rec.attribution, "unattributed")
self.assertEqual(rec.confidence, "low")
self.assertEqual(rec.mod_id, "__unattributed__")
class LookbackBoundaryTests(unittest.TestCase):
"""Phase 3 — 40-line inferred-attribution window boundary."""
def test_lua_marker_beyond_lookback_does_not_attribute(self) -> None:
# Fixture places the Lua((MOD:...)) >40 lines before the ERROR.
entries = pz_parser.parse_file(fixture("fixture_lookback_boundary.txt"))
records = pz_parser.classify_entries(entries, source_file="lb.txt")
self.assertEqual(len(records), 1)
rec = records[0]
# The Lua-shaped ERROR is far enough back to be unattributed.
self.assertEqual(rec.attribution, "unattributed")
self.assertEqual(rec.mod_id, "__unattributed__")
def test_non_lua_shaped_body_rejects_inferred_attribution(self) -> None:
# Recent Lua((MOD:Spongies Clothing)) emitted, but the ERROR body
# ("Disk full while writing chunk data") isn't Lua-shaped.
entries = pz_parser.parse_file(fixture("fixture_non_lua_no_inferred.txt"))
records = pz_parser.classify_entries(entries, source_file="nl.txt")
self.assertEqual(len(records), 1)
rec = records[0]
self.assertEqual(rec.attribution, "unattributed")
class NeededByTests(unittest.TestCase):
"""Phase 3 — direct attribution via "needed by <mod>" hint."""
def test_needed_by_extracts_dependent_mod(self) -> None:
entries = pz_parser.parse_file(fixture("fixture_require_failed.txt"))
records = pz_parser.classify_entries(entries, source_file="rf.txt")
self.assertEqual(len(records), 1)
rec = records[0]
# "needed by Test Mod Alpha" should set the mod to Test Mod Alpha
# (preferred over the require("...") side which would mention
# DependencyMod). Either way we want direct/high.
self.assertEqual(rec.attribution, "direct")
self.assertEqual(rec.confidence, "high")
# The "needed by" branch is checked before the require() branch in
# the priority order; mod_id should reflect Test Mod Alpha.
self.assertEqual(rec.mod_id, "testmodalpha")
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,199 @@
"""Tests for pz_parser parsing pipeline (phases 1, 2, 4-7, 9)."""
from __future__ import annotations
import pathlib
import sys
import unittest
# Make the parser module importable when running via `python -m unittest
# discover -s tools/pz-analyzer/tests`.
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1]))
import pz_parser # noqa: E402
FIXTURE_DIR = pathlib.Path(__file__).resolve().parent / "fixtures"
def fixture(name: str) -> pathlib.Path:
return FIXTURE_DIR / name
class ParseFileTests(unittest.TestCase):
"""Phase 0 — basic line-shape recognition and continuation folding."""
def test_parse_file_groups_continuations_under_entry(self) -> None:
entries = pz_parser.parse_file(fixture("fixture_java_exception.txt"))
# 3 bracketed entries; the ERROR has 4 continuation lines.
self.assertEqual(len(entries), 3)
error_entry = entries[1]
self.assertEqual(error_entry.level, "ERROR")
self.assertGreater(len(error_entry.body), 1)
# First continuation should be the java exception line.
self.assertIn("NoSuchFileException", error_entry.body[1])
def test_parse_file_handles_empty_file(self) -> None:
self.assertEqual(pz_parser.parse_file(fixture("fixture_empty.txt")), [])
def test_parse_file_handles_no_errors(self) -> None:
entries = pz_parser.parse_file(fixture("fixture_no_errors.txt"))
self.assertEqual(len(entries), 3)
self.assertTrue(all(e.level == "LOG" for e in entries))
class SeverityRecognitionTests(unittest.TestCase):
"""Phase 1 — ERROR / WARN / SEVERE recognition."""
def test_classify_picks_up_error_warn_and_severe(self) -> None:
entries = pz_parser.parse_file(fixture("fixture_severity_variants.txt"))
records = pz_parser.classify_entries(entries, source_file="severity.txt")
levels = sorted({r.level for r in records})
# Spec accepts ERROR / WARN / SEVERE. The third entry has bracketed
# ERROR but body starts with SEVERE: ; effective_level should be SEVERE.
self.assertIn("ERROR", levels)
self.assertIn("WARN", levels)
self.assertIn("SEVERE", levels)
def test_log_lines_are_ignored(self) -> None:
entries = pz_parser.parse_file(fixture("fixture_no_errors.txt"))
records = pz_parser.classify_entries(entries, source_file="x.txt")
self.assertEqual(records, [])
class StackCollectionTests(unittest.TestCase):
"""Phase 2 — bidirectional stack collection."""
def test_pre_stack_walk_picks_up_preceding_lua_frames(self) -> None:
entries = pz_parser.parse_file(fixture("fixture_pre_stack.txt"))
# The ERROR entry is the 5th LOG-bracketed line; its predecessors are
# LOG-bracketed entries whose bodies are stack-shaped lines.
records = pz_parser.classify_entries(entries, source_file="pre.txt")
self.assertEqual(len(records), 1)
rec = records[0]
# Pre-stack walk should pick up at least the "at media/lua/.../A.lua:11" frame.
self.assertTrue(any("A.lua:11" in f for f in rec.stack))
def test_post_stack_collected_from_entry_body_continuations(self) -> None:
entries = pz_parser.parse_file(fixture("fixture_post_stack.txt"))
records = pz_parser.classify_entries(entries, source_file="post.txt")
self.assertEqual(len(records), 1)
rec = records[0]
self.assertTrue(any("X.lua:11" in f for f in rec.stack))
self.assertTrue(any("Y.lua:22" in f for f in rec.stack))
# Lua [string "..."]:N form preserves quoting in the captured frame.
self.assertTrue(any("Z.lua" in f and ":33" in f for f in rec.stack))
def test_stack_capped_at_eight_frames(self) -> None:
# Synthesise an ERROR with many continuation frames.
lines = ["[16-04-26 00:00:42.314] ERROR: General f:0, t:1, st:1,2,3,4> Lua((MOD:Test Mod Alpha)) crash"]
for i in range(20):
lines.append(f"\tat media/lua/client/F{i}.lua:{i + 1}")
path = FIXTURE_DIR / "_runtime_stack_cap.txt"
path.write_text("\n".join(lines) + "\n")
try:
entries = pz_parser.parse_file(path)
records = pz_parser.classify_entries(entries, source_file="cap.txt")
self.assertEqual(len(records), 1)
self.assertLessEqual(len(records[0].stack), pz_parser.MAX_STACK_FRAMES)
# And it should be exactly MAX_STACK_FRAMES given >MAX inputs.
self.assertEqual(len(records[0].stack), pz_parser.MAX_STACK_FRAMES)
finally:
path.unlink()
class FileLineExtractionTests(unittest.TestCase):
"""Phase 4 — five-fallback file:line extraction."""
def test_each_fallback_form_extracts_path(self) -> None:
entries = pz_parser.parse_file(fixture("fixture_file_line_fallbacks.txt"))
records = pz_parser.classify_entries(entries, source_file="ff.txt")
# 5 distinct ERRORs, distinct mods — should produce 5 records.
files = sorted(r.file for r in records)
self.assertEqual(
files,
sorted([
"media/lua/client/F1.lua",
"media/lua/client/F2.lua",
"media/lua/client/F3.lua",
"media/lua/client/F4.lua",
"media/lua/client/F5.lua",
]),
)
def test_quoted_path_without_line_number_yields_zero(self) -> None:
# Format 4 fixture line lacks a :NN suffix on the quoted path.
file_path, line_no = pz_parser.extract_file_line(
'failure about "media/lua/client/F4.lua" tail'
)
self.assertEqual(file_path, "media/lua/client/F4.lua")
self.assertEqual(line_no, 0)
class CauseChainTests(unittest.TestCase):
"""Phase 5 — Caused-by chain unwinding."""
def test_caused_by_chain_renders_with_arrow_separator(self) -> None:
entries = pz_parser.parse_file(fixture("fixture_cause_chain.txt"))
records = pz_parser.classify_entries(entries, source_file="cc.txt")
self.assertEqual(len(records), 1)
chain = records[0].cause_chain
self.assertIn("RuntimeException", chain)
self.assertIn("IllegalStateException", chain)
self.assertIn("NullPointerException", chain)
# Order preserved (outer -> inner).
idx_runtime = chain.index("RuntimeException")
idx_illegal = chain.index("IllegalStateException")
idx_null = chain.index("NullPointerException")
self.assertLess(idx_runtime, idx_illegal)
self.assertLess(idx_illegal, idx_null)
def test_no_cause_chain_when_no_exceptions(self) -> None:
entries = pz_parser.parse_file(fixture("fixture_unattributed.txt"))
records = pz_parser.classify_entries(entries, source_file="u.txt")
self.assertEqual(len(records), 1)
self.assertEqual(records[0].cause_chain, "")
class KindDetectionTests(unittest.TestCase):
"""Phases 6 & 7 — kind classification."""
def test_java_exception_kind_when_no_lua_marker(self) -> None:
entries = pz_parser.parse_file(fixture("fixture_java_exception.txt"))
records = pz_parser.classify_entries(entries, source_file="je.txt")
self.assertEqual(len(records), 1)
self.assertEqual(records[0].kind, "java_exception")
# Java engine errors should resolve to __unattributed__.
self.assertEqual(records[0].mod_id, "__unattributed__")
def test_engine_noise_kind_for_kahluathread(self) -> None:
entries = pz_parser.parse_file(fixture("fixture_engine_noise.txt"))
records = pz_parser.classify_entries(entries, source_file="en.txt")
self.assertEqual(len(records), 1)
self.assertEqual(records[0].kind, "engine_noise")
def test_lua_runtime_kind_for_attributed_lua_error(self) -> None:
entries = pz_parser.parse_file(fixture("fixture_lua_attributed.txt"))
records = pz_parser.classify_entries(entries, source_file="la.txt")
self.assertEqual(len(records), 1)
self.assertEqual(records[0].kind, "lua_runtime")
def test_require_failed_kind(self) -> None:
entries = pz_parser.parse_file(fixture("fixture_require_failed.txt"))
records = pz_parser.classify_entries(entries, source_file="rf.txt")
self.assertEqual(len(records), 1)
self.assertEqual(records[0].kind, "require_failed")
class AggregationTests(unittest.TestCase):
"""Phase 9 — dedup, occurrence_count, files-set growth."""
def test_three_identical_errors_dedup_to_one_record(self) -> None:
entries = pz_parser.parse_file(fixture("fixture_dedup.txt"))
records = pz_parser.classify_entries(entries, source_file="dd.txt")
self.assertEqual(len(records), 1)
self.assertEqual(records[0].occurrence_count, 3)
# files list shouldn't duplicate "dd.txt".
self.assertEqual(records[0].files, ["dd.txt"])
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,59 @@
"""Tests for pz_parser phase 8 — signature computation."""
from __future__ import annotations
import pathlib
import sys
import unittest
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1]))
import pz_parser # noqa: E402
class PatternIdStabilityTests(unittest.TestCase):
"""pattern_id should be invariant under formatting variations."""
def test_pattern_id_collapses_numeric_runs(self) -> None:
a = pz_parser.compute_pattern_id(
"ERROR",
"General f:0, t:1776297642, st:48,648,157,434> failed at offset 12345",
)
b = pz_parser.compute_pattern_id(
"ERROR",
"General f:0, t:9999999999, st:99,99,99,99> failed at offset 99999",
)
self.assertEqual(a, b)
def test_pattern_id_collapses_quoted_strings_and_whitespace(self) -> None:
a = pz_parser.compute_pattern_id(
"ERROR",
'no such function "doStuff" in module',
)
b = pz_parser.compute_pattern_id(
"ERROR",
'no such function "fooBarBaz" in module',
)
# Whitespace-collapse plus quoted-string-flatten => same pattern_id.
self.assertEqual(a, b)
def test_pattern_id_changes_with_level(self) -> None:
a = pz_parser.compute_pattern_id("ERROR", "exception thrown")
b = pz_parser.compute_pattern_id("WARN", "exception thrown")
self.assertNotEqual(a, b)
class SignatureUniquenessTests(unittest.TestCase):
"""signature should fan out across mods sharing a pattern_id."""
def test_signature_unique_per_mod_for_shared_pattern(self) -> None:
# Same first line, different mod_ids — different signatures, same pattern_id.
pat = pz_parser.compute_pattern_id("ERROR", "Lua((MOD:X)) crash")
sig_a = pz_parser.compute_signature(pat, "spongiesclothing")
sig_b = pz_parser.compute_signature(pat, "testmodalpha")
self.assertNotEqual(sig_a, sig_b)
# Both should share their pattern_id (consumer's pattern-fanout view).
self.assertEqual(pat[:7], "sha256:")
if __name__ == "__main__":
unittest.main()