From d6831c5851121f3df2d679dccfc7b1c988354568 Mon Sep 17 00:00:00 2001 From: indifferentketchup Date: Fri, 1 May 2026 15:02:57 +0000 Subject: [PATCH] test: add Redactor integration coverage against existing PZ fixtures Co-Authored-By: Claude Opus 4.7 (1M context) --- .../ProjectZomboidRedactorIntegrationTest.php | 205 ++++++++++++++++++ 1 file changed, 205 insertions(+) create mode 100644 test/tests/Util/Redactor/ProjectZomboidRedactorIntegrationTest.php diff --git a/test/tests/Util/Redactor/ProjectZomboidRedactorIntegrationTest.php b/test/tests/Util/Redactor/ProjectZomboidRedactorIntegrationTest.php new file mode 100644 index 0000000..be54778 --- /dev/null +++ b/test/tests/Util/Redactor/ProjectZomboidRedactorIntegrationTest.php @@ -0,0 +1,205 @@ +` coords survive because COORDS_AT_CLAUSE_REGEX + * anchors on ` at `, not ` to `. + */ +class ProjectZomboidRedactorIntegrationTest extends TestCase +{ + private static string $fixturesDir = __DIR__ . '/../../../src/Games/ProjectZomboid/fixtures'; + + // --------------------------------------------------------------------------- + // Data providers + // --------------------------------------------------------------------------- + + /** + * Yields [fixturePath] for every PZ fixture file. + */ + public static function fixturePathProvider(): array + { + $dir = self::$fixturesDir; + return [ + 'admin' => [$dir . '/admin-minimal.txt'], + 'burd-journals' => [$dir . '/burd-journals-minimal.txt'], + 'chat' => [$dir . '/chat-minimal.txt'], + 'client-action' => [$dir . '/client-action-minimal.txt'], + 'cmd' => [$dir . '/cmd-minimal.txt'], + 'debug-server' => [$dir . '/debug-server-minimal.txt'], + 'item' => [$dir . '/item-minimal.txt'], + 'map' => [$dir . '/map-minimal.txt'], + 'perk' => [$dir . '/perk-minimal.txt'], + 'pvp' => [$dir . '/pvp-minimal.txt'], + 'user' => [$dir . '/user-minimal.txt'], + ]; + } + + /** + * Yields [fixturePath, logClass] for the fixtures whose log class parses + * them. All 11 fixtures are represented. + */ + public static function fixtureWithLogClassProvider(): array + { + $dir = self::$fixturesDir; + return [ + 'admin' => [$dir . '/admin-minimal.txt', ProjectZomboidAdminLog::class], + 'burd-journals' => [$dir . '/burd-journals-minimal.txt', ProjectZomboidBurdJournalsLog::class], + 'chat' => [$dir . '/chat-minimal.txt', ProjectZomboidChatLog::class], + 'client-action' => [$dir . '/client-action-minimal.txt', ProjectZomboidClientActionLog::class], + 'cmd' => [$dir . '/cmd-minimal.txt', ProjectZomboidCmdLog::class], + 'debug-server' => [$dir . '/debug-server-minimal.txt', ProjectZomboidServerLog::class], + 'item' => [$dir . '/item-minimal.txt', ProjectZomboidItemLog::class], + 'map' => [$dir . '/map-minimal.txt', ProjectZomboidMapLog::class], + 'perk' => [$dir . '/perk-minimal.txt', ProjectZomboidPerkLog::class], + 'pvp' => [$dir . '/pvp-minimal.txt', ProjectZomboidPvpLog::class], + 'user' => [$dir . '/user-minimal.txt', ProjectZomboidUserLog::class], + ]; + } + + // --------------------------------------------------------------------------- + // Helper + // --------------------------------------------------------------------------- + + private function redact(string $content): string + { + return (new ProjectZomboidRedactor())->redact($content); + } + + // --------------------------------------------------------------------------- + // Test 1 — Steam ID normalisation + // --------------------------------------------------------------------------- + + /** + * After redaction every 17-digit Steam ID that is NOT the zero-placeholder + * must be gone. The zero-placeholder itself (76561198000000000) is the only + * Steam ID that may remain. + */ + #[DataProvider('fixturePathProvider')] + public function testFixtureContainsNoSteamIdsAfterRedaction(string $fixturePath): void + { + $content = (new PathLogFile($fixturePath))->getContent(); + $redacted = $this->redact($content); + + $matches = preg_match_all('/(?assertSame( + 0, + $matches, + sprintf( + 'After redaction, fixture "%s" must contain no non-zero-placeholder Steam IDs, but %d were found.', + basename($fixturePath), + $matches, + ), + ); + } + + // --------------------------------------------------------------------------- + // Test 2 — Structural preservation (re-parse after redaction) + // --------------------------------------------------------------------------- + + /** + * The redacted content, fed back through the corresponding parser, must + * produce exactly the same number of log entries as the original content. + * + * This asserts that the redactor does not corrupt timestamps, delimiters, + * or structural tokens that the parser relies on. + * + * @param string $fixturePath Path to the fixture file. + * @param class-string<\IndifferentKetchup\Codex\Log\Log> $logClass + * Fully-qualified name of the Log subclass that corresponds to this fixture. + */ + #[DataProvider('fixtureWithLogClassProvider')] + public function testFixtureRedactedOutputParsesToSameEntryCount(string $fixturePath, string $logClass): void + { + $content = (new PathLogFile($fixturePath))->getContent(); + + /** @var \IndifferentKetchup\Codex\Log\Log $originalLog */ + $originalLog = (new $logClass())->setLogFile(new PathLogFile($fixturePath)); + $originalLog->parse(); + $originalCount = count($originalLog->getEntries()); + + $redacted = $this->redact($content); + + /** @var \IndifferentKetchup\Codex\Log\Log $redactedLog */ + $redactedLog = (new $logClass())->setLogFile(new StringLogFile($redacted)); + $redactedLog->parse(); + $redactedCount = count($redactedLog->getEntries()); + + $this->assertSame( + $originalCount, + $redactedCount, + sprintf( + 'Parsing the redacted "%s" fixture with %s must yield the same entry count (%d) as parsing the original, but got %d.', + basename($fixturePath), + $logClass, + $originalCount, + $redactedCount, + ), + ); + } + + // --------------------------------------------------------------------------- + // Test 3 — Idempotence + // --------------------------------------------------------------------------- + + /** + * Applying redact() a second time must produce no further changes: + * redact(redact(content)) === redact(content). + * + * This guards against poorly-anchored regexes that would re-match the + * redaction placeholders themselves on a second pass. + */ + #[DataProvider('fixturePathProvider')] + public function testFixtureIsIdempotent(string $fixturePath): void + { + $content = (new PathLogFile($fixturePath))->getContent(); + + $redactor = new ProjectZomboidRedactor(); + $once = $redactor->redact($content); + $twice = $redactor->redact($once); + + $this->assertSame( + $once, + $twice, + sprintf( + 'redact(redact(content)) must equal redact(content) for fixture "%s"; a second pass must be a no-op.', + basename($fixturePath), + ), + ); + } +}