diff --git a/.gitlab-ci/pipeline.yml b/.gitlab-ci/pipeline.yml index 21b0808dca8e35ebd6dc4eab9fe8c278d78789d5..ea8af938328b5a4c92cb00eeebc8c033ba600de5 100644 --- a/.gitlab-ci/pipeline.yml +++ b/.gitlab-ci/pipeline.yml @@ -1,4 +1,4 @@ -# cspell:ignore drupaltestbot drupaltestbotpw +# cspell:ignore cobertura drupaltestbot drupaltestbotpw stages: - 🗜️ Test @@ -173,6 +173,35 @@ variables: TESTSUITE: PHPUnit-Unit KUBERNETES_CPU_REQUEST: "16" +'✅️ PHPStan Tests': + <<: [ *default-job-settings ] + variables: + KUBERNETES_CPU_REQUEST: "2" + # Run if PHPStan files have changed, or manually. + rules: + - if: $CI_PIPELINE_SOURCE == "parent_pipeline" && $PERFORMANCE_TEST != "1" + changes: + - core/tests/PHPStan/* + - composer/Metapackage/PinnedDevDependencies/composer.json + - when: manual + allow_failure: true + # Default job settings runs a script that expects vendor to exist. + before_script: [] + script: + - docker-php-ext-enable pcov + - cd core/tests/PHPStan + - composer install + - vendor/bin/phpunit tests --coverage-text --colors=never --coverage-cobertura=coverage.cobertura.xml --log-junit junit.xml + # Default job settings runs a script that junit files in a specific location.. + after_script: [] + artifacts: + when: always + reports: + junit: core/tests/PHPStan/junit.xml + coverage_report: + coverage_format: cobertura + path: core/tests/PHPStan/coverage.cobertura.xml + '🦉️️️ Nightwatch': <<: [ *with-composer-and-yarn, *default-job-settings ] variables: diff --git a/composer.json b/composer.json index 61c5d3735acf2153e6b5f7b70a17a4a3fc54b1f2..9fef43ac90fa13a8451ead72677b295c0037df73 100644 --- a/composer.json +++ b/composer.json @@ -105,6 +105,11 @@ "Drupal\\Composer\\": "composer" } }, + "autoload-dev": { + "psr-4": { + "Drupal\\PHPStan\\Rules\\": "core/tests/PHPStan/Rules" + } + }, "scripts": { "pre-install-cmd": "Drupal\\Composer\\Composer::ensureComposerVersion", "pre-update-cmd": "Drupal\\Composer\\Composer::ensureComposerVersion", diff --git a/core/.cspell.json b/core/.cspell.json index f4136d3bef9f11f460809a75e5aaa282871ba36d..229697d909a8cac824bf0e4ccbe895db265d7937 100644 --- a/core/.cspell.json +++ b/core/.cspell.json @@ -26,6 +26,7 @@ "profiles/demo_umami/modules/demo_umami_content/default_content/languages/es/**/*", "tests/fixtures/files/*", "tests/Drupal/Tests/Component/Annotation/Doctrine/**", + "tests/PHPStan/vendor/**", "themes/olivero/fonts/**", "COPYRIGHT.txt", "MAINTAINERS.txt", diff --git a/core/phpstan.neon.dist b/core/phpstan.neon.dist index 17c3970c21ac5222cb89e6edc6313ac98ad2637e..35bcfd1e64b51a005a28a1a2ff8252241ba5ec98 100644 --- a/core/phpstan.neon.dist +++ b/core/phpstan.neon.dist @@ -26,6 +26,8 @@ parameters: - ../*/node_modules/* - */tests/fixtures/*.php - */tests/fixtures/*.php.gz + # Skip Drupal's own PHPStan rules test fixtures. + - tests/PHPStan/fixtures/* # Skip Drupal 6 & 7 code. - scripts/dump-database-d?.sh - scripts/generate-d?-content.sh @@ -44,3 +46,6 @@ parameters: - "#Drupal calls should be avoided in classes, use dependency injection instead#" - "#^Plugin definitions cannot be altered.#" - "#^Class .* extends @internal class#" + +rules: + - Drupal\PHPStan\Rules\ComponentTestDoesNotExtendCoreTest diff --git a/core/tests/Drupal/Tests/Listeners/DrupalComponentTestListenerTrait.php b/core/tests/Drupal/Tests/Listeners/DrupalComponentTestListenerTrait.php deleted file mode 100644 index 90a5638e2dc0899ffbd178337f437b512484fa9c..0000000000000000000000000000000000000000 --- a/core/tests/Drupal/Tests/Listeners/DrupalComponentTestListenerTrait.php +++ /dev/null @@ -1,37 +0,0 @@ -<?php - -declare(strict_types=1); - -namespace Drupal\Tests\Listeners; - -use Drupal\KernelTests\KernelTestBase; -use Drupal\Tests\BrowserTestBase; -use Drupal\Tests\UnitTestCase; -use PHPUnit\Framework\AssertionFailedError; - -/** - * Ensures that no component tests are extending a core test base class. - * - * @internal - */ -trait DrupalComponentTestListenerTrait { - - /** - * Reacts to the end of a test. - * - * @param \PHPUnit\Framework\Test $test - * The test object that has ended its test run. - * @param float $time - * The time the test took. - */ - protected function componentEndTest($test, $time) { - /** @var \PHPUnit\Framework\Test $test */ - if (str_starts_with($test->toString(), 'Drupal\Tests\Component')) { - if ($test instanceof BrowserTestBase || $test instanceof KernelTestBase || $test instanceof UnitTestCase) { - $error = new AssertionFailedError('Component tests should not extend a core test base class.'); - $test->getTestResultObject()->addFailure($test, $error, $time); - } - } - } - -} diff --git a/core/tests/Drupal/Tests/Listeners/DrupalListener.php b/core/tests/Drupal/Tests/Listeners/DrupalListener.php index 182e4fe4e7b23a27aef9b9ccbaf940f420a7bd13..26fa1e5001e364d7c14715c4b08537cd2e271f87 100644 --- a/core/tests/Drupal/Tests/Listeners/DrupalListener.php +++ b/core/tests/Drupal/Tests/Listeners/DrupalListener.php @@ -18,7 +18,6 @@ class DrupalListener implements TestListener { use TestListenerDefaultImplementation; - use DrupalComponentTestListenerTrait; /** * The wrapped Symfony test listener. @@ -60,7 +59,6 @@ public function startTest(Test $test): void { */ public function endTest(Test $test, float $time): void { $this->symfonyListener->endTest($test, $time); - $this->componentEndTest($test, $time); } } diff --git a/core/tests/PHPStan/.gitignore b/core/tests/PHPStan/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..fa62fc5129cc317e061e3403ba75a6b0190ab0ec --- /dev/null +++ b/core/tests/PHPStan/.gitignore @@ -0,0 +1,4 @@ +composer.lock +coverage.cobertura.xml +junit.xml +vendor/ diff --git a/core/tests/PHPStan/README.md b/core/tests/PHPStan/README.md new file mode 100644 index 0000000000000000000000000000000000000000..dfe13aeaf99e16ca35399a1dadab5e8da55a2757 --- /dev/null +++ b/core/tests/PHPStan/README.md @@ -0,0 +1,43 @@ +# Drupal custom PHPStan rules + +This directory contains PHPStan rules specifically developed for Drupal. + +## Subdirectories + +* _Rules_: contains the actual rules. +* _tests_: contains PHPUnit tests for the rules. +* _fixtures_: contains fixture files for the PHPUnit tests of the rules. + +## Enabling rules + +Rules are executed when they are added to the the phpstan.neon(.dist) +configuration file of a PHPStan scan run. You need to add them under the +`rules` entry in the file, specifying the fully qualified class name of the +rule. For example: +``` + +rules: + - Drupal\PHPStan\Rules\ComponentTestDoesNotExtendCoreTest + +``` + +## Testing rules + +PHPStan rules must be tested in the context of the PHPStan testing framework, +that differs in terms of dependencies from Drupal's one. + +Note that for this reason, these tests are run _separately_ from Drupal core +tests. + +A _composer.json_ file is present in this directory, indicating the required +packages for the execution of the tests. Installing via composer +``` +$ composer install +``` +builds a _vendor_ subdirectory that includes all the packages required. Note +this packages' codebase is totally independent from Drupal core's one. + +In the context of this directory, you can then execute the rule tests like +``` +$ vendor/bin/phpunit tests +``` diff --git a/core/tests/PHPStan/Rules/ComponentTestDoesNotExtendCoreTest.php b/core/tests/PHPStan/Rules/ComponentTestDoesNotExtendCoreTest.php new file mode 100644 index 0000000000000000000000000000000000000000..4b5eb9f8062c27ee039056ab00d492d57799f6c2 --- /dev/null +++ b/core/tests/PHPStan/Rules/ComponentTestDoesNotExtendCoreTest.php @@ -0,0 +1,64 @@ +<?php + +declare(strict_types=1); + +// cspell:ignore analyse +namespace Drupal\PHPStan\Rules; + +use Drupal\BuildTests\Framework\BuildTestBase; +use Drupal\KernelTests\KernelTestBase; +use Drupal\Tests\BrowserTestBase; +use Drupal\Tests\UnitTestCase; +use PhpParser\Node; +use PHPStan\Analyser\Scope; +use PHPStan\Node\InClassNode; +use PHPStan\Rules\Rule; +use PHPStan\Rules\RuleErrorBuilder; + +/** + * Ensures that no component tests are extending a core test base class. + * + * @implements Rule<\PHPStan\Node\InClassNode> + * + * @internal + */ +final class ComponentTestDoesNotExtendCoreTest implements Rule { + + /** + * {@inheritdoc} + */ + public function getNodeType(): string { + return InClassNode::class; + } + + /** + * {@inheritdoc} + */ + public function processNode(Node $node, Scope $scope): array { + $class = $node->getClassReflection(); + + if (!str_starts_with($class->getName(), 'Drupal\Tests\Component')) { + return []; + } + + $invalidParents = [ + UnitTestCase::class, + BuildTestBase::class, + KernelTestBase::class, + BrowserTestBase::class, + ]; + + foreach ($invalidParents as $invalidParent) { + if ($class->isSubclassOf($invalidParent)) { + return [ + RuleErrorBuilder::message("Component tests should not extend {$invalidParent}.") + ->line($node->getStartLine()) + ->build(), + ]; + } + } + + return []; + } + +} diff --git a/core/tests/PHPStan/composer.json b/core/tests/PHPStan/composer.json new file mode 100644 index 0000000000000000000000000000000000000000..f59e19499771b4a104c61fc80431f484af18151c --- /dev/null +++ b/core/tests/PHPStan/composer.json @@ -0,0 +1,20 @@ +{ + "name": "drupal/phpstan-testing", + "description": "Tests Drupal core's PHPStan rules", + "require-dev": { + "phpunit/phpunit": "^9", + "phpstan/phpstan": "1.10.66" + }, + "license": "GPL-2.0-or-later", + "autoload": { + "psr-4": { + "Drupal\\PHPStan\\Rules\\": "Rules/", + "Drupal\\BuildTests\\": "../Drupal/BuildTests/", + "Drupal\\FunctionalJavascriptTests\\": "../Drupal/FunctionalJavascriptTests", + "Drupal\\FunctionalTests\\": "../Drupal/FunctionalTests", + "Drupal\\KernelTests\\": "../Drupal/KernelTests/", + "Drupal\\Tests\\": "../Drupal/Tests/" + } + }, + "require": {} +} diff --git a/core/tests/PHPStan/fixtures/component-tests.php b/core/tests/PHPStan/fixtures/component-tests.php new file mode 100644 index 0000000000000000000000000000000000000000..e6a8da7c606c2fc38a7d48b1205f9277e7678754 --- /dev/null +++ b/core/tests/PHPStan/fixtures/component-tests.php @@ -0,0 +1,63 @@ +<?php + +// phpcs:ignoreFile + +declare(strict_types=1); + +namespace Drupal\Tests\Component\Foo { + + use Drupal\BuildTests\Framework\BuildTestBase; + use Drupal\FunctionalJavascriptTests\WebDriverTestBase; + use Drupal\KernelTests\KernelTestBase; + use Drupal\Tests\BrowserTestBase; + use Drupal\Tests\UnitTestCase; + use PHPUnit\Framework\TestCase; + + final class FooTest extends TestCase { + } + + final class UnitTest extends UnitTestCase { + } + + final class BuildTest extends BuildTestBase { + } + + final class KernelTest extends KernelTestBase { + } + + final class FunctionalTest extends BrowserTestBase { + } + + final class FunctionalJavascriptTest extends WebDriverTestBase { + } + +} + +namespace Drupal\Tests\Core\Foo { + + use Drupal\BuildTests\Framework\BuildTestBase; + use Drupal\FunctionalJavascriptTests\WebDriverTestBase; + use Drupal\KernelTests\KernelTestBase; + use Drupal\Tests\BrowserTestBase; + use Drupal\Tests\UnitTestCase; + use PHPUnit\Framework\TestCase; + + final class FooTest extends TestCase { + } + + final class UnitTest extends UnitTestCase { + } + + final class BuildTest extends BuildTestBase { + } + + final class KernelTest extends KernelTestBase { + } + + final class FunctionalTest extends BrowserTestBase { + } + + final class FunctionalJavascriptTest extends WebDriverTestBase { + } + +} diff --git a/core/tests/PHPStan/phpunit.xml.dist b/core/tests/PHPStan/phpunit.xml.dist new file mode 100644 index 0000000000000000000000000000000000000000..7ca6a7c507c2d414e95d52b5f88223d3dd785962 --- /dev/null +++ b/core/tests/PHPStan/phpunit.xml.dist @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="UTF-8"?> +<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + colors="true" + beStrictAboutTestsThatDoNotTestAnything="true" + beStrictAboutOutputDuringTests="true" + beStrictAboutChangesToGlobalState="true" + failOnWarning="true" + cacheResult="false" + xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd"> + <php> + <!-- Set error reporting to E_ALL. --> + <ini name="error_reporting" value="32767"/> + <!-- Do not limit the amount of memory tests take to run. --> + <ini name="memory_limit" value="-1"/> + </php> + <testsuites> + <testsuite name="PHPStan tests"> + <directory>tests</directory> + </testsuite> + </testsuites> + <!-- Settings for coverage reports. --> + <coverage> + <include> + <directory>Rules</directory> + </include> + </coverage> +</phpunit> diff --git a/core/tests/PHPStan/tests/ComponentTestDoesNotExtendCoreTestTest.php b/core/tests/PHPStan/tests/ComponentTestDoesNotExtendCoreTestTest.php new file mode 100644 index 0000000000000000000000000000000000000000..1169f39b8839866d1aff19e69222f975d2d16058 --- /dev/null +++ b/core/tests/PHPStan/tests/ComponentTestDoesNotExtendCoreTestTest.php @@ -0,0 +1,55 @@ +<?php + +declare(strict_types=1); + +// cspell:ignore analyse +namespace Drupal\PHPStan\Tests; + +use Drupal\PHPStan\Rules\ComponentTestDoesNotExtendCoreTest; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; + +/** + * Tests ComponentTestDoesNotExtendCoreTest rule. + */ +class ComponentTestDoesNotExtendCoreTestTest extends RuleTestCase { + + /** + * {@inheritdoc} + */ + protected function getRule(): Rule { + return new ComponentTestDoesNotExtendCoreTest(); + } + + /** + * {@inheritdoc} + */ + public function testRule(): void { + $this->analyse( + [__DIR__ . '/../fixtures/component-tests.php'], + [ + [ + 'Component tests should not extend Drupal\Tests\UnitTestCase.', + 19, + ], + [ + 'Component tests should not extend Drupal\BuildTests\Framework\BuildTestBase.', + 22, + ], + [ + 'Component tests should not extend Drupal\KernelTests\KernelTestBase.', + 25, + ], + [ + 'Component tests should not extend Drupal\Tests\BrowserTestBase.', + 28, + ], + [ + 'Component tests should not extend Drupal\Tests\BrowserTestBase.', + 31, + ], + ] + ); + } + +} diff --git a/core/tests/PHPStan/tests/EnsurePHPStanVersionsMatchTest.php b/core/tests/PHPStan/tests/EnsurePHPStanVersionsMatchTest.php new file mode 100644 index 0000000000000000000000000000000000000000..10c539db91ba0b5ddf8ba79aaf80f21e707986f9 --- /dev/null +++ b/core/tests/PHPStan/tests/EnsurePHPStanVersionsMatchTest.php @@ -0,0 +1,20 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\PHPStan\Tests; + +use PHPUnit\Framework\TestCase; + +/** + * Tests that PHPStan versions match. + */ +class EnsurePHPStanVersionsMatchTest extends TestCase { + + public function testVersions(): void { + $test_composer = json_decode(file_get_contents(__DIR__ . '/../composer.json'), TRUE); + $drupal_composer = json_decode(file_get_contents(__DIR__ . '/../../../../composer/Metapackage/PinnedDevDependencies/composer.json'), TRUE); + $this->assertSame($test_composer['require-dev']['phpstan/phpstan'], $drupal_composer['require']['phpstan/phpstan']); + } + +}