diff --git a/.dev/.ci/check-cs.sh b/.dev/.ci/check-cs.sh index 91db2ae..bfd8f33 100755 --- a/.dev/.ci/check-cs.sh +++ b/.dev/.ci/check-cs.sh @@ -3,9 +3,9 @@ set -e if [[ ${CHECK_CS} == true ]]; then - docker-compose exec -T php composer normalize -d .. --no-update-lock --dry-run + docker compose exec -T php composer normalize -d .. --no-update-lock --dry-run # @TODO There is a weird file access issue on GHA. # Drupal QA: Unable to set 511 visibility for file at /mnt/files/local_mount. - docker-compose exec -T php ./vendor/bin/phpcs -s ./vendor/pronovix/drupal-qa/config/phpcs.xml.dist --ignore="contrib/*,*/build/*" web/modules - docker-compose exec -T php ./vendor/bin/phpstan --no-progress + docker compose exec -T php ./vendor/bin/phpcs -s ./vendor/pronovix/drupal-qa/config/phpcs.xml.dist --ignore="contrib/*,*/build/*" web/modules + docker compose exec -T php ./vendor/bin/phpstan --no-progress fi diff --git a/.dev/.ci/init-env.sh b/.dev/.ci/init-env.sh index 0b91681..2972a98 100755 --- a/.dev/.ci/init-env.sh +++ b/.dev/.ci/init-env.sh @@ -4,29 +4,29 @@ set -e # Lock Drupal core to the expected major version. if [[ -n "${DRUPAL_CORE}" ]]; then - docker-compose exec php composer require -d .. --no-update drupal/core:${DRUPAL_CORE} + docker compose exec php composer require -d .. --no-update drupal/core:${DRUPAL_CORE} fi # We need to run both "install" and "update" commands because: # * `--prefer-lowest` is not supported by "install". # * it seems there is an issue with the merge plugin and because of that if we would only run # `composer update --prefer-lowest` then incorrect lower versions could be installed, ex.: drupal/core:8.5.0 where # there is a drupal/core: ^8.7 constraint. -docker-compose exec php composer install -d .. ${COMPOSER_GLOBAL_OPTIONS} +docker compose exec php composer install -d .. ${COMPOSER_GLOBAL_OPTIONS} if [[ -n "${DEPENDENCIES}" ]]; then # Avoid failing builds caused by "Source directory /mnt/files/local_mount/build/vendor/drupal/coder has uncommitted changes.". - docker-compose exec php composer config -d .. --global discard-changes true - docker-compose exec php composer update -d .. ${COMPOSER_GLOBAL_OPTIONS} ${DEPENDENCIES} -n --with-dependencies + docker compose exec php composer config -d .. --global discard-changes true + docker compose exec php composer update -d .. ${COMPOSER_GLOBAL_OPTIONS} ${DEPENDENCIES} -n --with-dependencies else # Ensure Drupal coding standard is registered. # TODO Check why it gets immediately unregistered after it has been registered # Error: # PHP CodeSniffer Config installed_paths set to ../../drupal/coder/coder_sniffer # PHP CodeSniffer Config installed_paths delete - docker-compose exec php composer update -d .. none + docker compose exec php composer update -d .. none fi # Log the installed versions. -docker-compose exec php composer --version -docker-compose exec php composer show -d .. -f json +docker compose exec php composer --version +docker compose exec php composer show -d .. -f json sudo chown -R travis:travis . ln -s ../../../../drupal-dev/drupal/settings.php build/web/sites/default/settings.php diff --git a/.dev/.ci/run-phpunit-tests.sh b/.dev/.ci/run-phpunit-tests.sh index ac861e1..b1034d0 100755 --- a/.dev/.ci/run-phpunit-tests.sh +++ b/.dev/.ci/run-phpunit-tests.sh @@ -5,5 +5,5 @@ set -e if [[ ${RUN_PHPUNIT_TESTS} == true ]]; then # Custom bootstrap file is required to prevent infinite loop caused by symlinking that Drupal's original # bootstrap file cannot handle. - docker-compose run --rm php ./vendor/bin/phpunit -c web/core -v --debug --printer '\Drupal\Tests\Listeners\HtmlOutputPrinter' --bootstrap=vendor/pronovix/drupal-qa/src/Drupal/PhpUnit/bootstrap.php web/modules/drupal_module/tests/ + docker compose run --rm php ./vendor/bin/phpunit -c web/core -v --debug --printer '\Drupal\Tests\Listeners\HtmlOutputPrinter' --bootstrap=/mnt/files/local_mount/tests/src/bootstrap.php web/modules/drupal_module/tests/ fi diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a7f855f..4849178 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -32,13 +32,13 @@ jobs: env: PHP_IMAGE: "wodby/drupal-php:8.1-dev" run: | - docker-compose pull --quiet - docker-compose up -d --build + docker compose pull --quiet + docker compose up -d --build - name: Install dependencies run: | chmod a+rw . - docker-compose exec -T php composer install --no-interaction --no-suggest --no-progress -d .. + docker compose exec -T php composer install --no-interaction --no-suggest --no-progress -d .. - name: Run code style check run: CHECK_CS="true" ./.dev/.ci/check-cs.sh @@ -48,10 +48,10 @@ jobs: strategy: fail-fast: false matrix: - php_image: ["wodby/drupal-php:8.1-dev"] + php_image: ["wodby/drupal-php:8.1-dev", "wodby/drupal-php:8.2-dev"] db_image: ["wodby/mariadb:10.5"] # TODO Get version range from composer.json dynamically. - drupal_version: ["~9.4.0", "~9.5.0", "^10.0"] + drupal_version: ["^10.1"] lowest_highest: ["--prefer-lowest", ""] steps: @@ -64,6 +64,12 @@ jobs: repository: Pronovix/docker-drupal-dev path: ./drupal-dev + # TODO Remove this after Drupal QA got a PHP 8.2 compatible release. + - name: Drupal QA workaround + if: matrix.php_image == 'wodby/drupal-php:8.2-dev' + run: | + export COMPOSER_IGNORE_PLATFORM_REQ=php+ + - name: Setup environment run: | mkdir build @@ -77,19 +83,19 @@ jobs: PHP_IMAGE: ${{ matrix.php_image }} DB_IMAGE: ${{ matrix.db_image }} run: | - docker-compose pull --quiet - docker-compose up -d --build + docker compose pull --quiet + docker compose up -d --build - name: Install dependencies run: | chmod a+rw . - docker-compose exec -T php composer install --no-interaction --no-suggest --no-progress -d .. + docker compose exec -T php composer install --no-interaction --no-suggest --no-progress -d .. chmod a+rw composer.json - docker-compose exec -T php composer require drupal/core:${{ matrix.drupal_version }} drupal/core-dev:${{ matrix.drupal_version }} drupal/core-recommended:${{ matrix.drupal_version }} drupal/core-composer-scaffold:${{ matrix.drupal_version }} --no-update -d .. - docker-compose exec --env COMPOSER_DISCARD_CHANGES=true -T php composer update --no-progress ${{ matrix.lowest_highest }} -d .. + docker compose exec -T php composer require drupal/core:${{ matrix.drupal_version }} drupal/core-dev:${{ matrix.drupal_version }} drupal/core-recommended:${{ matrix.drupal_version }} drupal/core-composer-scaffold:${{ matrix.drupal_version }} --no-update -d .. + docker compose exec --env COMPOSER_DISCARD_CHANGES=true -T php composer update --no-progress ${{ matrix.lowest_highest }} -d .. - name: List installed dependencies - run: docker-compose exec -T php composer show -d .. + run: docker compose exec -T php composer show -d .. - name: Set up Drupal settings files run: | diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index c5af75c..1f5aacb 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -9,15 +9,15 @@ $ git clone https://github.com/Pronovix/docker-drupal-dev.git drupal-dev $ mkdir build; $ ln -s drupal-dev/docker-compose.yml $ ln -s drupal-dev/Dockerfile -$ printf "COMPOSE_PROJECT_NAME=swagger_ui_formatter\nPHP_IMAGE=wodby/drupal-php:7.4-dev\n#You can find examples for available customization in the drupal-dev/examples/.env file.\n" > .env && source .env -$ docker-compose up -d --build -$ docker-compose exec php composer install +$ printf "COMPOSE_PROJECT_NAME=swagger_ui_formatter\n#You can find examples for available customization in the drupal-dev/examples/.env file.\n" > .env && source .env +$ docker compose up -d --build +$ docker compose exec php composer install $ ln -s ../../../../drupal-dev/drupal/settings.php build/web/sites/default/settings.php $ ln -s ../../../../drupal-dev/drupal/settings.shared.php build/web/sites/default/settings.shared.php $ ln -s ../../../../drupal-dev/drupal/settings.testing.php build/web/sites/default/settings.testing.php $ ln -s ../../../drupal-dev/drupal/development.services.yml.dist build/web/sites/development.services.yml.dist -$ docker-compose exec php drush si -y -$ docker-compose exec php drush en swagger_ui_formatter -y +$ docker compose exec php vendor/bin/drush si -y +$ docker compose exec php vendor/bin/drush en swagger_ui_formatter -y ``` ## QA @@ -25,14 +25,14 @@ $ docker-compose exec php drush en swagger_ui_formatter -y ### Code-style checks ```sh -$ docker-compose exec php composer normalize --indent-size=4 --indent-style=space --no-update-lock -$ docker-compose exec php ./vendor/bin/phpcbf -s ../phpcs.xml.dist --ignore="contrib/*" web/modules -$ docker-compose exec php ./vendor/bin/phpcs -s ../phpcs.xml.dist --ignore="contrib/*" web/modules -$ docker-compose exec php ./vendor/bin/drupal-check --drupal-root=. -e "*/build/*" .. +$ docker compose exec php composer normalize --indent-size=4 --indent-style=space --no-update-lock +$ docker compose exec php ./vendor/bin/phpcbf -s ../phpcs.xml.dist --ignore="contrib/*" web/modules +$ docker compose exec php ./vendor/bin/phpcs -s ../phpcs.xml.dist --ignore="contrib/*" web/modules +$ docker compose exec php ./vendor/bin/phpstan --no-progress .. ``` ### Running tests ```sh -$ docker-compose run --rm php ./vendor/bin/phpunit -c web/core -v --debug --printer '\Drupal\Tests\Listeners\HtmlOutputPrinter' --bootstrap=vendor/pronovix/drupal-qa/src/Drupal/PhpUnit/bootstrap.php web/modules/drupal_module/tests/ +$ docker compose run --rm php ./vendor/bin/phpunit -c web/core -v --debug --printer '\Drupal\Tests\Listeners\HtmlOutputPrinter' --bootstrap=/mnt/files/local_mount/tests/src/bootstrap.php web/modules/drupal_module/tests/ ``` diff --git a/composer.json b/composer.json index cd6af31..7c354be 100644 --- a/composer.json +++ b/composer.json @@ -19,19 +19,18 @@ "league/container version contraint is here to prevent randomy failed highest-lowest tests caused by this missing fix: https://github.com/thephpleague/container/commit/97a0c39bf37d709d3bbc31d0505cea9373d927e7" ], "require": { - "php": "~8.1.0", - "drupal/core": "^9.4 || ^10.0" + "php": "~8.1.0 || ~8.2.0", + "drupal/core": "^10.1" }, "require-dev": { - "bower-asset/swagger-ui": "^4.15.0", - "composer/installers": "^v2.2.0", - "drupal/core-composer-scaffold": "^9.4 || ^10.0.0", - "drupal/core-dev": "^9.4 || ^10.0.0", - "drupal/core-recommended": "^9.4 || ^10.0.0", - "drupal/devel": "^5.1.1", - "league/container": "<4.0.0 || >=4.1.1", - "pronovix/drupal-qa": "^3.11.1", - "pronovix/simple-symlink": "^3.11.1", + "bower-asset/swagger-ui": "^4.17.0", + "composer/installers": "^2.2.0", + "drupal/core-composer-scaffold": "^10.1", + "drupal/core-dev": "^10.1", + "drupal/core-recommended": "^10.1", + "drupal/devel": "^5.2.1", + "league/container": "<4.0.0 || >=4.2.2", + "pronovix/drupal-qa": "^4", "zaporylie/composer-drupal-optimizations": "^1.2" }, "repositories": [ @@ -60,15 +59,19 @@ "config": { "allow-plugins": { "composer/installers": true, - "drupal/core-composer-scaffold": true, "cweagans/composer-patches": true, - "zaporylie/composer-drupal-optimizations": true, "dealerdirect/phpcodesniffer-composer-installer": true, + "drupal/core-composer-scaffold": true, "ergebnis/composer-normalize": true, + "php-http/discovery": false, + "phpstan/extension-installer": false, "pronovix/drupal-qa": true, - "phpstan/extension-installer": false + "zaporylie/composer-drupal-optimizations": true }, "optimize-autoloader": true, + "platform": { + "php": "8.1.6" + }, "sort-packages": true, "vendor-dir": "build/vendor" }, @@ -139,10 +142,16 @@ }, "scripts": { "post-install-cmd": [ - "Pronovix\\SimpleSymlink\\ScriptHandler::createSymlinks" + "@symlink-for-local-dev-env" ], "post-update-cmd": [ - "Pronovix\\SimpleSymlink\\ScriptHandler::createSymlinks" + "@symlink-for-local-dev-env" + ], + "symlink-for-local-dev-env": [ + "@php -r \"@symlink('../../..', 'build/web/modules/drupal_module');\"", + "@php -r \"@symlink('../phpcs.xml.dist', 'build/phpcs.xml.dist');\"", + "@php -r \"@symlink('../phpstan-baseline.neon', 'build/phpstan-baseline.neon');\"", + "@php -r \"@symlink('../phpstan.neon.dist', 'build/phpstan.neon.dist');\"" ] } } diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index aab4991..8398789 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1,2 +1,24 @@ parameters: - ignoreErrors: [] + ignoreErrors: + - + message: """ + #^Call to method __construct\\(\\) of deprecated class Drupal\\\\Core\\\\Extension\\\\Discovery\\\\RecursiveExtensionFilterIterator\\: + in drupal\\:10\\.2\\.0 and is removed from drupal\\:11\\.0\\.0\\. Use + \\\\Drupal\\\\Core\\\\Extension\\\\Discovery\\\\RecursiveExtensionFilterCallback instead\\.$# + """ + count: 1 + path: ../tests/src/DrupalExtensionFilterIterator.php + + - + message: """ + #^Class Drupal\\\\Tests\\\\swagger_ui_formatter\\\\DrupalExtensionFilterIterator extends deprecated class Drupal\\\\Core\\\\Extension\\\\Discovery\\\\RecursiveExtensionFilterIterator\\: + in drupal\\:10\\.2\\.0 and is removed from drupal\\:11\\.0\\.0\\. Use + \\\\Drupal\\\\Core\\\\Extension\\\\Discovery\\\\RecursiveExtensionFilterCallback instead\\.$# + """ + count: 1 + path: ../tests/src/DrupalExtensionFilterIterator.php + + - + message: "#^Parameter \\#2 \\$callback of function array_filter expects \\(callable\\(string\\|false\\)\\: bool\\)\\|null, 'file_exists' given\\.$#" + count: 1 + path: ../tests/src/bootstrap.php diff --git a/tests/src/DrupalExtensionFilterIterator.php b/tests/src/DrupalExtensionFilterIterator.php new file mode 100644 index 0000000..2b6ca68 --- /dev/null +++ b/tests/src/DrupalExtensionFilterIterator.php @@ -0,0 +1,51 @@ + ../../../ + * └── my_module.info.yml + * + * Related issues on Drupal.org: + * https://www.drupal.org/project/drupal/issues/2943172 + * https://www.drupal.org/project/drupal/issues/3050881 + */ +final class DrupalExtensionFilterIterator extends DrupalRecursiveExtensionFilterIterator +{ + /** + * DrupalExtensionFilterIterator constructor. + * + * @param \RecursiveIterator $iterator + * The iterator to filter. + */ + public function __construct(\RecursiveIterator $iterator) + { + // We should not initialize Settings here to retrieve + // `file_scan_ignore_directories` here although that would remove some + // code duplications. + parent::__construct($iterator, ['build', 'node_modules', 'bower_components']); + } +} diff --git a/tests/src/bootstrap.php b/tests/src/bootstrap.php new file mode 100644 index 0000000..2c11d8e --- /dev/null +++ b/tests/src/bootstrap.php @@ -0,0 +1,181 @@ +acceptTests(TRUE); + $dirs = new \RecursiveIteratorIterator($filter); + foreach ($dirs as $dir) { + if (strpos($dir->getPathname(), '.info.yml') !== FALSE) { + // Cut off ".info.yml" from the filename for use as the extension name. We + // use getRealPath() so that we can scan extensions represented by + // directory aliases. + $extensions[substr($dir->getFilename(), 0, -9)] = $dir->getPathInfo() + ->getRealPath(); + } + } + return $extensions; +} + +/** + * Returns directories under which contributed extensions may exist. + * + * @param string $root + * (optional) Path to the root of the Drupal installation. + * + * @return array + * An array of directories under which contributed extensions may exist. + */ +function drupal_phpunit_contrib_extension_directory_roots($root = NULL) { + if ($root === NULL) { + $root = dirname(__DIR__, 2); + } + $paths = [ + $root . '/core', + $root . '/', + ]; + $sites_path = $root . '/sites'; + // Note this also checks sites/../modules and sites/../profiles. + foreach (scandir($sites_path) ?: [] as $site) { + if ($site[0] === '.' || $site === 'simpletest') { + continue; + } + $path = "$sites_path/$site"; + $paths[] = is_dir("$path/modules") ? realpath("$path/modules") : ''; + $paths[] = is_dir("$path/profiles") ? realpath("$path/profiles") : ''; + $paths[] = is_dir("$path/themes") ? realpath("$path/themes") : ''; + } + return array_filter($paths, 'file_exists'); +} + +/** + * Registers the namespace for each extension directory with the autoloader. + * + * @param array $dirs + * An associative array of extension directories, keyed by extension name. + * + * @return array + * An associative array of extension directories, keyed by their namespace. + */ +function drupal_phpunit_get_extension_namespaces($dirs) { + $suite_names = ['Unit', 'Kernel', 'Functional', 'Build', 'FunctionalJavascript']; + $namespaces = []; + foreach ($dirs as $extension => $dir) { + if (is_dir($dir . '/src')) { + // Register the PSR-4 directory for module-provided classes. + $namespaces['Drupal\\' . $extension . '\\'][] = $dir . '/src'; + } + $test_dir = $dir . '/tests/src'; + if (is_dir($test_dir)) { + foreach ($suite_names as $suite_name) { + $suite_dir = $test_dir . '/' . $suite_name; + if (is_dir($suite_dir)) { + // Register the PSR-4 directory for PHPUnit-based suites. + $namespaces['Drupal\\Tests\\' . $extension . '\\' . $suite_name . '\\'][] = $suite_dir; + } + } + // Extensions can have a \Drupal\Tests\extension\Traits namespace for + // cross-suite trait code. + $trait_dir = $test_dir . '/Traits'; + if (is_dir($trait_dir)) { + $namespaces['Drupal\\Tests\\' . $extension . '\\Traits\\'][] = $trait_dir; + } + } + } + return $namespaces; +} + +// We define the COMPOSER_INSTALL constant, so that PHPUnit knows where to +// autoload from. This is needed for tests run in isolation mode, because +// phpunit.xml.dist is located in a non-default directory relative to the +// PHPUnit executable. +if (!defined('PHPUNIT_COMPOSER_INSTALL')) { + define('PHPUNIT_COMPOSER_INSTALL', __DIR__ . '/../../autoload.php'); +} + +/** + * Populate class loader with additional namespaces for tests. + * + * We run this in a function to avoid setting the class loader to a global + * that can change. This change can cause unpredictable false positives for + * phpunit's global state change watcher. The class loader can be retrieved from + * composer at any time by requiring autoload.php. + */ +function drupal_phpunit_populate_class_loader(): ClassLoader { + $webroot = dirname(__DIR__, 2) . '/build/web'; + /** @var \Composer\Autoload\ClassLoader $loader */ + $loader = require "{$webroot}/autoload.php"; + $core_tests_dir = "{$webroot}/core/tests"; + + // Start with classes in known locations. + $loader->add('Drupal\\Tests', $core_tests_dir); + $loader->add('Drupal\\TestSite', $core_tests_dir); + $loader->add('Drupal\\KernelTests', $core_tests_dir); + $loader->add('Drupal\\FunctionalTests', $core_tests_dir); + $loader->add('Drupal\\FunctionalJavascriptTests', $core_tests_dir); + $loader->add('Drupal\\TestTools', $core_tests_dir); + + if (!isset($GLOBALS['namespaces'])) { + // Scan for arbitrary extension namespaces from core and contrib. + $extension_roots = drupal_phpunit_contrib_extension_directory_roots($webroot); + + $dirs = array_map('drupal_phpunit_find_extension_directories', $extension_roots); + $dirs = array_reduce($dirs, 'array_merge', []); + $GLOBALS['namespaces'] = drupal_phpunit_get_extension_namespaces($dirs); + } + foreach ($GLOBALS['namespaces'] as $prefix => $paths) { + $loader->addPsr4($prefix, $paths); + } + + return $loader; +} + +// Do class loader population. +$loader = drupal_phpunit_populate_class_loader(); + +// Set sane locale settings, to ensure consistent string, dates, times and +// numbers handling. +// @see \Drupal\Core\DrupalKernel::bootEnvironment() +setlocale(LC_ALL, 'C'); + +// Set appropriate configuration for multi-byte strings. +mb_internal_encoding('utf-8'); +mb_language('uni'); + +// Set the default timezone. While this doesn't cause any tests to fail, PHP +// complains if 'date.timezone' is not set in php.ini. The Australia/Sydney +// timezone is chosen so all tests are run using an edge case scenario (UTC+10 +// and DST). This choice is made to prevent timezone related regressions and +// reduce the fragility of the testing system in general. +date_default_timezone_set('Australia/Sydney'); + +// Ensure ignored deprecation patterns listed in .deprecation-ignore.txt are +// considered in testing. +if (getenv('SYMFONY_DEPRECATIONS_HELPER') === FALSE) { + $deprecation_ignore_filename = realpath(__DIR__ . "/../.deprecation-ignore.txt"); + putenv("SYMFONY_DEPRECATIONS_HELPER=ignoreFile=$deprecation_ignore_filename"); +} + +// Drupal expects to be run from its root directory. This ensures all test types +// are consistent. +chdir(dirname(__DIR__, 2) . '/build/web');