diff --git a/.readme-partials/USING.md b/.readme-partials/USING.md index f9857143..2f8f7b6f 100644 --- a/.readme-partials/USING.md +++ b/.readme-partials/USING.md @@ -278,7 +278,7 @@ WP_CLI_TEST_DOCKER=auto WP_CLI_TEST_PARALLEL=4 composer behat Docker is not used on Windows, when collecting code coverage, when Docker is not available, when the image cannot be built or when the database server cannot be identified. -Each worker gets its own network, temporary directory and database server, of the same type and version as the one configured through the `WP_CLI_TEST_DB*` variables (or set the image through `WP_CLI_TEST_DOCKER_DB`, e.g. `mysql:8.0`). Nothing is shared between workers, so they can run any scenario. The database servers keep their data in memory. Scenarios selected by path or name run in a single container. +Each worker gets its own network, temporary directory and database server, of the same type and version as the one configured through the `WP_CLI_TEST_DB*` variables (or set the image through `WP_CLI_TEST_DOCKER_DB`, e.g. `mysql:8.0`, so that the host needs neither a database server nor a client). Nothing is shared between workers, so they can run any scenario. The database servers keep their data in memory. Scenarios selected by path or name run in a single container. The containers run the same PHP version as the host, unless `WP_CLI_TEST_DOCKER_PHP` sets another one: ```bash diff --git a/bin/run-behat-tests b/bin/run-behat-tests index c86c8214..ff23541d 100755 --- a/bin/run-behat-tests +++ b/bin/run-behat-tests @@ -27,9 +27,50 @@ is_numeric() { esac } +# With WP_CLI_TEST_DOCKER set, the scenarios run in throwaway containers instead, every worker in +# its own, with a database server of the same type and version, so they share nothing. +# "auto" runs them without Docker where it cannot be used, "1" (or "yes", "true", "on") fails instead. +case "${WP_CLI_TEST_DOCKER-}" in + ""|0|no|false|off) DOCKER="";; + auto) DOCKER="auto";; + 1|yes|true|on) DOCKER="force";; + *) + echo "Warning: WP_CLI_TEST_DOCKER is neither \"auto\" nor a yes or no, running the scenarios without Docker." >&2 + DOCKER="";; +esac + +without_docker() { + if [ "${DOCKER}" = "force" ]; then + echo "Error: $1 Set WP_CLI_TEST_DOCKER to \"auto\" to run the scenarios without Docker in that case." >&2 + exit 1 + fi + echo "$1 Running the scenarios without Docker." >&2 + DOCKER="" +} + +case "$(uname -s)" in + MINGW*|MSYS*|CYGWIN*) + if [ -n "${DOCKER}" ]; then + without_docker "The scenarios cannot run in Docker containers on Windows." + fi;; +esac + +if [ -n "${DOCKER}" ]; then + if ! command -v docker > /dev/null 2>&1 || ! docker info > /dev/null 2>&1; then + without_docker "Docker is not available." + elif [[ "${WP_CLI_TEST_COVERAGE}" == "true" ]]; then + without_docker "Code coverage cannot be collected in Docker containers." + fi +fi + +export WP_CLI_TEST_DOCKER="${DOCKER}" + # If DB type is already set to SQLite, there's nothing to do. +# Neither is there when the containers bring their own database server of a given image. if [ "${WP_CLI_TEST_DBTYPE-}" = "sqlite" ]; then echo "WP_CLI_TEST_DBTYPE is set to 'sqlite', skipping database check." +elif [ -n "${DOCKER}" ] && [ -n "${WP_CLI_TEST_DOCKER_DB-}" ]; then + echo "The database servers run in Docker containers of ${WP_CLI_TEST_DOCKER_DB}, skipping database check." else # Check for database client and connectivity. DB_CLIENT="" @@ -90,6 +131,9 @@ else fi fi +# The version as requested, before it is resolved below, for a run that starts over without Docker. +export WP_CLI_TEST_REQUESTED_WP_VERSION="${WP_VERSION-}" + # When installing from an arbitrary archive, there is no version to resolve against # WordPress.org. Default to "trunk" so that no @require-wp tags are filtered out, as # such an archive is usually a development build. Set WP_VERSION explicitly alongside @@ -276,44 +320,10 @@ done # to be allowed to create, as set up by "composer prepare-tests". PARALLEL="${WP_CLI_TEST_PARALLEL-}" -# With WP_CLI_TEST_DOCKER set, the scenarios run in throwaway containers instead, every worker in -# its own, with a database server of the same type and version, so they share nothing. -# "auto" runs them without Docker where it cannot be used, "1" (or "yes", "true", "on") fails instead. -case "${WP_CLI_TEST_DOCKER-}" in - ""|0|no|false|off) DOCKER="";; - auto) DOCKER="auto";; - 1|yes|true|on) DOCKER="force";; - *) - echo "Warning: WP_CLI_TEST_DOCKER is neither \"auto\" nor a yes or no, running the scenarios without Docker." >&2 - DOCKER="";; -esac - -without_docker() { - if [ "${DOCKER}" = "force" ]; then - echo "Error: $1 Set WP_CLI_TEST_DOCKER to \"auto\" to run the scenarios without Docker in that case." >&2 - exit 1 - fi - echo "$1 Running the scenarios without Docker." >&2 - DOCKER="" -} - case "$(uname -s)" in - MINGW*|MSYS*|CYGWIN*) - PARALLEL="" - if [ -n "${DOCKER}" ]; then - without_docker "The scenarios cannot run in Docker containers on Windows." - fi;; + MINGW*|MSYS*|CYGWIN*) PARALLEL="";; esac -if [ -n "${DOCKER}" ]; then - if ! command -v docker > /dev/null 2>&1 || ! docker info > /dev/null 2>&1; then - without_docker "Docker is not available." - elif [[ "${WP_CLI_TEST_COVERAGE}" == "true" ]]; then - without_docker "Code coverage cannot be collected in Docker containers." - fi -fi - -export WP_CLI_TEST_DOCKER="${DOCKER}" # A rerun after a parallel run covers exactly the scenarios that failed in it, which only the # parallel runner knows, whether or not WP_CLI_TEST_PARALLEL is set for the rerun as well. diff --git a/tests/tests/TestBehatTags.php b/tests/tests/TestBehatTags.php index 0ff7f3d2..64ccc046 100644 --- a/tests/tests/TestBehatTags.php +++ b/tests/tests/TestBehatTags.php @@ -349,6 +349,33 @@ public function test_behat_tags_db_version(): void { $this->assertSame( $expected, $output ); } + public function test_behat_tags_docker_db_image(): void { + if ( Utils\is_windows() ) { + $this->markTestSkipped( 'The scenarios do not run in Docker containers on Windows.' ); + } + + file_put_contents( $this->temp_dir . DIRECTORY_SEPARATOR . 'features' . DIRECTORY_SEPARATOR . 'db.feature', '@require-mysql @require-mariadb-10.6 @require-mariadb-11.4 @less-than-mariadb-10.5 @less-than-mariadb-11' ); + + $base_expecteds = array( '~@github-api', '~@broken' ); + if ( 'sqlite' !== getenv( 'WP_CLI_TEST_OBJECT_CACHE' ) ) { + $base_expecteds[] = '~@require-object-cache'; + } else { + $base_expecteds[] = '~@skip-object-cache'; + } + $expecteds = array( '~@require-mysql', '~@require-sqlite', '~@skip-mariadb', '~@require-mariadb-11.4', '~@less-than-mariadb-10.5' ); + $expected = '--tags=' . implode( '&&', array_merge( $base_expecteds, $expecteds ) ); + $output = $this->run_with_env( + array( + 'GITHUB_TOKEN' => null, + 'WP_VERSION' => null, + 'WP_CLI_TEST_DBTYPE' => 'mysql', + 'WP_CLI_TEST_DOCKER' => 'force', + 'WP_CLI_TEST_DOCKER_DB' => 'mariadb:10.11', + ) + ); + $this->assertSame( $expected, $output ); + } + public function test_behat_tags_os(): void { $env_github_token = getenv( 'GITHUB_TOKEN' ); $db_type = getenv( 'WP_CLI_TEST_DBTYPE' ); diff --git a/utils/behat-parallel.php b/utils/behat-parallel.php index fec809e1..30a01077 100644 --- a/utils/behat-parallel.php +++ b/utils/behat-parallel.php @@ -71,6 +71,28 @@ function wp_cli_tests_without_docker( $reason ) { fwrite( STDERR, "{$reason} Running the scenarios without Docker.\n" ); } +/** + * Start the run over without Docker and exit with its exit code. + * + * The database check and the scenario selection were made for the containers, + * so run-behat-tests has to make them again for the host. + * + * @param array $user_args + * @param array|null $rerun_paths Scenarios of a rerun, which are put back for it. + */ +function wp_cli_tests_restart_without_docker( array $user_args, $rerun_paths ) { + if ( null !== $rerun_paths ) { + file_put_contents( wp_cli_tests_failures_file(), implode( "\n", $rerun_paths ) . "\n" ); + $user_args[] = '--rerun'; + } + + $wp_version = getenv( 'WP_CLI_TEST_REQUESTED_WP_VERSION' ); + putenv( false === $wp_version || '' === $wp_version ? 'WP_VERSION' : "WP_VERSION={$wp_version}" ); + putenv( 'WP_CLI_TEST_DOCKER=0' ); + passthru( escapeshellarg( dirname( __DIR__ ) . '/bin/run-behat-tests' ) . ' ' . implode( ' ', array_map( 'escapeshellarg', $user_args ) ), $exit_code ); + exit( $exit_code ); +} + /** * Run Behat in the current process' environment and exit with its exit code. * @@ -567,15 +589,10 @@ function wp_cli_tests_docker_worker_command( $name, $php_image, $db_image, array if ( null === $php_image ) { wp_cli_tests_without_docker( 'The containers could not be prepared.' ); + wp_cli_tests_restart_without_docker( $user_args, $rerun_paths ); } elseif ( null === $db_image && 'sqlite' !== getenv( 'WP_CLI_TEST_DBTYPE' ) ) { wp_cli_tests_without_docker( 'The type and version of the database server could not be identified, set WP_CLI_TEST_DOCKER_DB to its Docker image.' ); - } - - if ( null === $php_image || ( null === $db_image && 'sqlite' !== getenv( 'WP_CLI_TEST_DBTYPE' ) ) ) { - $docker = false; - if ( $workers < 2 || null !== $rerun_paths || $selects_scenarios ) { - wp_cli_tests_run_behat_and_exit( $all_args ); - } + wp_cli_tests_restart_without_docker( $user_args, $rerun_paths ); } } diff --git a/utils/behat-tags.php b/utils/behat-tags.php index a17d7c05..f0c3b8e1 100644 --- a/utils/behat-tags.php +++ b/utils/behat-tags.php @@ -48,6 +48,15 @@ function version_tags( } function get_db_type_and_version() { + // Docker workers run their own database server of the given image, so it decides. + $docker_db = getenv( 'WP_CLI_TEST_DOCKER_DB' ); + if ( getenv( 'WP_CLI_TEST_DOCKER' ) && is_string( $docker_db ) && preg_match( '@^(?:[^/]+/)*(mysql|mariadb)(?::([0-9]+(?:\.[0-9]+)*))?@', $docker_db, $matches ) ) { + return array( + 'type' => $matches[1], + 'version' => isset( $matches[2] ) ? $matches[2] : '', + ); + } + // Detect which client binary is available. $client_binary = null; $output = array(); diff --git a/utils/docker/Dockerfile b/utils/docker/Dockerfile index 137d0b8a..0a66c0b0 100644 --- a/utils/docker/Dockerfile +++ b/utils/docker/Dockerfile @@ -10,13 +10,34 @@ RUN install_packages() { \ libfreetype6-dev libjpeg-dev libpng-dev libzip-dev; \ } \ && ( install_packages || { \ - sed -i -e 's|deb.debian.org|archive.debian.org|g' -e '/-updates/d' /etc/apt/sources.list \ + sed -i -e 's|deb.debian.org|archive.debian.org|g' -e 's|security.debian.org|archive.debian.org|g' -e '/-updates/d' /etc/apt/sources.list \ && echo 'Acquire::Check-Valid-Until "false";' > /etc/apt/apt.conf.d/99archive \ && install_packages; \ } ) \ - && docker-php-ext-configure gd --with-freetype --with-jpeg \ + && ( docker-php-ext-configure gd --with-freetype --with-jpeg \ + || docker-php-ext-configure gd --with-freetype-dir=/usr --with-jpeg-dir=/usr ) \ && docker-php-ext-install exif gd mysqli pdo_mysql zip \ && ( php -m | grep -qi '^zend opcache$' || docker-php-ext-install opcache ) \ && rm -rf /var/lib/apt/lists/* +# The SQLite integration plugin needs SQLite 3.37 or later, which older Debian releases do not ship. +# PHP links to the library, so a newer build in /usr/local/lib takes the place of the system one, +# with the same options as Debian's, as PHP uses the functions they enable. +ARG SQLITE_VERSION=3540100 +ARG SQLITE_YEAR=2026 +ARG SQLITE_SHA3_256=b6ced585c750916b1052667239a12c25a7ff34e7f06e1e6e01ecb0427657d9c4 +RUN if dpkg --compare-versions "$(dpkg-query -W -f '${Version}' libsqlite3-0)" lt 3.37; then \ + cd /tmp \ + && curl -fsSL -o sqlite.tar.gz "https://www.sqlite.org/${SQLITE_YEAR}/sqlite-autoconf-${SQLITE_VERSION}.tar.gz" \ + && [ "$(openssl dgst -sha3-256 -r sqlite.tar.gz | cut -d' ' -f1)" = "${SQLITE_SHA3_256}" ] \ + && tar xzf sqlite.tar.gz \ + && cd "sqlite-autoconf-${SQLITE_VERSION}" \ + && CFLAGS="-O2 -DSQLITE_ENABLE_COLUMN_METADATA=1 -DSQLITE_ENABLE_FTS3 -DSQLITE_ENABLE_FTS3_PARENTHESIS -DSQLITE_ENABLE_FTS4 -DSQLITE_ENABLE_FTS5 -DSQLITE_ENABLE_RTREE -DSQLITE_ENABLE_UNLOCK_NOTIFY -DSQLITE_SECURE_DELETE" \ + ./configure --prefix=/usr/local --disable-static > /dev/null \ + && make -j"$(nproc)" > /dev/null \ + && make install > /dev/null \ + && ldconfig \ + && cd /tmp && rm -rf sqlite.tar.gz "sqlite-autoconf-${SQLITE_VERSION}"; \ + fi + COPY --from=composer:2 /usr/bin/composer /usr/bin/composer