diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index bf67592d8..94241454e 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -13,3 +13,34 @@ on: jobs: test: uses: wp-cli/.github/.github/workflows/reusable-testing.yml@main + with: + matrix: | + { + "include": [ + { + "php": "8.4", + "wp": "latest", + "mysql": "mysql-8.0", + "os": "macos-15" + }, + { + "php": "8.4", + "wp": "latest", + "dbtype": "sqlite", + "os": "macos-15" + }, + { + "php": "8.4", + "wp": "latest", + "mysql": "mysql-8.0", + "os": "windows-2025" + }, + { + "php": "8.4", + "wp": "latest", + "dbtype": "sqlite", + "os": "windows-2025" + } + ], + "exclude": [] + } diff --git a/bin/install-package-tests b/bin/install-package-tests index 2646ebb1b..ec3916cdd 100755 --- a/bin/install-package-tests +++ b/bin/install-package-tests @@ -15,9 +15,9 @@ is_numeric() { *) return 0;; # returns 0 if numeric esac } -# Promt color vars. -C_RED="\033[31m" -C_BLUE="\033[34m" +# Prompt color vars. +C_RED="\033[0;31m" +C_BLUE="\033[0;34m" NO_FORMAT="\033[0m" HOST=localhost diff --git a/composer.json b/composer.json index 01e690e90..e91479f32 100644 --- a/composer.json +++ b/composer.json @@ -25,7 +25,7 @@ "wp-cli/config-command": "^1 || ^2", "wp-cli/core-command": "^1 || ^2", "wp-cli/eval-command": "^1 || ^2", - "wp-cli/wp-cli": "^2.12", + "wp-cli/wp-cli": "^2.13", "wp-coding-standards/wpcs": "^3", "yoast/phpunit-polyfills": "^4.0.0" }, diff --git a/features/steps.feature b/features/steps.feature index 6968ba636..10fcf7384 100644 --- a/features/steps.feature +++ b/features/steps.feature @@ -61,7 +61,7 @@ Feature: Make sure "Given", "When", "Then" steps work as expected Scenario: Special variables When I run `echo {INVOKE_WP_CLI_WITH_PHP_ARGS-} cli info` - Then STDOUT should match /wp cli info/ + Then STDOUT should match /(wp|wp\.bat) cli info/ And STDERR should be empty When I run `echo {WP_VERSION-latest}` diff --git a/features/testing.feature b/features/testing.feature index 4bee962c4..48e714062 100644 --- a/features/testing.feature +++ b/features/testing.feature @@ -11,6 +11,10 @@ Feature: Test that WP-CLI loads. Scenario: WP Cron is disabled by default Given a WP install + And the wp-config.php file should contain: + """ + if ( defined( 'DISABLE_WP_CRON' ) === false ) { define( 'DISABLE_WP_CRON', true ); } + """ And a test_cron.php file: """ #' - message: '#Dynamic call to static method#' path: 'tests/tests' strictRules: diff --git a/phpunit.xml.dist b/phpunit.xml.dist index b7619f503..97c0c774b 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -11,7 +11,6 @@ > - tests/ tests/tests diff --git a/src/Context/FeatureContext.php b/src/Context/FeatureContext.php index 4ec3c15a3..9b81a9009 100644 --- a/src/Context/FeatureContext.php +++ b/src/Context/FeatureContext.php @@ -13,7 +13,6 @@ use Behat\Behat\Hook\Scope\AfterFeatureScope; use Behat\Behat\Hook\Scope\BeforeFeatureScope; use Behat\Behat\Hook\Scope\BeforeStepScope; -use Behat\Testwork\Hook\Scope\HookScope; use SebastianBergmann\CodeCoverage\Report\Clover; use SebastianBergmann\CodeCoverage\Driver\Selector; use SebastianBergmann\CodeCoverage\Driver\Xdebug; @@ -21,7 +20,6 @@ use SebastianBergmann\CodeCoverage\CodeCoverage; use SebastianBergmann\Environment\Runtime; use RuntimeException; -use WP_CLI; use DirectoryIterator; use WP_CLI\Process; use WP_CLI\ProcessRun; @@ -324,9 +322,9 @@ public static function get_vendor_dir(): ?string { // We try to detect the vendor folder in the most probable locations. $vendor_locations = [ // wp-cli/wp-cli-tests is a dependency of the current working dir. - getcwd() . '/vendor', + getcwd() . DIRECTORY_SEPARATOR . 'vendor', // wp-cli/wp-cli-tests is the root project. - dirname( __DIR__, 2 ) . '/vendor', + dirname( __DIR__, 2 ) . DIRECTORY_SEPARATOR . 'vendor', // wp-cli/wp-cli-tests is a dependency. dirname( __DIR__, 4 ), ]; @@ -365,7 +363,7 @@ public static function get_framework_dir(): ?string { // wp-cli/wp-cli is the root project. dirname( $vendor_folder ), // wp-cli/wp-cli is a dependency. - "{$vendor_folder}/wp-cli/wp-cli", + $vendor_folder . DIRECTORY_SEPARATOR . 'wp-cli' . DIRECTORY_SEPARATOR . 'wp-cli', ]; $framework_folder = ''; @@ -402,18 +400,20 @@ public static function get_bin_path(): ?string { } $bin_paths = [ - self::get_vendor_dir() . '/bin', - self::get_framework_dir() . '/bin', + self::get_vendor_dir() . DIRECTORY_SEPARATOR . 'bin', + self::get_framework_dir() . DIRECTORY_SEPARATOR . 'bin', ]; + $bin = Utils\is_windows() ? 'wp.bat' : 'wp'; + foreach ( $bin_paths as $path ) { - if ( is_file( "{$path}/wp" ) && is_executable( "{$path}/wp" ) ) { - $bin_path = $path; - break; + $full_bin_path = $path . DIRECTORY_SEPARATOR . $bin; + if ( is_file( $full_bin_path ) && ( Utils\is_windows() || is_executable( $full_bin_path ) ) ) { + return $path; } } - return $bin_path; + return null; } /** @@ -430,24 +430,32 @@ private static function get_process_env_variables(): array { // Ensure we're using the expected `wp` binary. $bin_path = self::get_bin_path(); - wp_cli_behat_env_debug( "WP-CLI binary path: {$bin_path}" ); - if ( ! file_exists( "{$bin_path}/wp" ) ) { - wp_cli_behat_env_debug( "WARNING: No file named 'wp' found in the provided/detected binary path." ); + if ( ! $bin_path ) { + throw new RuntimeException( 'Could not find WP-CLI binary path.' ); } - if ( ! is_executable( "{$bin_path}/wp" ) ) { - wp_cli_behat_env_debug( "WARNING: File named 'wp' found in the provided/detected binary path is not executable." ); + wp_cli_behat_env_debug( "WP-CLI binary path: {$bin_path}" ); + + $bin = Utils\is_windows() ? 'wp.bat' : 'wp'; + $full_bin_path = $bin_path . DIRECTORY_SEPARATOR . $bin; + + if ( ! is_executable( $full_bin_path ) ) { + wp_cli_behat_env_debug( "WARNING: File named '{$bin}' found in the provided/detected binary path is not executable." ); } - $path_separator = Utils\is_windows() ? ';' : ':'; - $env = [ - 'PATH' => $bin_path . $path_separator . getenv( 'PATH' ), - 'BEHAT_RUN' => 1, - 'HOME' => sys_get_temp_dir() . '/wp-cli-home', - 'TEST_RUN_DIR' => self::$behat_run_dir, + $path_separator = Utils\is_windows() ? ';' : ':'; + $php_binary_path = dirname( PHP_BINARY ); + $env = [ + 'PATH' => $php_binary_path . $path_separator . $bin_path . $path_separator . getenv( 'PATH' ), + 'BEHAT_RUN' => 1, + 'HOME' => sys_get_temp_dir() . '/wp-cli-home', + 'COMPOSER_HOME' => sys_get_temp_dir() . '/wp-cli-composer-home', + 'TEST_RUN_DIR' => self::$behat_run_dir, ]; + $env = array_merge( $_ENV, $env ); + if ( self::running_with_code_coverage() ) { $has_coverage_driver = ( new Runtime() )->hasXdebug() || ( new Runtime() )->hasPCOV(); @@ -565,13 +573,11 @@ private static function download_sqlite_plugin( $dir ): void { mkdir( $dir ); } - Process::create( - Utils\esc_cmd( - 'curl -sSfL %1$s > %2$s', - $download_url, - $download_location - ) - )->run_check(); + $response = Utils\http_request( 'GET', $download_url, null, [], [ 'filename' => $download_location ] ); + + if ( 200 !== $response->status_code ) { + throw new RuntimeException( "Could not download SQLite plugin (HTTP code {$response->status_code})" ); + } $zip = new \ZipArchive(); $new_zip_file = $download_location; @@ -655,7 +661,13 @@ public static function prepare( BeforeSuiteScope $scope ): void { self::log_run_times_before_suite( $scope ); } self::$behat_run_dir = getcwd(); - self::$mysql_binary = Utils\get_mysql_binary_path(); + + // TODO: Improve Windows support upstream in Utils\get_mysql_binary_path(). + if ( Utils\is_windows() ) { + self::$mysql_binary = 'mysql.exe'; + } else { + self::$mysql_binary = Utils\get_mysql_binary_path(); + } $result = Process::create( 'wp cli info', null, self::get_process_env_variables() )->run_check(); echo "{$result->stdout}\n"; @@ -699,10 +711,9 @@ public function beforeScenario( BeforeScenarioScope $scope ): void { self::get_behat_internal_variables() ); - $mysql_binary = Utils\get_mysql_binary_path(); $sql_dump_command = Utils\get_sql_dump_command(); - $this->variables['MYSQL_BINARY'] = $mysql_binary; + $this->variables['MYSQL_BINARY'] = self::$mysql_binary; $this->variables['SQL_DUMP_COMMAND'] = $sql_dump_command; // Used in the names of the RUN_DIR and SUITE_CACHE_DIR directories. @@ -760,6 +771,10 @@ public function afterScenario( AfterScenarioScope $scope ): void { * @param int $master_pid */ private static function terminate_proc( $master_pid ): void { + if ( Utils\is_windows() ) { + shell_exec( "taskkill /F /T /PID $master_pid > NUL 2>&1" ); + return; + } $output = shell_exec( "ps -o ppid,pid,command | grep $master_pid" ); @@ -774,6 +789,10 @@ private static function terminate_proc( $master_pid ): void { } } + if ( ! function_exists( 'posix_kill' ) ) { + return; + } + if ( ! posix_kill( (int) $master_pid, 9 ) ) { $errno = posix_get_last_error(); // Ignore "No such process" error as that's what we want. @@ -896,18 +915,19 @@ private function replace_invoke_wp_cli_with_php_args( $str ) { $phar_begin = '#!/usr/bin/env php'; $phar_begin_len = strlen( $phar_begin ); $bin_dir = getenv( 'WP_CLI_BIN_DIR' ); + $bin = Utils\is_windows() ? 'wp.bat' : 'wp'; if ( false !== $bin_dir && file_exists( $bin_dir . '/wp' ) && file_get_contents( $bin_dir . '/wp', false, null, 0, $phar_begin_len ) === $phar_begin ) { - $phar_path = $bin_dir . '/wp'; + $phar_path = $bin_dir . $bin; } else { $src_dir = dirname( __DIR__, 2 ); - $bin_path = $src_dir . '/bin/wp'; - $vendor_bin_path = $src_dir . '/vendor/bin/wp'; + $bin_path = $src_dir . '/bin/' . $bin; + $vendor_bin_path = $src_dir . '/vendor/bin/' . $bin; if ( file_exists( $bin_path ) && is_executable( $bin_path ) ) { $shell_path = $bin_path; } elseif ( file_exists( $vendor_bin_path ) && is_executable( $vendor_bin_path ) ) { $shell_path = $vendor_bin_path; } else { - $shell_path = 'wp'; + $shell_path = $bin; } } } @@ -1045,14 +1065,18 @@ public function build_phar( $version = 'same' ): void { $this->composer_command( 'dump-autoload --working-dir=' . dirname( self::get_vendor_dir() ) ); } - $this->proc( - Utils\esc_cmd( - 'php -dphar.readonly=0 %1$s %2$s --version=%3$s && chmod +x %2$s', - $make_phar_path, - $this->variables['PHAR_PATH'], - $version - ) - )->run_check(); + $command = Utils\esc_cmd( + 'php -dphar.readonly=0 %1$s %2$s --version=%3$s', + $make_phar_path, + $this->variables['PHAR_PATH'], + $version + ); + + if ( ! Utils\is_windows() ) { + $command .= Utils\esc_cmd( ' && chmod +x %s', $this->variables['PHAR_PATH'] ); + } + + $this->proc( $command )->run_check(); // Revert the suffix change again if ( $is_bundle && self::running_with_code_coverage() ) { @@ -1078,13 +1102,11 @@ public function download_phar( $version = 'same' ): void { . uniqid( 'wp-cli-download-', true ) . '.phar'; - Process::create( - Utils\esc_cmd( - 'curl -sSfL %1$s > %2$s && chmod +x %2$s', - $download_url, - $this->variables['PHAR_PATH'] - ) - )->run_check(); + $response = Utils\http_request( 'GET', $download_url, null, [], [ 'filename' => $this->variables['PHAR_PATH'] ] ); + + if ( 200 !== $response->status_code ) { + throw new RuntimeException( "Could not download WP-CLI PHAR (HTTP code {$response->status_code})" ); + } } /** @@ -1226,20 +1248,41 @@ public function proc( $command, $assoc_args = [], $path = '' ): Process { * @param string $cmd */ public function background_proc( $cmd ): void { - $descriptors = [ - 0 => STDIN, - 1 => [ 'pipe', 'w' ], - 2 => [ 'pipe', 'w' ], - ]; + if ( Utils\is_windows() ) { + // On Windows, leaving pipes open can cause hangs. + // Redirect output to files and close stdin. + $stdout_file = tempnam( sys_get_temp_dir(), 'behat-stdout-' ); + $stderr_file = tempnam( sys_get_temp_dir(), 'behat-stderr-' ); + $descriptors = [ + 0 => [ 'pipe', 'r' ], + 1 => [ 'file', $stdout_file, 'a' ], + 2 => [ 'file', $stderr_file, 'a' ], + ]; + } else { + $descriptors = [ + 0 => STDIN, + 1 => [ 'pipe', 'w' ], + 2 => [ 'pipe', 'w' ], + ]; + } $proc = proc_open( $cmd, $descriptors, $pipes, $this->variables['RUN_DIR'], self::get_process_env_variables() ); + if ( Utils\is_windows() ) { + fclose( $pipes[0] ); + } + sleep( 1 ); $status = proc_get_status( $proc ); if ( ! $status['running'] ) { - $stderr = is_resource( $pipes[2] ) ? ( ': ' . stream_get_contents( $pipes[2] ) ) : ''; + if ( Utils\is_windows() ) { + $stderr = file_get_contents( $stderr_file ); + $stderr = $stderr ? ': ' . $stderr : ''; + } else { + $stderr = is_resource( $pipes[2] ) ? ( ': ' . stream_get_contents( $pipes[2] ) ) : ''; + } throw new RuntimeException( sprintf( "Failed to start background process '%s'%s.", $cmd, $stderr ) ); } @@ -1260,7 +1303,24 @@ public function move_files( $src, $dest ): void { * @param string $dir */ public static function remove_dir( $dir ): void { - Process::create( Utils\esc_cmd( 'rm -rf %s', $dir ) )->run_check(); + if ( ! is_dir( $dir ) ) { + return; + } + + $iterator = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator( $dir, \FilesystemIterator::SKIP_DOTS ), + \RecursiveIteratorIterator::CHILD_FIRST + ); + + foreach ( $iterator as $file ) { + if ( $file->isDir() ) { + rmdir( $file->getRealPath() ); + } else { + unlink( $file->getRealPath() ); + } + } + + rmdir( $dir ); } /** @@ -1270,10 +1330,21 @@ public static function remove_dir( $dir ): void { * @param string $dest_dir */ public static function copy_dir( $src_dir, $dest_dir ): void { - $shell_command = ( 'Darwin' === PHP_OS ) - ? Utils\esc_cmd( 'cp -R %s/* %s', $src_dir, $dest_dir ) - : Utils\esc_cmd( 'cp -r %s/* %s', $src_dir, $dest_dir ); - Process::create( $shell_command )->run_check(); + $iterator = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator( $src_dir, \RecursiveDirectoryIterator::SKIP_DOTS ), + \RecursiveIteratorIterator::SELF_FIRST + ); + + foreach ( $iterator as $item ) { + $dest_path = $dest_dir . '/' . $iterator->getSubPathname(); + if ( $item->isDir() ) { + if ( ! is_dir( $dest_path ) ) { + mkdir( $dest_path, 0777, true ); + } + } else { + copy( $item->getPathname(), $dest_path ); + } + } } /** @@ -1377,7 +1448,7 @@ public function install_wp( $subdir = '' ): void { $subdir = $this->replace_variables( $subdir ); // Disable WP Cron by default to avoid bogus HTTP requests in CLI context. - $config_extra_php = "if ( ! defined( 'DISABLE_WP_CRON' ) ) { define( 'DISABLE_WP_CRON', true ); }\n"; + $config_extra_php = "if ( defined( 'DISABLE_WP_CRON' ) === false ) { define( 'DISABLE_WP_CRON', true ); }\n"; if ( 'sqlite' !== self::$db_type ) { $this->create_db(); @@ -1418,7 +1489,8 @@ public function install_wp( $subdir = '' ): void { if ( 'sqlite' !== self::$db_type ) { $mysqldump_binary = Utils\get_sql_dump_command(); $mysqldump_binary = Utils\force_env_on_nix_systems( $mysqldump_binary ); - $support_column_statistics = exec( "{$mysqldump_binary} --help | grep 'column-statistics'" ); + $help_output = shell_exec( "{$mysqldump_binary} --help" ); + $support_column_statistics = false !== strpos( $help_output, 'column-statistics' ); $command = "{$mysqldump_binary} --no-defaults --no-tablespaces"; if ( $support_column_statistics ) { $command .= ' --skip-column-statistics'; @@ -1452,7 +1524,7 @@ public function install_wp_with_composer( $vendor_directory = 'vendor' ): void { $this->composer_command( 'require johnpbloch/wordpress-core-installer johnpbloch/wordpress-core --optimize-autoloader' ); // Disable WP Cron by default to avoid bogus HTTP requests in CLI context. - $config_extra_php = "if ( ! defined( 'DISABLE_WP_CRON' ) ) { define( 'DISABLE_WP_CRON', true ); }\n"; + $config_extra_php = "if ( defined( 'DISABLE_WP_CRON' ) === false ) { define( 'DISABLE_WP_CRON', true ); }\n"; $config_extra_php .= "require_once dirname(__DIR__) . '/" . $vendor_directory . "/autoload.php';\n"; @@ -1527,7 +1599,14 @@ public function start_php_server( $subdir = '' ): void { */ private function composer_command( $cmd ): void { if ( ! isset( $this->variables['COMPOSER_PATH'] ) ) { - $this->variables['COMPOSER_PATH'] = exec( 'which composer' ); + $command = Utils\is_windows() ? 'where composer' : 'which composer'; + $path = exec( $command ); + if ( false === $path ) { + throw new RuntimeException( 'Could not find composer.' ); + } + // In case of multiple paths, pick the first one. + $path = strtok( $path, PHP_EOL ); + $this->variables['COMPOSER_PATH'] = $path; } $this->proc( $this->variables['COMPOSER_PATH'] . ' --no-interaction ' . $cmd )->run_check(); } @@ -1791,7 +1870,6 @@ function wpcli_bootstrap_behat_feature_context(): void { // Load helper functionality that is needed for the tests. require_once "{$framework_folder}/php/utils.php"; - require_once "{$framework_folder}/php/WP_CLI/Process.php"; require_once "{$framework_folder}/php/WP_CLI/ProcessRun.php"; // Manually load Composer file includes by generating a config with require: diff --git a/src/Context/GivenStepDefinitions.php b/src/Context/GivenStepDefinitions.php index 248766a0e..5deca4b73 100644 --- a/src/Context/GivenStepDefinitions.php +++ b/src/Context/GivenStepDefinitions.php @@ -604,7 +604,10 @@ public function given_a_download( TableNode $table ): void { continue; } - Process::create( Utils\esc_cmd( 'curl -sSL %s > %s', $row['url'], $path ) )->run_check(); + $response = Utils\http_request( 'GET', $row['url'], null, [], [ 'filename' => $path ] ); + if ( 200 !== $response->status_code ) { + throw new RuntimeException( "Could not download file (HTTP code {$response->status_code})" ); + } } } diff --git a/tests/tests/TestBehatTags.php b/tests/tests/TestBehatTags.php index 5b3ca49c1..5b581bc14 100644 --- a/tests/tests/TestBehatTags.php +++ b/tests/tests/TestBehatTags.php @@ -19,22 +19,79 @@ protected function set_up(): void { $this->temp_dir = Utils\get_temp_dir() . uniqid( 'wp-cli-test-behat-tags-', true ); mkdir( $this->temp_dir ); - mkdir( $this->temp_dir . '/features' ); + mkdir( $this->temp_dir . DIRECTORY_SEPARATOR . 'features' ); } protected function tear_down(): void { - if ( $this->temp_dir && file_exists( $this->temp_dir ) ) { - foreach ( glob( $this->temp_dir . '/features/*' ) as $feature_file ) { - unlink( $feature_file ); - } - rmdir( $this->temp_dir . '/features' ); - rmdir( $this->temp_dir ); + $this->remove_dir( $this->temp_dir ); } parent::tear_down(); } + /** + * Recursively removes a directory and its contents. + * + * @param string $dir The directory to remove. + */ + private function remove_dir( $dir ): void { + if ( ! is_dir( $dir ) ) { + return; + } + + $iterator = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator( $dir, \FilesystemIterator::SKIP_DOTS ), + \RecursiveIteratorIterator::CHILD_FIRST + ); + + foreach ( $iterator as $file ) { + if ( $file->isDir() ) { + rmdir( $file->getRealPath() ); + } else { + unlink( $file->getRealPath() ); + } + } + + rmdir( $dir ); + } + + /** + * Runs the behat-tags.php script in a cross-platform way. + * + * @param string $env Environment variable string to set (e.g., 'WP_VERSION=4.5'). + * @return string|false The output of the script. + */ + private function run_behat_tags_script( $env = '' ) { + $behat_tags = dirname( dirname( __DIR__ ) ) . DIRECTORY_SEPARATOR . 'utils' . DIRECTORY_SEPARATOR . 'behat-tags.php'; + + // Use the `-n` flag to disable loading of `php.ini` and ensure a clean environment. + $php_run = escapeshellarg( PHP_BINARY ) . ' -n ' . escapeshellarg( $behat_tags ); + + $features_dir = $this->temp_dir . DIRECTORY_SEPARATOR . 'features'; + + $command = ''; + if ( Utils\is_windows() ) { + // `set` is internal to `cmd.exe`. Do not escape the values, as `set` doesn't understand quotes from escapeshellarg. + // Note: `set "VAR=VALUE"` is more robust than `set VAR=VALUE`. + $command = 'set "BEHAT_FEATURES_FOLDER=' . $features_dir . '" && '; + if ( ! empty( $env ) ) { + $command .= 'set "' . $env . '" && '; + } + } else { + // On Unix-like systems, this sets the variable for the duration of the command. + $command = 'BEHAT_FEATURES_FOLDER=' . escapeshellarg( $features_dir ); + if ( ! empty( $env ) ) { + $command .= ' ' . $env; + } + $command .= ' '; + } + + $command = 'cd ' . escapeshellarg( $this->temp_dir ) . ' && ' . $command . $php_run; + $output = exec( $command ); + + return $output; + } /** * @dataProvider data_behat_tags_wp_version_github_token * @@ -50,12 +107,10 @@ public function test_behat_tags_wp_version_github_token( $env, $expected ): void putenv( 'WP_VERSION' ); putenv( 'GITHUB_TOKEN' ); - $behat_tags = dirname( dirname( __DIR__ ) ) . '/utils/behat-tags.php'; - $contents = '@require-wp-4.6 @require-wp-4.8 @require-wp-4.9 @less-than-wp-4.6 @less-than-wp-4.8 @less-than-wp-4.9'; - file_put_contents( $this->temp_dir . '/features/wp_version.feature', $contents ); + file_put_contents( $this->temp_dir . DIRECTORY_SEPARATOR . 'features' . DIRECTORY_SEPARATOR . 'wp_version.feature', $contents ); - $output = exec( "cd {$this->temp_dir}; $env php $behat_tags" ); + $output = $this->run_behat_tags_script( $env ); $expected .= '&&~@broken'; if ( in_array( $env, array( 'WP_VERSION=trunk', 'WP_VERSION=nightly' ), true ) ) { @@ -109,8 +164,6 @@ public function test_behat_tags_php_version(): void { putenv( 'GITHUB_TOKEN' ); - $behat_tags = dirname( dirname( __DIR__ ) ) . '/utils/behat-tags.php'; - $php_version = substr( PHP_VERSION, 0, 3 ); $contents = ''; $expected = ''; @@ -163,9 +216,9 @@ public function test_behat_tags_php_version(): void { break; } - file_put_contents( $this->temp_dir . '/features/php_version.feature', $contents ); + file_put_contents( $this->temp_dir . DIRECTORY_SEPARATOR . 'features' . DIRECTORY_SEPARATOR . 'php_version.feature', $contents ); - $output = exec( "cd {$this->temp_dir}; php $behat_tags" ); + $output = $this->run_behat_tags_script(); $this->assertSame( '--tags=' . $expected, $output ); putenv( false === $env_github_token ? 'GITHUB_TOKEN' : "GITHUB_TOKEN=$env_github_token" ); @@ -177,9 +230,7 @@ public function test_behat_tags_extension(): void { putenv( 'GITHUB_TOKEN' ); - $behat_tags = dirname( dirname( __DIR__ ) ) . '/utils/behat-tags.php'; - - file_put_contents( $this->temp_dir . '/features/extension.feature', '@require-extension-imagick @require-extension-curl' ); + file_put_contents( $this->temp_dir . DIRECTORY_SEPARATOR . 'features' . DIRECTORY_SEPARATOR . 'extension.feature', '@require-extension-imagick @require-extension-curl' ); $expecteds = array(); @@ -200,15 +251,18 @@ public function test_behat_tags_extension(): void { break; } - if ( ! extension_loaded( 'imagick' ) ) { + // Check which extensions are loaded in the clean `php -n` environment to build the correct expectation. + $imagick_loaded_in_script = (bool) exec( 'cd ' . escapeshellarg( $this->temp_dir ) . ' && ' . escapeshellarg( PHP_BINARY ) . ' -n -r "echo (int)extension_loaded(\'imagick\');"' ); + if ( ! $imagick_loaded_in_script ) { $expecteds[] = '~@require-extension-imagick'; } - if ( ! extension_loaded( 'curl' ) ) { + $curl_loaded_in_script = (bool) exec( 'cd ' . escapeshellarg( $this->temp_dir ) . ' && ' . escapeshellarg( PHP_BINARY ) . ' -n -r "echo (int)extension_loaded(\'curl\');"' ); + if ( ! $curl_loaded_in_script ) { $expecteds[] = '~@require-extension-curl'; } $expected = '--tags=' . implode( '&&', array_merge( array( '~@github-api', '~@broken' ), $expecteds ) ); - $output = exec( "cd {$this->temp_dir}; php $behat_tags" ); + $output = $this->run_behat_tags_script(); $this->assertSame( $expected, $output ); putenv( false === $env_github_token ? 'GITHUB_TOKEN' : "GITHUB_TOKEN=$env_github_token" ); @@ -217,8 +271,13 @@ public function test_behat_tags_extension(): void { public function test_behat_tags_db_version(): void { $db_type = getenv( 'WP_CLI_TEST_DBTYPE' ); - $behat_tags = dirname( dirname( __DIR__ ) ) . '/utils/behat-tags.php'; + $behat_tags = dirname( dirname( __DIR__ ) ) . DIRECTORY_SEPARATOR . 'utils' . DIRECTORY_SEPARATOR . 'behat-tags.php'; + + // Just to get the get_db_version() function. Prevents unexpected output. + ob_start(); require $behat_tags; + ob_end_clean(); + // @phpstan-ignore-next-line $db_version = get_db_version(); $minimum_db_version = $db_version . '.1'; @@ -249,10 +308,10 @@ public function test_behat_tags_db_version(): void { break; } - file_put_contents( $this->temp_dir . '/features/extension.feature', $contents ); + file_put_contents( $this->temp_dir . DIRECTORY_SEPARATOR . 'features' . DIRECTORY_SEPARATOR . 'extension.feature', $contents ); $expected = '--tags=' . implode( '&&', array_merge( array( '~@github-api', '~@broken' ), $expecteds ) ); - $output = exec( "cd {$this->temp_dir}; php $behat_tags" ); + $output = $this->run_behat_tags_script(); $this->assertSame( $expected, $output ); } } diff --git a/utils/behat-tags.php b/utils/behat-tags.php index c6fa9833e..c11c49d57 100644 --- a/utils/behat-tags.php +++ b/utils/behat-tags.php @@ -23,10 +23,17 @@ function version_tags( return array(); } - exec( - "grep '@{$prefix}-[0-9\.]*' -h -o {$features_folder}/*.feature | uniq", - $existing_tags - ); + $existing_tags = array(); + $feature_files = glob( $features_folder . DIRECTORY_SEPARATOR . '*.feature' ); + if ( ! empty( $feature_files ) ) { + foreach ( $feature_files as $feature_file ) { + $contents = file_get_contents( $feature_file ); + if ( preg_match_all( '/@' . $prefix . '-[0-9\.]+/', $contents, $matches ) ) { + $existing_tags = array_merge( $existing_tags, $matches[0] ); + } + } + $existing_tags = array_unique( $existing_tags ); + } $skip_tags = array(); @@ -41,7 +48,11 @@ function version_tags( } function get_db_version() { - $version_string = exec( getenv( 'WP_CLI_TEST_DBTYPE' ) === 'mariadb' ? 'mariadb --version' : 'mysql -V' ); + $db_type = getenv( 'WP_CLI_TEST_DBTYPE' ); + if ( 'sqlite' === $db_type ) { + return ''; + } + $version_string = exec( 'mariadb' === $db_type ? 'mariadb --version' : 'mysql -V' ); preg_match( '@[0-9]+\.[0-9]+\.[0-9]+@', $version_string, $version ); return $version[0]; } @@ -85,11 +96,11 @@ function get_db_version() { $skip_tags[] = '@broken-trunk'; } -$db_version = get_db_version(); switch ( getenv( 'WP_CLI_TEST_DBTYPE' ) ) { case 'mariadb': - $skip_tags = array_merge( + $db_version = get_db_version(); + $skip_tags = array_merge( $skip_tags, [ '@require-mysql', '@require-sqlite' ], version_tags( 'require-mariadb', $db_version, '<', $features_folder ), @@ -103,7 +114,8 @@ function get_db_version() { break; case 'mysql': default: - $skip_tags = array_merge( + $db_version = get_db_version(); + $skip_tags = array_merge( $skip_tags, [ '@require-mariadb', '@require-sqlite' ], version_tags( 'require-mysql', $db_version, '<', $features_folder ), @@ -115,10 +127,16 @@ function get_db_version() { # Require PHP extension, eg 'imagick'. function extension_tags( $features_folder = 'features' ) { $extension_tags = array(); - exec( - "grep '@require-extension-[A-Za-z_]*' -h -o {$features_folder}/*.feature | uniq", - $extension_tags - ); + $feature_files = glob( $features_folder . DIRECTORY_SEPARATOR . '*.feature' ); + if ( ! empty( $feature_files ) ) { + foreach ( $feature_files as $feature_file ) { + $contents = file_get_contents( $feature_file ); + if ( preg_match_all( '/@require-extension-[A-Za-z_]*/', $contents, $matches ) ) { + $extension_tags = array_merge( $extension_tags, $matches[0] ); + } + } + $extension_tags = array_unique( $extension_tags ); + } $skip_tags = array();