diff --git a/.github/.release-please-config.json b/.github/.release-please-config.json index afcbefa3..e7ce35ac 100644 --- a/.github/.release-please-config.json +++ b/.github/.release-please-config.json @@ -1,4 +1,5 @@ { + "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", "release-type": "php", "packages": { ".": { @@ -7,6 +8,8 @@ } }, "include-component-in-tag": false, + "include-v-in-tag": false, + "changelog-type": "github", "changelog-sections": [ { "type": "feat", diff --git a/.github/phar/keys.asc.gpg b/.github/phar/keys.asc.gpg index fde9a21b..dbe42e96 100644 Binary files a/.github/phar/keys.asc.gpg and b/.github/phar/keys.asc.gpg differ diff --git a/.github/workflows/build-phar-release.yml b/.github/workflows/build-phar-release.yml index bc483416..93cd5873 100644 --- a/.github/workflows/build-phar-release.yml +++ b/.github/workflows/build-phar-release.yml @@ -91,37 +91,13 @@ jobs: run: rm ${{ env.GPG_KEYS }} - name: 📤 Upload release assets - uses: actions/github-script@v7.0.1 + uses: softprops/action-gh-release@v2.0.5 + if: startsWith(github.ref, 'refs/tags/') with: - github-token: "${{ secrets.GITHUB_TOKEN }}" - script: | - const fs = require("fs"); - - const files = [ - { - name: "trap.phar", - path: process.env.TRAP_PHAR, - }, - { - name: "trap.phar.asc", - path: process.env.TRAP_PHAR_SIGNATURE, - }, - ]; - - for (const file of files) { - try { - await github.rest.repos.uploadReleaseAsset({ - data: fs.readFileSync(file.path), - name: file.name, - origin: process.env.RELEASE_UPLOAD_URL, - owner: context.repo.owner, - release_id: process.env.RELEASE_ID, - repo: context.repo.repo, - }); - } catch (error) { - core.setFailed(error.message); - } - } + token: "${{ secrets.TRAP_RELEASE_TOKEN }}" + files: | + ${{ env.TRAP_PHAR }} + ${{ env.TRAP_PHAR_SIGNATURE }} notify-discord: runs-on: ubuntu-latest diff --git a/CHANGELOG.md b/CHANGELOG.md index d4ad27e3..c7a3c3dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,32 @@ # Changelog +## 1.8.0 (2024-05-29) + +## What's Changed +* Add tr() and td() functions by @roxblnfk in https://github.com/buggregator/trap/pull/102 +* Add `trap()->code()` sugar by @lee-to in https://github.com/buggregator/trap/pull/103 +* Change release generation type to Github API by @lotyp in https://github.com/buggregator/trap/pull/106 + +## New Contributors +* @lee-to made their first contribution in https://github.com/buggregator/trap/pull/103 + +**Full Changelog**: https://github.com/buggregator/trap/compare/v1.7.5...v1.8.0 + +## [1.7.5](https://github.com/buggregator/trap/compare/v1.7.4...v1.7.5) (2024-05-24) + + +### Bug Fixes + +* fix environments in build-phar-release ([453e522](https://github.com/buggregator/trap/commit/453e522b6c49c6e9d54cd6256137c8f25925939f)) + +## [1.7.4](https://github.com/buggregator/trap/compare/v1.7.3...v1.7.4) (2024-05-24) + + +### Bug Fixes + +* release asset upload ([#99](https://github.com/buggregator/trap/issues/99)) ([b4616c5](https://github.com/buggregator/trap/commit/b4616c52056cd1803b2d3990178577537a694147)) +* suppress warnings if Closure::bind() is run with static callable ([3d72a7e](https://github.com/buggregator/trap/commit/3d72a7ef551bd2f21b0935826e8093a58da0b774)) + ## [1.7.3](https://github.com/buggregator/trap/compare/v1.7.2...v1.7.3) (2024-05-24) diff --git a/README.md b/README.md index b04f4254..1f293b09 100644 --- a/README.md +++ b/README.md @@ -131,10 +131,13 @@ Then just call the `trap()` function in your code: ```php // dump the current stack trace trap()->stackTrace(); + // dump a variable with a depth limit trap($var)->depth(4); + // dump a named variables sequence trap($var, foo: $far, bar: $bar); + // dump a variable and return it $responder->respond(trap($response)->return()); ``` @@ -143,6 +146,27 @@ $responder->respond(trap($response)->return()); > The `trap()` function configures `$_SERVER['REMOTE_ADDR']` and `$_SERVER['REMOTE_PORT']` automatically, > if they are not set. +Also, there are a couple of shortcuts here: + +- `tr(...)` - equivalent to `trap(...)->return()` +- `td(...)` - equivalent to `trap(...); die;` + +If called without arguments, they will display a short stack trace, used memory, and time between shortcut calls. + +```php +function handle($input) { + tr(); // Trace #0 -.--- 3.42M + + $data = $this->prepareData($input); + + tr(); // Trace #1 0.015ms 6.58M + + $this->processor->process(tr(data: $data)); + + td(); // exit with output: Trace #2 1.15ms 7.73M +} +``` + ### Default port Trap automatically recognizes the type of traffic. diff --git a/composer.json b/composer.json index 6e55ed2d..6e3dd385 100644 --- a/composer.json +++ b/composer.json @@ -108,20 +108,35 @@ "sort-packages": true }, "scripts": { - "cs:diff": "php vendor/bin/php-cs-fixer fix --dry-run -v --diff", - "cs:fix": "php vendor/bin/php-cs-fixer fix -v", - "infect": "XDEBUG_MODE=coverage php vendor/bin/roave-infection-static-analysis-plugin --configuration=infection.json.dist", - "infect:ci": "XDEBUG_MODE=coverage php vendor/bin/roave-infection-static-analysis-plugin --ansi --configuration=infection.json.dist --logger-github --ignore-msi-with-no-mutations --only-covered", - "psalm": "php vendor/bin/psalm --show-info=true", - "psalm:baseline": "php vendor/bin/psalm --set-baseline=psalm-baseline.xml", - "psalm:ci": "php vendor/bin/psalm --output-format=github --shepherd --show-info=false --stats --threads=4", - "refactor": "php vendor/bin/rector process --config=rector.php", - "refactor:ci": "php vendor/bin/rector process --config=rector.php --dry-run --ansi", - "stan": "php vendor/bin/phpstan analyse --memory-limit=2G", - "stan:baseline": "php vendor/bin/phpstan analyse --generate-baseline --memory-limit=2G --allow-empty-baseline", - "stan:ci": "php vendor/bin/phpstan analyse --memory-limit=2G --error-format=github", - "test": "XDEBUG_MODE=coverage php vendor/bin/pest --exclude-group=phpunit-only --color=always", - "test:cc": "XDEBUG_MODE=coverage php vendor/bin/pest --coverage --coverage-clover=.build/phpunit/logs/clover.xml --exclude-group=phpunit-only --color=always", - "test:sep": "XDEBUG_MODE=coverage php vendor/bin/phpunit --group=phpunit-only --color=always --exclude-testsuite=Arch" + "cs:diff": "php-cs-fixer fix --dry-run -v --diff", + "cs:fix": "php-cs-fixer fix -v", + "infect": [ + "@putenv XDEBUG_MODE=coverage", + "roave-infection-static-analysis-plugin --configuration=infection.json.dist" + ], + "infect:ci": [ + "@putenv XDEBUG_MODE=coverage", + "roave-infection-static-analysis-plugin --ansi --configuration=infection.json.dist --logger-github --ignore-msi-with-no-mutations --only-covered" + ], + "psalm": "psalm --show-info=true", + "psalm:baseline": "psalm --set-baseline=psalm-baseline.xml", + "psalm:ci": "psalm --output-format=github --shepherd --show-info=false --stats --threads=4", + "refactor": "rector process --config=rector.php", + "refactor:ci": "rector process --config=rector.php --dry-run --ansi", + "stan": "phpstan analyse --memory-limit=2G", + "stan:baseline": "phpstan analyse --generate-baseline --memory-limit=2G --allow-empty-baseline", + "stan:ci": "phpstan analyse --memory-limit=2G --error-format=github", + "test": [ + "@putenv XDEBUG_MODE=coverage", + "pest --exclude-group=phpunit-only --color=always" + ], + "test:cc": [ + "@putenv XDEBUG_MODE=coverage", + "pest --coverage --coverage-clover=.build/phpunit/logs/clover.xml --exclude-group=phpunit-only --color=always" + ], + "test:sep": [ + "@putenv XDEBUG_MODE=coverage", + "phpunit --group=phpunit-only --color=always --exclude-testsuite=Arch" + ] } } diff --git a/composer.lock b/composer.lock index d2c44918..8bd01d12 100644 --- a/composer.lock +++ b/composer.lock @@ -1592,16 +1592,16 @@ }, { "name": "composer/pcre", - "version": "3.1.3", + "version": "3.1.4", "source": { "type": "git", "url": "https://github.com/composer/pcre.git", - "reference": "5b16e25a5355f1f3afdfc2f954a0a80aec4826a8" + "reference": "04229f163664973f68f38f6f73d917799168ef24" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/pcre/zipball/5b16e25a5355f1f3afdfc2f954a0a80aec4826a8", - "reference": "5b16e25a5355f1f3afdfc2f954a0a80aec4826a8", + "url": "https://api.github.com/repos/composer/pcre/zipball/04229f163664973f68f38f6f73d917799168ef24", + "reference": "04229f163664973f68f38f6f73d917799168ef24", "shasum": "" }, "require": { @@ -1643,7 +1643,7 @@ ], "support": { "issues": "https://github.com/composer/pcre/issues", - "source": "https://github.com/composer/pcre/tree/3.1.3" + "source": "https://github.com/composer/pcre/tree/3.1.4" }, "funding": [ { @@ -1659,7 +1659,7 @@ "type": "tidelift" } ], - "time": "2024-03-19T10:26:25+00:00" + "time": "2024-05-27T13:40:54+00:00" }, { "name": "composer/semver", @@ -3388,16 +3388,16 @@ }, { "name": "phpstan/phpstan", - "version": "1.11.1", + "version": "1.11.2", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "e524358f930e41a2b4cca1320e3b04fc26b39e0b" + "reference": "0d5d4294a70deb7547db655c47685d680e39cfec" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/e524358f930e41a2b4cca1320e3b04fc26b39e0b", - "reference": "e524358f930e41a2b4cca1320e3b04fc26b39e0b", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/0d5d4294a70deb7547db655c47685d680e39cfec", + "reference": "0d5d4294a70deb7547db655c47685d680e39cfec", "shasum": "" }, "require": { @@ -3442,7 +3442,7 @@ "type": "github" } ], - "time": "2024-05-15T08:00:59+00:00" + "time": "2024-05-24T13:23:04+00:00" }, { "name": "phpstan/phpstan-deprecation-rules", @@ -4415,16 +4415,16 @@ }, { "name": "react/promise", - "version": "v3.1.0", + "version": "v3.2.0", "source": { "type": "git", "url": "https://github.com/reactphp/promise.git", - "reference": "e563d55d1641de1dea9f5e84f3cccc66d2bfe02c" + "reference": "8a164643313c71354582dc850b42b33fa12a4b63" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/promise/zipball/e563d55d1641de1dea9f5e84f3cccc66d2bfe02c", - "reference": "e563d55d1641de1dea9f5e84f3cccc66d2bfe02c", + "url": "https://api.github.com/repos/reactphp/promise/zipball/8a164643313c71354582dc850b42b33fa12a4b63", + "reference": "8a164643313c71354582dc850b42b33fa12a4b63", "shasum": "" }, "require": { @@ -4476,7 +4476,7 @@ ], "support": { "issues": "https://github.com/reactphp/promise/issues", - "source": "https://github.com/reactphp/promise/tree/v3.1.0" + "source": "https://github.com/reactphp/promise/tree/v3.2.0" }, "funding": [ { @@ -4484,7 +4484,7 @@ "type": "open_collective" } ], - "time": "2023-11-16T16:21:57+00:00" + "time": "2024-05-24T10:39:05+00:00" }, { "name": "react/socket", @@ -6379,16 +6379,16 @@ }, { "name": "wayofdev/cs-fixer-config", - "version": "v1.4.4", + "version": "v1.4.5", "source": { "type": "git", "url": "https://github.com/wayofdev/php-cs-fixer-config.git", - "reference": "8d0578a412be010919a0109cf484f95742cf5291" + "reference": "d38222297a12344cb968b85213878534ffffbefc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/wayofdev/php-cs-fixer-config/zipball/8d0578a412be010919a0109cf484f95742cf5291", - "reference": "8d0578a412be010919a0109cf484f95742cf5291", + "url": "https://api.github.com/repos/wayofdev/php-cs-fixer-config/zipball/d38222297a12344cb968b85213878534ffffbefc", + "reference": "d38222297a12344cb968b85213878534ffffbefc", "shasum": "" }, "require": { @@ -6453,7 +6453,7 @@ "type": "github" } ], - "time": "2024-05-23T14:54:07+00:00" + "time": "2024-05-28T13:37:07+00:00" }, { "name": "webmozart/assert", diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 5e64aac1..c2612d42 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -204,7 +204,7 @@ - + getClass()]]> diff --git a/resources/version.json b/resources/version.json index 7d4ac4e4..3800c069 100644 --- a/resources/version.json +++ b/resources/version.json @@ -1,3 +1,3 @@ { - ".": "1.7.3" + ".": "1.8.0" } diff --git a/src/Support/Caster/EnumValue.php b/src/Client/Caster/EnumValue.php similarity index 84% rename from src/Support/Caster/EnumValue.php rename to src/Client/Caster/EnumValue.php index ad6a6f89..c3c4a109 100644 --- a/src/Support/Caster/EnumValue.php +++ b/src/Client/Caster/EnumValue.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Buggregator\Trap\Support\Caster; +namespace Buggregator\Trap\Client\Caster; /** * @internal diff --git a/src/Support/ProtobufCaster.php b/src/Client/Caster/ProtobufCaster.php similarity index 98% rename from src/Support/ProtobufCaster.php rename to src/Client/Caster/ProtobufCaster.php index 25789d4f..05dd8f4b 100644 --- a/src/Support/ProtobufCaster.php +++ b/src/Client/Caster/ProtobufCaster.php @@ -2,11 +2,10 @@ declare(strict_types=1); -namespace Buggregator\Trap\Support; +namespace Buggregator\Trap\Client\Caster; -use Buggregator\Trap\Support\Caster\EnumValue; -use Google\Protobuf\Internal\Descriptor as InternalDescriptor; use Google\Protobuf\Descriptor as PublicDescriptor; +use Google\Protobuf\Internal\Descriptor as InternalDescriptor; use Google\Protobuf\Internal\DescriptorPool; use Google\Protobuf\Internal\GPBType; use Google\Protobuf\Internal\MapField; diff --git a/src/Client/Caster/Trace.php b/src/Client/Caster/Trace.php new file mode 100644 index 00000000..9be5c575 --- /dev/null +++ b/src/Client/Caster/Trace.php @@ -0,0 +1,56 @@ + $number The tick number. + * @param float $delta The time delta between the current and previous tick. + * @param int<0, max> $memory The memory usage. + * @param list + * }> $stack The stack trace. + */ + public function __construct( + public readonly int $number, + public readonly float $delta, + public readonly int $memory, + public readonly array $stack, + ) {} + + public function __toString(): string + { + // Format delta + $delta = $this->delta; + $deltaStr = match (true) { + $delta === 0.0 => '-.---', + $delta < 0.001 => \sprintf('+%.3fms', $delta * 1000), + $delta < 0.01 => \sprintf('+%.2fms', $delta * 1000), + $delta < 1 => \sprintf('+%.1fms', ($delta * 1000)), + $delta < 10 => \sprintf('+%.2fs', $delta), + $delta < 60 => \sprintf('+%.1fs', $delta), + default => \sprintf('+%dm %ds', (int) $delta % 60, (int) $delta % 60), + }; + + return \sprintf( + "Trace #%d %s %sM", + $this->number, + $deltaStr, + \number_format($this->memory / 1024 / 1024, 2, '.', '_'), + ); + } +} diff --git a/src/Client/Caster/TraceCaster.php b/src/Client/Caster/TraceCaster.php new file mode 100644 index 00000000..377da934 --- /dev/null +++ b/src/Client/Caster/TraceCaster.php @@ -0,0 +1,60 @@ +class = $tick->__toString(); + $stub->type = $stub::TYPE_OBJECT; + $stub->attr = $tick->stack; + $stub->handle = 0; + + $result = []; + foreach ($tick->stack as $line) { + $result[Caster::PREFIX_VIRTUAL . self::renderMethod($line)] = new TraceFile($line); + } + + + return $result; + } + + public static function castLine(TraceFile $line, array $a, Stub $stub, bool $isNested): array + { + $stub->type = $stub::TYPE_REF; + $stub->attr = $line->line; + $stub->handle = 0; + $stub->class = $line->__toString(); + return []; + } + + /** + * @param array{ + * function: string, + * line?: int, + * file?: string, + * class?: class-string, + * type?: string, + * args?: list + * } $line The stack trace line. + */ + private static function renderMethod(array $line): string + { + if (!isset($line['class'])) { + return $line['function'] . '()'; + } + + $line['type'] ??= "::"; + + return "{$line['class']}{$line['type']}{$line['function']}()"; + } +} diff --git a/src/Client/Caster/TraceFile.php b/src/Client/Caster/TraceFile.php new file mode 100644 index 00000000..0447750c --- /dev/null +++ b/src/Client/Caster/TraceFile.php @@ -0,0 +1,32 @@ + + * } $line The stack trace line. + */ + public function __construct( + public readonly array $line, + ) {} + + public function __toString(): string + { + return isset($this->line['file'], $this->line['line']) + ? \basename($this->line['file']) . ':' . $this->line['line'] + : ''; + } +} diff --git a/src/Client/TrapHandle.php b/src/Client/TrapHandle.php index 505f25fb..24041b36 100644 --- a/src/Client/TrapHandle.php +++ b/src/Client/TrapHandle.php @@ -4,6 +4,7 @@ namespace Buggregator\Trap\Client; +use Buggregator\Trap\Client\Caster\Trace; use Buggregator\Trap\Client\TrapHandle\Counter; use Buggregator\Trap\Client\TrapHandle\Dumper as VarDumper; use Buggregator\Trap\Client\TrapHandle\StaticState; @@ -35,6 +36,22 @@ public static function fromArray(array $array): self return new self($array); } + /** + * Create a new instance with a single value. + * + * @param int<0, max> $number The tick number. + * @param float $delta The time delta between the current and previous tick. + * + * @internal + */ + public static function fromTicker(int $number, float $delta, int $memory): self + { + $self = new self([]); + $self->values[] = new Trace($number, $delta, $memory, \array_slice($self->staticState->stackTrace, 0, 3)); + + return $self; + } + /** * Dump only if the condition is true. * The check is performed immediately upon declaration. @@ -172,6 +189,28 @@ public function context(mixed ...$values): self return $this; } + /** + * Code syntax highlighting. + * + * Adds `language` data context to denote the passed data as source code. + * In this case, Buggregator will perform code highlighting. + * + * Note: it equals to `trap()->context(language: $syntax);` + * + * ```php + * trap( + * index: $indexCode, + * controller: $controllerCode, + * )->code('php'); + * ``` + * + * @param non-empty-string $syntax The name of the programming language + */ + public function code(string $syntax): self + { + return $this->context(language: $syntax); + } + public function __destruct() { $this->haveToSend() and $this->sendDump(); diff --git a/src/Client/TrapHandle/StackTrace.php b/src/Client/TrapHandle/StackTrace.php index 5d96006a..a8a9bb06 100644 --- a/src/Client/TrapHandle/StackTrace.php +++ b/src/Client/TrapHandle/StackTrace.php @@ -5,9 +5,8 @@ namespace Buggregator\Trap\Client\TrapHandle; /** - * * @psalm-type StackTraceWithObjects = list * }> * @psalm-type SimpleStackTrace = list + */ + public static array $facades = []; + /** * Returns a backtrace as an array. * Removes the internal frames and the first next frames after them. @@ -46,16 +52,19 @@ public static function stackTrace(string $baseDir = '', bool $provideObjects = f ($provideObjects ? \DEBUG_BACKTRACE_PROVIDE_OBJECT : 0) | \DEBUG_BACKTRACE_IGNORE_ARGS, ) as $frame ) { - if (\str_starts_with($frame['class'] ?? '', 'Buggregator\\Trap\\Client\\')) { + $class = $frame['class'] ?? ''; + if (\str_starts_with($class, 'Buggregator\\Trap\\Client\\')) { $internal = true; $stack = []; continue; } if ($internal) { - // todo check the NoStackTrace attribute - - $internal = false; + if ($class === '' && \array_key_exists($frame['function'], self::$facades)) { + $stack = []; + } else { + $internal = false; + } } // Convert absolute paths to relative ones diff --git a/src/Socket/Client.php b/src/Socket/Client.php index ab9a06de..9b1a2692 100644 --- a/src/Socket/Client.php +++ b/src/Socket/Client.php @@ -112,7 +112,7 @@ public function process(): void */ public function setOnPayload(callable $callable): void { - $this->onPayload = \Closure::bind($callable(...), $this) ?? $callable(...); + $this->onPayload = @\Closure::bind($callable(...), $this) ?? $callable(...); } /** @@ -121,7 +121,7 @@ public function setOnPayload(callable $callable): void */ public function setOnClose(callable $callable): void { - $this->onClose = \Closure::bind($callable(...), $this) ?? $callable(...); + $this->onClose = @\Closure::bind($callable(...), $this) ?? $callable(...); } public function send(string $payload): void diff --git a/src/functions.php b/src/functions.php index 622a505a..88f0f5fa 100644 --- a/src/functions.php +++ b/src/functions.php @@ -2,9 +2,13 @@ declare(strict_types=1); +use Buggregator\Trap\Client\Caster\EnumValue; +use Buggregator\Trap\Client\Caster\ProtobufCaster; +use Buggregator\Trap\Client\Caster\Trace; +use Buggregator\Trap\Client\Caster\TraceCaster; +use Buggregator\Trap\Client\Caster\TraceFile; use Buggregator\Trap\Client\TrapHandle; -use Buggregator\Trap\Support\Caster\EnumValue; -use Buggregator\Trap\Support\ProtobufCaster; +use Buggregator\Trap\Client\TrapHandle\StackTrace; use Google\Protobuf\Internal\MapField; use Google\Protobuf\Internal\Message; use Google\Protobuf\Internal\RepeatedField; @@ -22,6 +26,67 @@ function trap(mixed ...$values): TrapHandle /** @psalm-suppress InternalMethod */ return TrapHandle::fromArray($values); } + + StackTrace::$facades['trap'] = true; +} catch (\Throwable $e) { + // do nothing +} + +try { + /** + * Send values into Buggregator and return the first value. + * + * When arguments are passed it equals to {@see trap()} with `->return()` method call. + * When no arguments passed, it calculates ticks, time between the last `tr()` call and memory usage. + * + * @param mixed ...$values + */ + function tr(mixed ...$values): mixed + { + /** @var int<-1, max> $counter */ + static $counter = -1; + /** @var float $time */ + static $time = 0.0; + + ++$counter; + + $previous = $time; + $mem = $time = \microtime(true); + try { + if ($values === []) { + /** @psalm-suppress InternalMethod */ + return TrapHandle::fromTicker( + $counter, + $counter === 0 ? 0 : $mem - $previous, + \memory_get_usage(), + )->return(); + } + + /** @psalm-suppress InternalMethod */ + return TrapHandle::fromArray($values)->return(); + } finally { + $mem === $time and $time = \microtime(true); + } + } + + StackTrace::$facades['tr'] = true; + + /** + * Send values into Buggregator and die. + * + * When no arguments passed, it works like {@see tr()}. + * + * @param mixed ...$values + * + * @codeCoverageIgnore + */ + function td(mixed ...$values): never + { + tr(...$values); + die; + } + + StackTrace::$facades['td'] = true; } catch (\Throwable $e) { // do nothing } @@ -29,7 +94,7 @@ function trap(mixed ...$values): TrapHandle /** * Register the var-dump caster for protobuf messages */ -if (class_exists(AbstractCloner::class)) { +if (\class_exists(AbstractCloner::class)) { /** @psalm-suppress MixedAssignment */ AbstractCloner::$defaultCasters[Message::class] ??= [ProtobufCaster::class, 'cast']; /** @psalm-suppress MixedAssignment */ @@ -38,4 +103,8 @@ function trap(mixed ...$values): TrapHandle AbstractCloner::$defaultCasters[MapField::class] ??= [ProtobufCaster::class, 'castMap']; /** @psalm-suppress MixedAssignment */ AbstractCloner::$defaultCasters[EnumValue::class] ??= [ProtobufCaster::class, 'castEnum']; + /** @psalm-suppress MixedAssignment */ + AbstractCloner::$defaultCasters[Trace::class] = [TraceCaster::class, 'cast']; + /** @psalm-suppress MixedAssignment */ + AbstractCloner::$defaultCasters[TraceFile::class] = [TraceCaster::class, 'castLine']; } diff --git a/tests/Unit/Client/Caster/TraceFileTest.php b/tests/Unit/Client/Caster/TraceFileTest.php new file mode 100644 index 00000000..14012a0f --- /dev/null +++ b/tests/Unit/Client/Caster/TraceFileTest.php @@ -0,0 +1,43 @@ + 'foo', + 'line' => 42, + 'file' => '/path/to/file.php', + 'class' => 'Foo', + 'type' => '->', + 'args' => ['bar'], + ]); + + self::assertSame('file.php:42', (string) $traceFile); + } + + public function testToStringWithoutFile(): void + { + $traceFile = new \Buggregator\Trap\Client\Caster\TraceFile([ + 'function' => 'foo', + ]); + + self::assertSame('', (string) $traceFile); + } + + public function testToStringWithoutLine(): void + { + $traceFile = new \Buggregator\Trap\Client\Caster\TraceFile([ + 'function' => 'foo', + 'file' => '/path/to/file.php', + ]); + + self::assertSame('', (string) $traceFile); + } +} diff --git a/tests/Unit/Client/Caster/TraceTest.php b/tests/Unit/Client/Caster/TraceTest.php new file mode 100644 index 00000000..1b822684 --- /dev/null +++ b/tests/Unit/Client/Caster/TraceTest.php @@ -0,0 +1,58 @@ + $number + * @param int<0, max> $memory + * @param non-empty-string $result + */ + #[DataProvider('provideToString')] + public function testToString(int $number, float $delta, int $memory, string $result): void + { + $trace = new Trace( + number: $number, + delta: $delta, + memory: $memory, + stack: [ + [ + 'function' => 'foo', + 'line' => 42, + 'file' => '/path/to/file.php', + 'class' => 'Foo', + 'type' => '->', + 'args' => ['bar'], + ], + [ + 'function' => 'bar', + 'line' => 23, + 'file' => '/path/to/file.php', + 'class' => 'Bar', + 'type' => '::', + 'args' => ['baz'], + ], + ], + ); + + self::assertSame($result, (string) $trace); + } +} diff --git a/tests/Unit/Client/FunctionTrapTest.php b/tests/Unit/Client/FunctionTrapTest.php index 1a471215..e3cd7399 100644 --- a/tests/Unit/Client/FunctionTrapTest.php +++ b/tests/Unit/Client/FunctionTrapTest.php @@ -4,15 +4,14 @@ namespace Buggregator\Trap\Tests\Unit\Client; +use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\RunInSeparateProcess; use PHPUnit\Framework\TestCase; final class FunctionTrapTest extends TestCase { - /** - * @runInSeparateProcess - * - * @group phpunit-only - */ + #[RunInSeparateProcess] + #[Group('phpunit-only')] public function testLeak(): void { $object = new \stdClass(); diff --git a/tests/Unit/Client/TrTest.php b/tests/Unit/Client/TrTest.php new file mode 100644 index 00000000..5bb22e03 --- /dev/null +++ b/tests/Unit/Client/TrTest.php @@ -0,0 +1,77 @@ +getContext()['label']); + } + + /** + * Check the stacktrace contains three items and it begins with the right function name. + */ + #[RunInSeparateProcess] + #[Group('phpunit-only')] + public function testTrAsStackTrace(): void + { + tr(); + + // 3 Stack Trace items + self::assertCount(3, self::$lastData->getValue()); + self::assertStringMatchesFormat('Trace #0 -.--- %fM', self::$lastData->getType()); + $firstLineKey = \array_key_first(self::$lastData->getValue()); + self::assertStringContainsString('tr()', $firstLineKey); + + tr(); + + self::assertCount(3, self::$lastData->getValue()); + self::assertStringMatchesFormat('Trace #1 +%fms %fM', self::$lastData->getType()); + $firstLineKey = \array_key_first(self::$lastData->getValue()); + self::assertStringContainsString('tr()', $firstLineKey); + } + + /** + * After calling {@see tr()} the dumped data isn't stored in the memory. + */ + public function testLeak(): void + { + $object = new \stdClass(); + $ref = \WeakReference::create($object); + + tr($object, $object); + unset($object); + + $this->assertNull($ref->get()); + } + + public function testReturn(): void + { + $this->assertSame(42, tr(42)); + $this->assertSame(42, tr(named: 42)); + $this->assertSame(42, tr(42, 43)); + $this->assertSame('foo', tr(...['0' => 'foo', 42 => 90])); + $this->assertNull(tr(null)); + } + + public function testReturnSendsDumpOnce(): void + { + $dumper = $this->getMockBuilder(DataDumperInterface::class) + ->getMock(); + $dumper->expects($this->once()) + ->method('dump') + ->willReturnArgument(1); + Dumper::setDumper($dumper); + + $this->assertSame(42, tr(42)); + } +} diff --git a/tests/Unit/Client/TrapTest.php b/tests/Unit/Client/TrapTest.php index 55cb241b..52ac0740 100644 --- a/tests/Unit/Client/TrapTest.php +++ b/tests/Unit/Client/TrapTest.php @@ -47,6 +47,13 @@ public function testContextMultiple(): void self::assertSame(['foo' => 'new', 'bar' => 'bar-context'], self::$lastData->getContext()); } + public function testCodeHighlight(): void + { + trap('test-value')->code('php'); + + self::assertSame(['language' => 'php'], self::$lastData->getContext()); + } + /** * Check the first line of dumped stacktrace string contains right file and line. */