Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,23 @@

All Notable changes to the **Quality Assurance - Drupal** package.

## [5.0.3]

### Fixed

- Select the PHPUnit 11 or 12 configuration from the consumer project's
Composer metadata instead of the PHPUnit version bundled in GrumPHP.
- Preserve symlinked extension discovery while preventing recursive traversal
through directory-link cycles.
- Bound PHPUnit source exclusions to extension source paths so coverage
mapping no longer traverses Composer dependencies or follows recursive
repository links.

### Changed

- Require QA PHP 3.1.1 or later so generated PHPStan configurations include
the GrumPHP API stubs required by the shared event listener.

## [5.0.2]

### Fixed
Expand Down Expand Up @@ -193,6 +210,7 @@ Older releases (2.x.x) see [CHANGELOG-2.x.md](CHANGELOG-1.x.md).

Older releases (1.x.x) see [CHANGELOG-1.x.md](CHANGELOG-1.x.md).

[5.0.3]: https://github.com/district09/php_package_qa-drupal/compare/5.0.2...5.0.3
[5.0.2]: https://github.com/district09/php_package_qa-drupal/compare/5.0.1...5.0.2
[5.0.1]: https://github.com/district09/php_package_qa-drupal/compare/5.0.0...5.0.1
[5.0.0]: https://github.com/district09/php_package_qa-drupal/compare/4.1.4...5.0.0
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
"php": "^8.3",
"behat/mink": "^1.13",
"behat/mink-browserkit-driver": "^2.3",
"district09/qa-php": "^3.0.3",
"district09/qa-php": "^3.1.1",
"drupal/coder": "^9.0",
"drupal/core": "^11.4 || ^12.0",
"friendsoftwig/twigcs": "^6.5",
Expand Down
18 changes: 12 additions & 6 deletions configs/phpunit-extension-11.xml
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,18 @@
<directory suffix=".php">./modules/**/src</directory>
</include>
<exclude>
<directory suffix=".api.php">./</directory>
<directory suffix="Spy.php">./</directory>
<directory suffix="Stub.php">./</directory>
<directory suffix="Test.php">./</directory>
<directory suffix="TestBase.php">./</directory>
<directory suffix="TestCase.php">./</directory>
<directory suffix=".api.php">./src</directory>
<directory suffix="Spy.php">./src</directory>
<directory suffix="Stub.php">./src</directory>
<directory suffix="Test.php">./src</directory>
<directory suffix="TestBase.php">./src</directory>
<directory suffix="TestCase.php">./src</directory>
<directory suffix=".api.php">./modules/**/src</directory>
<directory suffix="Spy.php">./modules/**/src</directory>
<directory suffix="Stub.php">./modules/**/src</directory>
<directory suffix="Test.php">./modules/**/src</directory>
<directory suffix="TestBase.php">./modules/**/src</directory>
<directory suffix="TestCase.php">./modules/**/src</directory>
</exclude>
</source>
</phpunit>
18 changes: 12 additions & 6 deletions configs/phpunit-extension.xml
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,18 @@
<directory suffix=".php">./modules/**/src</directory>
</include>
<exclude>
<directory suffix=".api.php">./</directory>
<directory suffix="Spy.php">./</directory>
<directory suffix="Stub.php">./</directory>
<directory suffix="Test.php">./</directory>
<directory suffix="TestBase.php">./</directory>
<directory suffix="TestCase.php">./</directory>
<directory suffix=".api.php">./src</directory>
<directory suffix="Spy.php">./src</directory>
<directory suffix="Stub.php">./src</directory>
<directory suffix="Test.php">./src</directory>
<directory suffix="TestBase.php">./src</directory>
<directory suffix="TestCase.php">./src</directory>
<directory suffix=".api.php">./modules/**/src</directory>
<directory suffix="Spy.php">./modules/**/src</directory>
<directory suffix="Stub.php">./modules/**/src</directory>
<directory suffix="Test.php">./modules/**/src</directory>
<directory suffix="TestBase.php">./modules/**/src</directory>
<directory suffix="TestCase.php">./modules/**/src</directory>
</exclude>
</source>
</phpunit>
27 changes: 21 additions & 6 deletions configs/phpunit-site-11.xml
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,27 @@
<directory suffix=".php">web/themes/custom</directory>
</include>
<exclude>
<directory suffix=".api.php">./</directory>
<directory suffix="Spy.php">./</directory>
<directory suffix="Stub.php">./</directory>
<directory suffix="Test.php">./</directory>
<directory suffix="TestBase.php">./</directory>
<directory suffix="TestCase.php">./</directory>
Comment on lines -70 to -75

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we remove these?
This prevents PHPUnit wanting to have code coverage for test Fixtures and base test classes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right. The root-wide exclusions were removed to stop PHPUnit traversing the project and vendor tree, but the exclusions themselves are still needed. They are now restored and bounded to the custom module, profile, and theme roots, so fixtures, stubs, and test base classes remain outside coverage without reintroducing recursive scanning.

<directory suffix=".api.php">web/modules/custom</directory>
<directory suffix=".api.php">web/profiles/custom</directory>
<directory suffix=".api.php">web/themes/custom</directory>
<directory suffix="Spy.php">web/modules/custom</directory>
<directory suffix="Spy.php">web/profiles/custom</directory>
<directory suffix="Spy.php">web/themes/custom</directory>
<directory suffix="Stub.php">web/modules/custom</directory>
<directory suffix="Stub.php">web/profiles/custom</directory>
<directory suffix="Stub.php">web/themes/custom</directory>
<directory suffix="Test.php">web/modules/custom</directory>
<directory suffix="Test.php">web/profiles/custom</directory>
<directory suffix="Test.php">web/themes/custom</directory>
<directory suffix="TestBase.php">web/modules/custom</directory>
<directory suffix="TestBase.php">web/profiles/custom</directory>
<directory suffix="TestBase.php">web/themes/custom</directory>
<directory suffix="TestCase.php">web/modules/custom</directory>
<directory suffix="TestCase.php">web/profiles/custom</directory>
<directory suffix="TestCase.php">web/themes/custom</directory>
<directory>web/modules/custom/**/tests</directory>
<directory>web/profiles/custom/**/tests</directory>
<directory>web/themes/custom/**/tests</directory>
<directory>web/themes/custom/*/source</directory>
</exclude>
</source>
Expand Down
27 changes: 21 additions & 6 deletions configs/phpunit-site.xml
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,27 @@
<directory suffix=".php">web/themes/custom</directory>
</include>
<exclude>
<directory suffix=".api.php">./</directory>
<directory suffix="Spy.php">./</directory>
<directory suffix="Stub.php">./</directory>
<directory suffix="Test.php">./</directory>
<directory suffix="TestBase.php">./</directory>
<directory suffix="TestCase.php">./</directory>
Comment on lines -70 to -75

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we remove these?
This prevents PHPUnit wanting to have code coverage for test Fixtures and base test classes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, see previous comments

<directory suffix=".api.php">web/modules/custom</directory>
<directory suffix=".api.php">web/profiles/custom</directory>
<directory suffix=".api.php">web/themes/custom</directory>
<directory suffix="Spy.php">web/modules/custom</directory>
<directory suffix="Spy.php">web/profiles/custom</directory>
<directory suffix="Spy.php">web/themes/custom</directory>
<directory suffix="Stub.php">web/modules/custom</directory>
<directory suffix="Stub.php">web/profiles/custom</directory>
<directory suffix="Stub.php">web/themes/custom</directory>
<directory suffix="Test.php">web/modules/custom</directory>
<directory suffix="Test.php">web/profiles/custom</directory>
<directory suffix="Test.php">web/themes/custom</directory>
<directory suffix="TestBase.php">web/modules/custom</directory>
<directory suffix="TestBase.php">web/profiles/custom</directory>
<directory suffix="TestBase.php">web/themes/custom</directory>
<directory suffix="TestCase.php">web/modules/custom</directory>
<directory suffix="TestCase.php">web/profiles/custom</directory>
<directory suffix="TestCase.php">web/themes/custom</directory>
<directory>web/modules/custom/**/tests</directory>
<directory>web/profiles/custom/**/tests</directory>
<directory>web/themes/custom/**/tests</directory>
<directory>web/themes/custom/*/source</directory>
</exclude>
</source>
Expand Down
13 changes: 10 additions & 3 deletions src/GrumPHP/ConfigFileMerger.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
use GrumPHP\Task\Phpunit;
use GrumPHP\Task\TaskInterface;
use Nette\Neon\Neon;
use PHPUnit\Runner\Version;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Yaml\Yaml;

Expand All @@ -35,6 +34,14 @@ final class ConfigFileMerger
private const FILETYPE_YAML = 'yaml';
private const FILETYPE_NEON = 'neon';

/**
* PHPUnit configuration suffixes by supported runner major.
*/
private const PHPUNIT_TEMPLATE_SUFFIXES = [
11 => '-11',
12 => '',
];

/**
* Mapping of task types and their configuration files.
*
Expand Down Expand Up @@ -179,8 +186,8 @@ private function getConfigFileCandidates(
$packageTypeFilename = $taskInfo['filename'] . '-' . $type;

// PHPUnit configuration schemas are specific to the runner major.
if ($taskInfo['filename'] === 'phpunit' && Version::majorVersionNumber() === 11) {
$packageTypeFilename .= '-11';
if ($taskInfo['filename'] === 'phpunit') {
$packageTypeFilename .= self::PHPUNIT_TEMPLATE_SUFFIXES[PhpunitVersionResolver::installedMajorVersion()];
}

return [
Expand Down
43 changes: 43 additions & 0 deletions src/GrumPHP/PhpunitVersionResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

declare(strict_types=1);

namespace Digipolisgent\QA\Drupal\GrumPHP;

use Composer\InstalledVersions;

/**
* Resolves the PHPUnit major installed in the consumer project.
*
* @internal
*/
final class PhpunitVersionResolver {

/**
* Gets the installed PHPUnit major version.
*/
public static function installedMajorVersion(): int {
$version = InstalledVersions::getVersion('phpunit/phpunit');
if ($version === NULL) {
throw new \LogicException('Unable to determine the installed PHPUnit version.');
}

if (!preg_match('/^(?:v)?(?<major>\d+)/', $version, $matches)) {
throw new \LogicException(sprintf(
'Unable to determine the PHPUnit major version from "%s".',
$version,
));
}

$major = (int) $matches['major'];
if (!in_array($major, [11, 12], TRUE)) {
throw new \LogicException(sprintf(
'Unsupported PHPUnit major version %d. Supported versions are 11 and 12.',
$major,
));
}

return $major;
}

}
25 changes: 22 additions & 3 deletions src/PHPUnit/Extension/bootstrap.php
Original file line number Diff line number Diff line change
Expand Up @@ -71,14 +71,33 @@ function drupal_phpunit_core_dir(): string {
*/
function drupal_phpunit_find_extension_directories(string $scanDirectory): array {
$extensions = [];
$visitedDirectories = [];
$realScanDirectory = realpath($scanDirectory);
if ($realScanDirectory !== FALSE) {
$visitedDirectories[$realScanDirectory] = TRUE;
}

$iterator = new RecursiveCallbackFilterIterator(
new RecursiveDirectoryIterator(
$scanDirectory,
RecursiveDirectoryIterator::FOLLOW_SYMLINKS | RecursiveDirectoryIterator::SKIP_DOTS

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we remove symlinks?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. Removing FOLLOW_SYMLINKS avoided a recursive link, but it also removed valid linked-extension support. I changed this to retain symlink traversal while tracking canonical directory paths and skipping only paths already visited, including cycles.

),
static function (SplFileInfo $file): bool {
return !$file->isDir()
|| !in_array($file->getFilename(), ['.git', 'node_modules', 'vendor'], TRUE);
static function (SplFileInfo $file) use (&$visitedDirectories): bool {
if (!$file->isDir()) {
return TRUE;
}

if (in_array($file->getFilename(), ['.git', 'node_modules', 'vendor'], TRUE)) {
return FALSE;
}

$realPath = $file->getRealPath();
if ($realPath === FALSE || isset($visitedDirectories[$realPath])) {
return FALSE;
}

$visitedDirectories[$realPath] = TRUE;
return TRUE;
}
);
$directories = new RecursiveIteratorIterator(
Expand Down
58 changes: 58 additions & 0 deletions tests/src/Compatibility/BootstrapCompatibilityTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,62 @@ public function testDrupalTestNamespacesAreAvailable(): void {
self::assertTrue(class_exists('Behat\\Mink\\Element\\DocumentElement'));
}

/**
* Tests that extension discovery follows symlinked extension directories.
*/
public function testExtensionDiscoveryFollowsDirectorySymlinks(): void {
$directory = sys_get_temp_dir() . '/qa-drupal-bootstrap-' . bin2hex(random_bytes(8));
$extensionDirectory = $directory . '-extension';
mkdir($directory, 0777, TRUE);
mkdir($extensionDirectory, 0777, TRUE);
file_put_contents($extensionDirectory . '/example.info.yml', "name: Example\ntype: module\n");

if (!symlink($extensionDirectory, $directory . '/example')) {
unlink($extensionDirectory . '/example.info.yml');
rmdir($extensionDirectory);
rmdir($directory);
self::markTestSkipped('Directory symlinks are not supported.');
}

try {
$extensions = drupal_phpunit_find_extension_directories($directory);
self::assertSame(realpath($extensionDirectory), $extensions['example']);
}
finally {
unlink($directory . '/example');
unlink($extensionDirectory . '/example.info.yml');
rmdir($extensionDirectory);
rmdir($directory);
}
}

/**
* Tests that extension discovery skips directory symlink cycles.
*/
public function testExtensionDiscoverySkipsDirectorySymlinkCycles(): void {
$directory = sys_get_temp_dir() . '/qa-drupal-bootstrap-' . bin2hex(random_bytes(8));
$extensionDirectory = $directory . '/example';
mkdir($extensionDirectory, 0777, TRUE);
file_put_contents($extensionDirectory . '/example.info.yml', "name: Example\ntype: module\n");

if (!symlink($directory, $extensionDirectory . '/loop')) {
unlink($extensionDirectory . '/example.info.yml');
rmdir($extensionDirectory);
rmdir($directory);
self::markTestSkipped('Directory symlinks are not supported.');
}

try {
$extensions = drupal_phpunit_find_extension_directories($directory);
self::assertSame(realpath($extensionDirectory), $extensions['example']);
self::assertCount(1, $extensions);
}
finally {
unlink($extensionDirectory . '/loop');
unlink($extensionDirectory . '/example.info.yml');
rmdir($extensionDirectory);
rmdir($directory);
}
}

}
Loading
Loading