diff --git a/src/Client/TrapHandle.php b/src/Client/TrapHandle.php index 4cfd3753..04737bc6 100644 --- a/src/Client/TrapHandle.php +++ b/src/Client/TrapHandle.php @@ -61,7 +61,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 @@ -145,6 +145,7 @@ private function haveToSend(): bool } 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/tests/Unit/Client/Base.php b/tests/Unit/Client/Base.php index a8ec2e0e..12b71dda 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 ?Data $lastData = null; protected static ?Closure $handler = null; protected function setUp(): void { - $cloner = new VarCloner(); - /** @psalm-suppress InvalidArgument */ - $cloner->addCasters(ReflectionCaster::UNSET_CLOSURE_FILE_INFO); + Counter::clear(); $dumper = $this->getMockBuilder(DataDumperInterface::class) ->getMock(); - $dumper->expects($this->once()) + $dumper->expects($this->atLeastOnce()) ->method('dump') ->with( $this->callback(static function (Data $data): bool { @@ -41,6 +38,7 @@ protected function setUp(): void protected function tearDown(): void { + 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..104433ee 100644 --- a/tests/Unit/Client/TrapTest.php +++ b/tests/Unit/Client/TrapTest.php @@ -12,6 +12,9 @@ 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(); @@ -22,4 +25,47 @@ public function testStackTrace(): void $this->assertStringContainsString($line, $neededLine); } + + /** + * 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; + } + } }