capture(fn () => $this->throwWithSecret($secret)); $trace = SafeTrace::format($caught); self::assertStringNotContainsString($secret, $trace); self::assertStringContainsString('throwWithSecret', $trace); } public function testFormattedTraceWalksPreviousChain(): void { $innerSecret = 'inner-secret-123'; $outerSecret = 'outer-secret-456'; $caught = $this->capture(function () use ($innerSecret, $outerSecret): void { try { $this->throwWithSecret($innerSecret); } catch (Throwable $inner) { $this->wrap($outerSecret, $inner); } }); $trace = SafeTrace::format($caught); self::assertStringNotContainsString($innerSecret, $trace); self::assertStringNotContainsString($outerSecret, $trace); self::assertStringContainsString('Caused by', $trace); } public function testFrameLayoutResemblesPhpNativeFormat(): void { $caught = $this->capture(fn () => $this->throwWithSecret('x')); $trace = SafeTrace::format($caught); // Numbered frames, file(line) suffix, class::method() call, then {main}. self::assertMatchesRegularExpression('/^#0 .+\(\d+\): .+\(\)$/m', $trace); self::assertStringEndsWith('{main}', $trace); } private function capture(callable $fn): Throwable { try { $fn(); } catch (Throwable $e) { return $e; } self::fail('expected callable to throw'); } private function throwWithSecret(string $secret): never { throw new RuntimeException('boom'); } private function wrap(string $secret, Throwable $inner): never { throw new RuntimeException('outer', 0, $inner); } }