From 5ba83a2b501e05e1f92042ff1408de0c71f68892 Mon Sep 17 00:00:00 2001 From: TangRufus Date: Tue, 20 Oct 2026 20:06:06 +0100 Subject: [PATCH] `PHPStan`: Analyse tests --- composer.json | 4 +- composer.lock | 142 +++++++++++++++++++- phpstan.neon | 1 + tests/TestCase.php | 13 ++ tests/Unit/ComposerTest.php | 56 ++++---- tests/Unit/Matrices/MatrixTest.php | 19 +-- tests/Unit/Matrices/MinorOnlyMatrixTest.php | 15 +-- tests/Unit/Releases/OfflineReleasesTest.php | 16 +-- tests/Unit/Releases/PhpNetReleasesTest.php | 18 +-- tests/Unit/VersionsTest.php | 11 +- 10 files changed, 207 insertions(+), 88 deletions(-) diff --git a/composer.json b/composer.json index 6279d1cd..df63c5e8 100644 --- a/composer.json +++ b/composer.json @@ -39,9 +39,11 @@ "laravel/pao": "^1.1", "mockery/mockery": "^1.6", "pestphp/pest": "^5.3", + "pestphp/pest-plugin-phpstan": "^5.2", "phpstan/extension-installer": "^1.4", - "phpstan/phpstan": "^2.1", + "phpstan/phpstan": "^2.3", "phpstan/phpstan-deprecation-rules": "^2.0", + "phpstan/phpstan-mockery": "^2.0", "phpstan/phpstan-strict-rules": "^2.0", "phpstan/phpstan-symfony": "^2.0" }, diff --git a/composer.lock b/composer.lock index 336d6058..0547f60d 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "42415ce59b83c3e01cba4896ccbda5c2", + "content-hash": "4adfbd21ccf29655b2c3b60273c34d3b", "packages": [ { "name": "composer/semver", @@ -2898,6 +2898,97 @@ ], "time": "2026-08-10T11:01:27+00:00" }, + { + "name": "pestphp/pest-plugin-phpstan", + "version": "v5.2.2", + "source": { + "type": "git", + "url": "https://github.com/pestphp/pest-plugin-phpstan.git", + "reference": "19afd92402be203788b73eba9fdb5f5185f66a25" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pestphp/pest-plugin-phpstan/zipball/19afd92402be203788b73eba9fdb5f5185f66a25", + "reference": "19afd92402be203788b73eba9fdb5f5185f66a25", + "shasum": "" + }, + "require": { + "pestphp/pest": "^5.0.0", + "php": "^8.4", + "phpstan/phpstan": "^2.2.5" + }, + "conflict": { + "pestphp/pest": "<5.0.0" + }, + "require-dev": { + "laravel/pao": "^1.1.2", + "laravel/pint": "^1.29.3", + "pestphp/pest-plugin-rector": "^5.0", + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan-strict-rules": "^2.0.12", + "rector/rector": "^2.5.7" + }, + "type": "phpstan-extension", + "extra": { + "phpstan": { + "includes": [ + "extension.neon" + ] + } + }, + "autoload": { + "psr-4": { + "Pest\\PHPStan\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Punyapal Shah", + "email": "mrpunyapal@gmail.com" + }, + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "PHPStan plugin for Pest", + "keywords": [ + "PHPStan", + "framework", + "pest", + "php", + "plugin", + "test", + "testing", + "unit" + ], + "support": { + "source": "https://github.com/pestphp/pest-plugin-phpstan/tree/v5.2.2" + }, + "funding": [ + { + "url": "https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=66BYDWAT92N6L", + "type": "custom" + }, + { + "url": "https://github.com/mrpunyapal", + "type": "github" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + }, + { + "url": "https://www.patreon.com/nunomaduro", + "type": "patreon" + } + ], + "time": "2026-10-02T16:21:04+00:00" + }, { "name": "pestphp/pest-plugin-profanity", "version": "v5.0.1", @@ -3467,6 +3558,55 @@ }, "time": "2026-07-22T06:50:43+00:00" }, + { + "name": "phpstan/phpstan-mockery", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan-mockery.git", + "reference": "89a949d0ac64298e88b7c7fa00caee565c198394" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan-mockery/zipball/89a949d0ac64298e88b7c7fa00caee565c198394", + "reference": "89a949d0ac64298e88b7c7fa00caee565c198394", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "phpstan/phpstan": "^2.0" + }, + "require-dev": { + "mockery/mockery": "^1.6.11", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6" + }, + "type": "phpstan-extension", + "extra": { + "phpstan": { + "includes": [ + "extension.neon" + ] + } + }, + "autoload": { + "psr-4": { + "PHPStan\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan Mockery extension", + "support": { + "issues": "https://github.com/phpstan/phpstan-mockery/issues", + "source": "https://github.com/phpstan/phpstan-mockery/tree/2.0.0" + }, + "time": "2024-10-14T03:18:12+00:00" + }, { "name": "phpstan/phpstan-strict-rules", "version": "2.1.0", diff --git a/phpstan.neon b/phpstan.neon index 7cffe7a4..2f52996b 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -3,3 +3,4 @@ parameters: paths: - bin - src + - tests diff --git a/tests/TestCase.php b/tests/TestCase.php index 06ebf9d0..7cee3306 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -5,6 +5,7 @@ namespace Tests; use PHPUnit\Framework\TestCase as BaseTestCase; +use RuntimeException; abstract class TestCase extends BaseTestCase { @@ -12,6 +13,7 @@ abstract class TestCase extends BaseTestCase protected const string ALL_VERSIONS_FILE = __DIR__ . '/../data/all-versions.json'; + /** @var string[] */ protected const array RELEASES_JSONS = [ self::DATA_DIR . '/versions/v5.json', self::DATA_DIR . '/versions/v7.json', @@ -24,12 +26,23 @@ abstract class TestCase extends BaseTestCase protected function allVersions(): array { static $allVersions = null; + if ($allVersions === null) { $content = file_get_contents(self::ALL_VERSIONS_FILE); + if (!is_string($content)) { + $message = sprintf('Failed to read fixture file content at path "%s".', self::ALL_VERSIONS_FILE); + throw new RuntimeException($message); + } + $allVersions = json_decode($content, true, 512, JSON_THROW_ON_ERROR); + + if (!is_array($allVersions) || !array_all($allVersions, static fn(mixed $v) => is_string($v))) { + throw new RuntimeException('Failed to decode fixture into string[].'); + } } + /** @var string[] $allVersions */ return $allVersions; } } diff --git a/tests/Unit/ComposerTest.php b/tests/Unit/ComposerTest.php index fbdabe45..ccbbdb55 100644 --- a/tests/Unit/ComposerTest.php +++ b/tests/Unit/ComposerTest.php @@ -11,7 +11,7 @@ covers(Composer::class); -describe(Composer::class, static function (): void { +describe(Composer::class, function (): void { beforeEach(function () { $this->tempFile = tempnam(sys_get_temp_dir(), 'composer_'); }); @@ -20,14 +20,12 @@ unlink($this->tempFile); }); - describe('::fromFile()', static function (): void { + describe('::fromFile()', function (): void { it('reads valid file', function () { file_put_contents($this->tempFile, '{"require":{"php":"^8.1"}}'); $composer = Composer::fromFile($this->tempFile); - - expect($composer)->toBeInstanceOf(Composer::class); - }); + })->throwsNoExceptions(); it('throws on unreadable file', function () { $path = '/nonexistent/path/composer.json'; @@ -42,7 +40,7 @@ })->throws(JsonException::class); }); - describe('::requiredPhpConstraint()', static function (): void { + describe('::requiredPhpConstraint()', function (): void { it('reads valid file', function (string $content) { file_put_contents($this->tempFile, $content); @@ -60,29 +58,27 @@ $composer = Composer::fromFile($this->tempFile); - $composer->requiredPhpConstraint(); - }) - ->throws(UnexpectedValueException::class) - ->with([ - // No "require.php" is set. - '{"require":{"some/package":"^1.0"}}', - '{"require":{}}', - '{"require":123}', - '{"require-dev":{"php":"^1.0"}}', - '{"php":"^1.0"}', - '{}', - // "require.php" is not a string. - '{"require":{"php":null}}', - '{"require":{"php":{}}}', - '{"require":{"php":[]}}', - '{"require":{"php":123}}', - '{"require":{"php":12.3}}', - '{"require":{"php":true}}', - '{"require":{"php":false}}', - // "require.php" is not a valid constraint. - '{"require":{"php":"invalid constraint"}}', - '{"require":{"php":"~>1.0"}}', - '{"require":{"php":""}}', - ]); + expect($composer->requiredPhpConstraint(...))->toThrow(UnexpectedValueException::class); + })->with([ + // No "require.php" is set. + '{"require":{"some/package":"^1.0"}}', + '{"require":{}}', + '{"require":123}', + '{"require-dev":{"php":"^1.0"}}', + '{"php":"^1.0"}', + '{}', + // "require.php" is not a string. + '{"require":{"php":null}}', + '{"require":{"php":{}}}', + '{"require":{"php":[]}}', + '{"require":{"php":123}}', + '{"require":{"php":12.3}}', + '{"require":{"php":true}}', + '{"require":{"php":false}}', + // "require.php" is not a valid constraint. + '{"require":{"php":"invalid constraint"}}', + '{"require":{"php":"~>1.0"}}', + '{"require":{"php":""}}', + ]); }); }); diff --git a/tests/Unit/Matrices/MatrixTest.php b/tests/Unit/Matrices/MatrixTest.php index df9d56a7..53127146 100644 --- a/tests/Unit/Matrices/MatrixTest.php +++ b/tests/Unit/Matrices/MatrixTest.php @@ -7,22 +7,13 @@ use Mockery; use TypistTech\PhpMatrix\Exceptions\UnexpectedValueException; use TypistTech\PhpMatrix\Matrices\Matrix; -use TypistTech\PhpMatrix\Matrices\MatrixInterface; use TypistTech\PhpMatrix\Releases\ReleasesInterface; covers(Matrix::class); -describe(Matrix::class, static function (): void { - it('implements ReleasesInterface', function () { - $releases = Mockery::mock(ReleasesInterface::class); - - $matrix = new Matrix($releases); - - expect($matrix)->toBeInstanceOf(MatrixInterface::class); - }); - - describe('::satisfiedBy()', static function (): void { - dataset('satisfied_by', static function (): array { +describe(Matrix::class, function (): void { + describe('::satisfiedBy()', function (): void { + dataset('satisfied_by', function (): array { $releases = [ '1.0.0', '2.0.1', @@ -75,8 +66,8 @@ $matrix = new Matrix($releases); - $matrix->satisfiedBy('invalid constraint'); - })->throws(UnexpectedValueException::class); + expect(fn() => $matrix->satisfiedBy('invalid constraint'))->toThrow(UnexpectedValueException::class); + }); it('does not invoke ReleasesInterface::all() when invalid constraint', function () { $releases = Mockery::mock(ReleasesInterface::class); diff --git a/tests/Unit/Matrices/MinorOnlyMatrixTest.php b/tests/Unit/Matrices/MinorOnlyMatrixTest.php index c8449a12..6c9e2694 100644 --- a/tests/Unit/Matrices/MinorOnlyMatrixTest.php +++ b/tests/Unit/Matrices/MinorOnlyMatrixTest.php @@ -5,23 +5,14 @@ namespace Tests\Unit\Matrices; use Mockery; -use TypistTech\PhpMatrix\Matrices\MatrixInterface; use TypistTech\PhpMatrix\Matrices\MinorOnlyMatrix; use TypistTech\PhpMatrix\Releases\ReleasesInterface; covers(MinorOnlyMatrix::class); -describe(MinorOnlyMatrix::class, static function (): void { - it('implements ReleasesInterface', function () { - $releases = Mockery::mock(ReleasesInterface::class); - - $matrix = new MinorOnlyMatrix($releases); - - expect($matrix)->toBeInstanceOf(MatrixInterface::class); - }); - - describe('::satisfiedBy()', static function (): void { - dataset('satisfied_by', static function (): array { +describe(MinorOnlyMatrix::class, function (): void { + describe('::satisfiedBy()', function (): void { + dataset('satisfied_by', function (): array { $releases = [ '1.0.0', '2.0.1', diff --git a/tests/Unit/Releases/OfflineReleasesTest.php b/tests/Unit/Releases/OfflineReleasesTest.php index c53f30af..dc3ac03e 100644 --- a/tests/Unit/Releases/OfflineReleasesTest.php +++ b/tests/Unit/Releases/OfflineReleasesTest.php @@ -5,28 +5,18 @@ namespace Tests\Unit\Releases; use TypistTech\PhpMatrix\Releases\OfflineReleases; -use TypistTech\PhpMatrix\Releases\ReleasesInterface; covers(OfflineReleases::class); -describe(OfflineReleases::class, static function (): void { - it('implements ReleasesInterface', function () { - $releases = new OfflineReleases(); - - expect($releases)->toBeInstanceOf(ReleasesInterface::class); - }); - - describe('::all()', static function (): void { +describe(OfflineReleases::class, function (): void { + describe('::all()', function (): void { it('fetches all versions', function () { $releases = new OfflineReleases(); $actual = $releases->all(); - expect($actual)->each->toBeString(); - $expected = $this->allVersions(); - expect($actual)->toContain(...$expected); - expect($expected)->toContain(...$actual); + expect($actual)->toEqualCanonicalizing($expected); }); }); }); diff --git a/tests/Unit/Releases/PhpNetReleasesTest.php b/tests/Unit/Releases/PhpNetReleasesTest.php index 5f147d05..5aefcb02 100644 --- a/tests/Unit/Releases/PhpNetReleasesTest.php +++ b/tests/Unit/Releases/PhpNetReleasesTest.php @@ -9,22 +9,16 @@ use GuzzleHttp\HandlerStack; use GuzzleHttp\Psr7\Response; use TypistTech\PhpMatrix\Releases\PhpNetReleases; -use TypistTech\PhpMatrix\Releases\ReleasesInterface; covers(PhpNetReleases::class); -describe(PhpNetReleases::class, static function (): void { - it('implements ReleasesInterface', function () { - $releases = new PhpNetReleases(); - - expect($releases)->toBeInstanceOf(ReleasesInterface::class); - }); - - describe('::all()', static function (): void { +describe(PhpNetReleases::class, function (): void { + describe('::all()', function (): void { it('fetches all versions', function () { $mock = new MockHandler(); foreach ($this::RELEASES_JSONS as $json) { $body = file_get_contents($json); + $body = $body === false ? null : $body; $mock->append(new Response(202, [], $body)); } $handlerStack = HandlerStack::create($mock); @@ -34,12 +28,8 @@ $actual = $releases->all(); - expect($actual)->each->toBeString(); - $expected = $this->allVersions(); - - expect($actual)->toContain(...$expected); - expect($expected)->toContain(...$actual); + expect($actual)->toEqualCanonicalizing($expected); }); }); }); diff --git a/tests/Unit/VersionsTest.php b/tests/Unit/VersionsTest.php index e75162c6..494e8ba7 100644 --- a/tests/Unit/VersionsTest.php +++ b/tests/Unit/VersionsTest.php @@ -9,21 +9,24 @@ covers(Versions::class); -describe(Versions::class, static function (): void { - describe('::sort()', static function (): void { +describe(Versions::class, function (): void { + describe('::sort()', function (): void { dataset('sort', [ [['1.2.3']], [['1.2.3', '1.2.3']], [['1.2.3', '2.2.3']], [['1.2.3', '2.2.3', '3.2.3']], + [['1.2.3', '2.2.3', '2.2.3', '4.2.3']], [['1.2']], [['1.2', '1.2']], [['1.2', '2.2']], [['1.2', '2.2', '3.2']], + [['1.2', '2.2', '2.2', '4.2']], ]); it('sorts the versions', function (array $versions): void { + /** @var string[] $versions */ $expected = $versions; shuffle($versions); @@ -31,7 +34,9 @@ $actual = Versions::sort(...$versions); expect($actual)->toBe($expected); - })->with('sort'); + }) + ->with('sort') + ->repeat(10); it('throws exception when argument is empty', function (): void { Versions::sort();