diff --git a/src/Client/TrapHandle.php b/src/Client/TrapHandle.php index 4cfd3753..d3cb066f 100644 --- a/src/Client/TrapHandle.php +++ b/src/Client/TrapHandle.php @@ -45,6 +45,20 @@ public function if(bool|callable $condition): self return $this; } + /** + * Add stack trace to the dump. + */ + public function stackTrace(): self + { + $cwd = \getcwd(); + $this->values['Stack trace'] = [ + 'cwd' => $cwd, + 'trace' => new TraceStub($this->staticState->stackTrace), + ]; + + return $this; + } + /** * Set max depth for the dump. * @@ -61,7 +75,7 @@ public function depth(int $depth): self * The counter isn't incremented if the dump is not sent (any other condition is not met). * It might be useful for debugging in loops, recursive or just multiple function calls. * - * @param positive-int $times + * @param positive-int $times Zero means no limit. * @param bool $fullStack If true, the counter is incremented for each stack trace, not for the line. */ public function times(int $times, bool $fullStack = false): self @@ -102,16 +116,6 @@ private function sendDump(): void $_SERVER['VAR_DUMPER_SERVER'] = '127.0.0.1:9912'; } - // If there are no values - stack trace - if ($this->values === []) { - VarDumper::dump([ - 'cwd' => \getcwd(), - // todo StackTrace::stackTrace(\getcwd()) - add CWD - 'trace' => new TraceStub($this->staticState->stackTrace), - ], depth: $this->depth); - return; - } - // Dump single value if (\array_keys($this->values) === [0]) { VarDumper::dump($this->values[0], depth: $this->depth); @@ -140,11 +144,12 @@ private function __construct( private function haveToSend(): bool { - if (!$this->haveToSend) { + if (!$this->haveToSend || $this->values === []) { return false; } if ($this->times > 0) { + \assert($this->timesCounterKey !== ''); return Counter::checkAndIncrement($this->timesCounterKey, $this->times); } diff --git a/src/Client/TrapHandle/Counter.php b/src/Client/TrapHandle/Counter.php index 9c3f8644..bea25d63 100644 --- a/src/Client/TrapHandle/Counter.php +++ b/src/Client/TrapHandle/Counter.php @@ -4,14 +4,21 @@ namespace Buggregator\Trap\Client\TrapHandle; +/** + * Static counter for {@see TrapHandle::once()} and {@see TrapHandle::times()} methods. + * + * @internal + * @psalm-internal Buggregator\Trap\Client + */ final class Counter { - /** @var array> */ + /** @var array> */ private static array $counters = []; /** * Returns true if the counter of related stack trace is less than $times. In this case, the counter is incremented. * + * @param non-empty-string $key * @param int<0, max> $times */ public static function checkAndIncrement(string $key, int $times): bool @@ -25,4 +32,9 @@ public static function checkAndIncrement(string $key, int $times): bool return false; } + + public static function clear(): void + { + self::$counters = []; + } } diff --git a/src/Sender/Frontend/Http/StaticFiles.php b/src/Sender/Frontend/Http/StaticFiles.php index a5d96d50..4e7cb389 100644 --- a/src/Sender/Frontend/Http/StaticFiles.php +++ b/src/Sender/Frontend/Http/StaticFiles.php @@ -26,16 +26,19 @@ public function handle(ServerRequestInterface $request, callable $next): Respons } if (\preg_match('#^/((?:[a-zA-Z0-9\\-_]+/)*[a-zA-Z0-9.\\-\\[\\]() _]+?\\.([a-zA-Z0-4]++))$#', $path, $matches)) { - $file = \sprintf("%s/resources/frontend/%s", Info::TRAP_ROOT, $matches[1]); + $file = \sprintf('%s/resources/frontend/%s', Info::TRAP_ROOT, $matches[1]); - /** @var array $cache */ - static $cache = []; + /** @var array $cacheContent */ + static $cacheContent = []; + /** @var array $cacheHash */ + static $cacheHash = []; + /** @var array> $cacheSize */ + static $cacheSize = []; - if (!\array_key_exists($file, $cache) && !\is_file($file)) { + if (!\array_key_exists($file, $cacheContent) && !\is_file($file)) { return new Response(404); } - $content = null; $headers = []; $type = match($matches[2]) { @@ -54,11 +57,11 @@ public function handle(ServerRequestInterface $request, callable $next): Respons if ($path === '/index.html') { if (empty($this->earlyResponse)) { - $cache[$file] ??= \file_get_contents($file); + $cacheContent[$file] ??= \file_get_contents($file); // Find all CSS files \preg_match_all( '#\\bhref="([^"]+?\\.css)"#i', - $cache[$file], + $cacheContent[$file], $matches, ); $this->earlyResponse = \array_unique($matches[1]); @@ -74,18 +77,18 @@ public function handle(ServerRequestInterface $request, callable $next): Respons // (new \Buggregator\Trap\Support\Timer(2))->wait(); // to test early hints } - $cache[$file] ??= \file_get_contents($file); - + $cacheContent[$file] ??= \file_get_contents($file); return new Response( 200, [ 'Content-Type' => [$type], - 'Content-Length' => [\filesize($file)], + 'Content-Length' => [$cacheSize[$file] ??= \filesize($file)], 'Date' => [\gmdate('D, d M Y H:i:s T')], 'Cache-Control' => ['max-age=604801'], - 'ETag' => [\sha1($cache[$file])], + 'Accept-Ranges' => ['none'], + 'ETag' => [$cacheHash[$file] ??= \sha1($cacheContent[$file])], ] + $headers, - $cache[$file] ??= \file_get_contents($file), + $cacheContent[$file], ); } diff --git a/tests/Unit/Client/Base.php b/tests/Unit/Client/Base.php index a8ec2e0e..e3454e8c 100644 --- a/tests/Unit/Client/Base.php +++ b/tests/Unit/Client/Base.php @@ -4,27 +4,24 @@ namespace Buggregator\Trap\Tests\Unit\Client; +use Buggregator\Trap\Client\TrapHandle\Counter; use Buggregator\Trap\Client\TrapHandle\Dumper; use Closure; use PHPUnit\Framework\TestCase; -use Symfony\Component\VarDumper\Caster\ReflectionCaster; use Symfony\Component\VarDumper\Cloner\Data; -use Symfony\Component\VarDumper\Cloner\VarCloner; use Symfony\Component\VarDumper\Dumper\DataDumperInterface; class Base extends TestCase { - protected static Data $lastData; - protected static ?Closure $handler = null; + protected static ?Data $lastData = null; protected function setUp(): void { - $cloner = new VarCloner(); - /** @psalm-suppress InvalidArgument */ - $cloner->addCasters(ReflectionCaster::UNSET_CLOSURE_FILE_INFO); + self::$lastData = null; + Counter::clear(); $dumper = $this->getMockBuilder(DataDumperInterface::class) ->getMock(); - $dumper->expects($this->once()) + $dumper->expects($this->any()) ->method('dump') ->with( $this->callback(static function (Data $data): bool { @@ -41,6 +38,8 @@ protected function setUp(): void protected function tearDown(): void { + self::$lastData = null; + Counter::clear(); Dumper::setDumper(null); parent::tearDown(); } diff --git a/tests/Unit/Client/FunctionTrapTest.php b/tests/Unit/Client/FunctionTrapTest.php new file mode 100644 index 00000000..76fb2e20 --- /dev/null +++ b/tests/Unit/Client/FunctionTrapTest.php @@ -0,0 +1,24 @@ +assertNull($ref->get()); + } +} diff --git a/tests/Unit/Client/TrapTest.php b/tests/Unit/Client/TrapTest.php index 0fbdd1ab..05dc01d8 100644 --- a/tests/Unit/Client/TrapTest.php +++ b/tests/Unit/Client/TrapTest.php @@ -12,9 +12,12 @@ public function testLabel(): void $this->assertSame('FooName', static::$lastData->getContext()['label']); } + /** + * Check the first line of dumped stacktrace string contains right file and line. + */ public function testStackTrace(): void { - $line = __FILE__ . ':' . __LINE__ and trap(); + $line = __FILE__ . ':' . __LINE__ and trap()->stackTrace(); $this->assertArrayHasKey('trace', static::$lastData->getValue()); @@ -22,4 +25,57 @@ public function testStackTrace(): void $this->assertStringContainsString($line, $neededLine); } + + /** + * Nothing is dumped if no arguments are passed to {@see trap()}. + */ + public function testEmptyTrapCall(): void + { + trap(); + + self::assertNull(self::$lastData); + } + + /** + * After calling {@see trap()} the dumped data isn't stored in the memory. + */ + public function testLeak(): void + { + $object = new \stdClass(); + $ref = \WeakReference::create($object); + + \trap($object, $object); + unset($object); + + $this->assertNull($ref->get()); + } + + public function testTrapOnce(): void + { + foreach ([false, true, true, true, true] as $isNull) { + \trap(42)->once(); + self::assertSame($isNull, static::$lastData === null); + static::$lastData = null; + } + } + + public static function provideTrapTimes(): iterable + { + yield 'no limit' => [0, [false, false, false, false, false]]; + yield 'once' => [1, [false, true, true, true, true, true]]; + yield 'twice' => [2, [false, false, true, true, true]]; + yield 'x' => [10, [false, false, false, false, false, false, false, false, false, false, true, true, true]]; + } + + /** + * @dataProvider provideTrapTimes + */ + public function testTrapTimes(int $times, array $sequence): void + { + foreach ($sequence as $isNull) { + \trap(42)->times($times); + self::assertSame($isNull, static::$lastData === null); + static::$lastData = null; + } + } }