From 49572a25dbfbac59636ab8bc54784aa8bedf072f Mon Sep 17 00:00:00 2001 From: dilipom13 Date: Thu, 13 Aug 2026 21:29:12 +0530 Subject: [PATCH] Add multi-PHP, environment labels, and commit tracking. Rebase PR 212 onto current master while keeping setup_runner_env_vars, and apply the requested review feedback for directory names, executable parsing, and commits.json. Co-authored-by: Cursor --- .env.default | 26 ++- .gitignore | 1 + README.md | 22 +++ cleanup.php | 50 +++-- commits.json.example | 5 + functions.php | 438 ++++++++++++++++++++++++++++++++++++++++++- prepare.php | 272 +++++++++++++-------------- report.php | 166 ++++++---------- test.php | 44 ++--- 9 files changed, 718 insertions(+), 306 deletions(-) create mode 100644 commits.json.example diff --git a/.env.default b/.env.default index df58ae0..69d76ce 100644 --- a/.env.default +++ b/.env.default @@ -11,6 +11,10 @@ # $ source .env ### +# Optional label to distinguish this environment in test reports, for example +# "shared", "vps", or "cloud". Use a single alphanumeric keyword. +export WPT_LABEL="" + # Path to the directory where files can be prepared before being delivered to the environment. export WPT_PREPARE_DIR="/tmp/wp-test-runner" @@ -35,7 +39,18 @@ export WPT_DB_HOST="" # (Optionally) set a custom table prefix to permit concurrency against the same database. export WPT_TABLE_PREFIX="${WPT_TABLE_PREFIX-wptests_}" -# (Optionally) define the PHP executable to be called +# PHP executable to be called. Default: php +# +# A single binary: +# export WPT_PHP_EXECUTABLE="php" +# export WPT_PHP_EXECUTABLE="/usr/bin/php8.1" +# +# Multiple versions (version=path, separated by semicolons). Each version +# gets its own prepare/test directories and database table prefix: +# export WPT_PHP_EXECUTABLE="8.1=/bin/php8.1;8.2=/bin/php8.2;8.3=/bin/php8.3" +# +# Use as many versions as you'd like, but keep in mind that it will take more time. +# Ideally all versions offered to users are tested. export WPT_PHP_EXECUTABLE="${WPT_PHP_EXECUTABLE-php}" # (Optionally) define the PHPUnit command execution call. @@ -83,3 +98,12 @@ export WPT_FLAVOR=0 # 2 = ms-files # 3 = external-http export WPT_EXTRATESTS=0 + +# Whether to queue recent wordpress-develop commits instead of only the latest. +# +# 0 = Off. Test only the most recent commit when the runner starts. +# 1 = On. Query the last 30 commits and queue any that have not been tested yet. +# +# Regardless of this value, commits.json tracks SHAs that were already tested +# and reported so the suite is not re-run for the same commit. +export WPT_COMMITS=0 diff --git a/.gitignore b/.gitignore index 023e69a..7554acc 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ vendor/ .cache/ package-lock.json commit.json +commits.json ignore.json # Exclude the default test directory. diff --git a/README.md b/README.md index 5baa664..cf9c435 100644 --- a/README.md +++ b/README.md @@ -387,6 +387,28 @@ journalctl -u wordpressphpunittestrunner.timer journalctl -n 120 -u wordpressphpunittestrunner.service ``` +## Multiple PHP versions, environments, and commits + +These options are configured in `.env`. Broader documentation updates live in a separate pull request; this section only covers the runner behavior added for multi-PHP, environment labels, and commit tracking. + +**Environment label.** Set `WPT_LABEL` to a short alphanumeric keyword such as `shared`, `vps`, or `cloud`. The label is included in the reported environment details so results from different setups on the same host can be distinguished. + +**Multiple PHP versions.** `WPT_PHP_EXECUTABLE` still defaults to `php`. A single binary path continues to work as before. To test more than one version, use `version=path` entries separated by semicolons: + +```bash +export WPT_PHP_EXECUTABLE="8.1=/bin/php8.1;8.2=/bin/php8.2;8.3=/bin/php8.3" +``` + +Each version uses its own prepare/test directory (the version is appended in plain text, for example `/tmp/wp-test-runner-8-1`) and a unique database table prefix so runs do not collide. + +**Commit tracking.** The runner writes `commits.json` (gitignored; copied from `commits.json.example` when missing): + +- `executed_commits`: SHAs that were successfully tested and reported +- `pending_commits`: SHAs waiting to be tested (oldest first) +- `testing_commit`: the SHA currently being tested (at most one) + +`WPT_COMMITS=0` tests only the latest commit when the runner starts. `WPT_COMMITS=1` queries the last 30 wordpress-develop commits (the GitHub API default page size) and queues any that have not been tested yet. In both modes, a commit already present in `commits.json` is skipped. + ## Contributing If you have questions about the process or run into test failures along the way, please [open an issue in the project repository](https://github.com/WordPress/phpunit-test-runner/issues) and we’ll help diagnose/get the documentation updated. Alternatively, you can also pop into the `#hosting` channel on [WordPress.org Slack](https://make.wordpress.org/chat/) for help. diff --git a/cleanup.php b/cleanup.php index e6fad20..2669e27 100644 --- a/cleanup.php +++ b/cleanup.php @@ -20,32 +20,28 @@ */ $runner_vars = setup_runner_env_vars(); -/** - * The directory path of the test preparation directory is assumed to be previously defined. - * For example: $runner_vars['WPT_PREPARE_DIR'] = '/path/to/your/preparation/dir'; - * Clean up the preparation directory. - * Forcefully deletes only the .git directory and the node_modules cache. - * Afterward, the entire preparation directory is removed to ensure a clean state for the next test run. - */ -perform_operations( - array( - 'rm -rf ' . escapeshellarg( $runner_vars['WPT_PREPARE_DIR'] . '/.git' ), - 'rm -rf ' . escapeshellarg( $runner_vars['WPT_PREPARE_DIR'] . '/node_modules/.cache' ), - 'rm -r ' . escapeshellarg( $runner_vars['WPT_PREPARE_DIR'] ), - ) -); +skip_if_no_prepared_environment( $runner_vars ); -/** - * Cleans up the test directory on a remote server. - * This conditional block checks if an SSH connection string is provided and is not empty. - * If a connection string is present, it triggers a cleanup operation on the remote environment. - * The cleanup operation is executed by the `perform_operations` function which takes an array - * of shell commands as its input. - */ -if ( ! empty( $runner_vars['WPT_SSH_CONNECT'] ) ) { - perform_operations( - array( - 'ssh ' . $runner_vars['WPT_SSH_OPTIONS'] . ' ' . escapeshellarg( $runner_vars['WPT_SSH_CONNECT'] ) . ' ' . escapeshellarg( $runner_vars['WPT_RM_TEST_DIR_CMD'] ), - ) - ); +foreach ( $runner_vars['WPT_PHP_EXECUTABLES'] as $php ) { + $paths = get_php_run_paths( $runner_vars, $php ); + + log_message( 'Cleaning environment for PHP ' . $php['version'] ); + + if ( is_dir( $paths['prepare_dir'] ) ) { + perform_operations( + array( + 'rm -rf ' . escapeshellarg( $paths['prepare_dir'] . '/.git' ), + 'rm -rf ' . escapeshellarg( $paths['prepare_dir'] . '/node_modules/.cache' ), + 'rm -r ' . escapeshellarg( $paths['prepare_dir'] ), + ) + ); + } + + if ( ! empty( $runner_vars['WPT_SSH_CONNECT'] ) ) { + perform_operations( + array( + 'ssh ' . $runner_vars['WPT_SSH_OPTIONS'] . ' ' . escapeshellarg( $runner_vars['WPT_SSH_CONNECT'] ) . ' ' . escapeshellarg( $paths['rm_cmd'] ), + ) + ); + } } diff --git a/commits.json.example b/commits.json.example new file mode 100644 index 0000000..8836f75 --- /dev/null +++ b/commits.json.example @@ -0,0 +1,5 @@ +{ + "executed_commits": [], + "pending_commits": [], + "testing_commit": "" +} diff --git a/functions.php b/functions.php index 1adc55a..c6820b6 100644 --- a/functions.php +++ b/functions.php @@ -73,6 +73,14 @@ function setup_runner_env_vars() { $ssh_options = trim( getenv( 'WPT_SSH_OPTIONS' ) ); $php_exec = trim( getenv( 'WPT_PHP_EXECUTABLE' ) ); $rm_test_dir = trim( getenv( 'WPT_RM_TEST_DIR_CMD' ) ); + $label = trim( getenv( 'WPT_LABEL' ) ); + $commits = strtolower( trim( (string) getenv( 'WPT_COMMITS' ) ) ); + + if ( '' === $php_exec ) { + $php_exec = 'php'; + } + + $php_executables = parse_php_executables( $php_exec ); $runner_configuration = array( 'WPT_TEST_DIR' => '' !== $test_dir ? $test_dir : '/tmp/wp-test-runner', @@ -84,14 +92,441 @@ function setup_runner_env_vars() { 'WPT_PREPARE_DIR' => '' !== $prepare_dir ? $prepare_dir : '/tmp/wp-test-runner', 'WPT_SSH_CONNECT' => trim( getenv( 'WPT_SSH_CONNECT' ) ), 'WPT_SSH_OPTIONS' => '' !== $ssh_options ? $ssh_options : '-o StrictHostKeyChecking=no', - 'WPT_PHP_EXECUTABLE' => '' !== $php_exec ? $php_exec : 'php', + 'WPT_PHP_EXECUTABLE' => $php_executables[0]['bin'], + 'WPT_PHP_EXECUTABLES' => $php_executables, 'WPT_RM_TEST_DIR_CMD' => '' !== $rm_test_dir ? $rm_test_dir : 'rm -r ' . $runner_configuration['WPT_TEST_DIR'], 'WPT_REPORT_API_KEY' => trim( getenv( 'WPT_REPORT_API_KEY' ) ), 'WPT_DEBUG' => (bool) getenv( 'WPT_DEBUG' ), + 'WPT_LABEL' => $label, + 'WPT_COMMITS' => ( '1' === $commits || 'true' === $commits || 'on' === $commits ), + ) + ); +} + +/** + * Converts a PHP version string into a filesystem-safe directory suffix. + * + * @param string $version PHP version such as '8.1' or 'default'. + * @return string Version with dots replaced by hyphens. + */ +function directory_name_from_php_version( $version ) { + return str_replace( '.', '-', (string) $version ); +} + +/** + * Parses WPT_PHP_EXECUTABLE into one or more PHP binaries. + * + * A single value such as `php` or `/usr/bin/php8.1` is treated as one executable. + * Values containing `=` and/or `;` are treated as a version=path map, for example + * `8.1=/bin/php8.1;8.2=/bin/php8.2`. + * + * @param string $php_executable Raw WPT_PHP_EXECUTABLE value. + * @return array[] { + * @type array ...$0 { + * @type string $version Version label (`default` for a single binary). + * @type string $bin Path or command used to invoke PHP. + * @type string $suffix Directory suffix; empty for a single binary. + * } + * } + */ +function parse_php_executables( $php_executable ) { + $php_executable = trim( $php_executable ); + if ( '' === $php_executable ) { + $php_executable = 'php'; + } + + $is_multi = ( false !== strpos( $php_executable, '=' ) || false !== strpos( $php_executable, ';' ) ); + + if ( ! $is_multi ) { + return array( + array( + 'version' => 'default', + 'bin' => $php_executable, + 'suffix' => '', + ), + ); + } + + $executables = array(); + $entries = explode( ';', $php_executable ); + foreach ( $entries as $entry ) { + $entry = trim( $entry ); + if ( '' === $entry ) { + continue; + } + + $parts = array_map( 'trim', explode( '=', $entry, 2 ) ); + if ( 2 !== count( $parts ) || '' === $parts[0] || '' === $parts[1] ) { + continue; + } + + $executables[] = array( + 'version' => $parts[0], + 'bin' => $parts[1], + 'suffix' => '-' . directory_name_from_php_version( $parts[0] ), + ); + } + + if ( empty( $executables ) ) { + return array( + array( + 'version' => 'default', + 'bin' => 'php', + 'suffix' => '', + ), + ); + } + + return $executables; +} + +/** + * Returns prepare/test directory paths for a parsed PHP executable. + * + * @param array $runner_vars Configuration from setup_runner_env_vars(). + * @param array $php One item from parse_php_executables(). + * @return array { + * @type string $prepare_dir Prepare directory for this PHP version. + * @type string $test_dir Test directory for this PHP version. + * @type string $rm_cmd Command used to remove the test directory. + * } + */ +function get_php_run_paths( $runner_vars, $php ) { + $prepare_dir = $runner_vars['WPT_PREPARE_DIR'] . $php['suffix']; + $test_dir = $runner_vars['WPT_TEST_DIR'] . $php['suffix']; + $custom_rm = trim( getenv( 'WPT_RM_TEST_DIR_CMD' ) ); + + if ( '' !== $custom_rm && '' === $php['suffix'] ) { + $rm_cmd = $custom_rm; + } else { + $rm_cmd = 'rm -r ' . $test_dir; + } + + return array( + 'prepare_dir' => $prepare_dir, + 'test_dir' => $test_dir, + 'rm_cmd' => $rm_cmd, + ); +} + +/** + * Whether any versioned prepare directory already exists. + * + * @param array $runner_vars Configuration from setup_runner_env_vars(). + * @return bool + */ +function any_prepare_directory_exists( $runner_vars ) { + foreach ( $runner_vars['WPT_PHP_EXECUTABLES'] as $php ) { + $paths = get_php_run_paths( $runner_vars, $php ); + if ( is_dir( $paths['prepare_dir'] ) ) { + return true; + } + } + + return false; +} + +/** + * Exits successfully when prepare did not create an environment (nothing to test). + * + * @param array $runner_vars Configuration from setup_runner_env_vars(). + * @return void + */ +function skip_if_no_prepared_environment( $runner_vars ) { + if ( any_prepare_directory_exists( $runner_vars ) ) { + return; + } + + log_message( 'No prepared test environment found. Skipping.' ); + exit( 0 ); +} + +/** + * Absolute path to the runner's commits.json state file. + * + * @return string + */ +function runner_commits_file_path() { + return __DIR__ . '/commits.json'; +} + +/** + * Default commits.json structure. + * + * executed_commits: SHAs that were successfully tested and reported. + * pending_commits: SHAs queued to be tested, oldest first. + * testing_commit: SHA currently being tested (empty string if none). Never more than one. + * + * @return array + */ +function default_commits_state() { + return array( + 'executed_commits' => array(), + 'pending_commits' => array(), + 'testing_commit' => '', + ); +} + +/** + * Creates commits.json from the example file when it is missing. + * + * @return void + */ +function ensure_commits_file() { + $file = runner_commits_file_path(); + $example = __DIR__ . '/commits.json.example'; + + if ( file_exists( $file ) ) { + return; + } + + if ( file_exists( $example ) ) { + copy( $example, $file ); + return; + } + + save_commits_state( default_commits_state() ); +} + +/** + * Loads and normalizes commits.json. + * + * @return array + */ +function load_commits_state() { + ensure_commits_file(); + + $decoded = json_decode( (string) file_get_contents( runner_commits_file_path() ), true ); + $state = default_commits_state(); + + if ( ! is_array( $decoded ) ) { + return $state; + } + + if ( ! empty( $decoded['executed_commits'] ) && is_array( $decoded['executed_commits'] ) ) { + $state['executed_commits'] = array_values( array_filter( $decoded['executed_commits'] ) ); + } + + if ( ! empty( $decoded['pending_commits'] ) && is_array( $decoded['pending_commits'] ) ) { + $state['pending_commits'] = array_values( array_filter( $decoded['pending_commits'] ) ); + } + + if ( isset( $decoded['testing_commit'] ) ) { + if ( is_array( $decoded['testing_commit'] ) ) { + $state['testing_commit'] = ! empty( $decoded['testing_commit'][0] ) ? (string) $decoded['testing_commit'][0] : ''; + } else { + $state['testing_commit'] = (string) $decoded['testing_commit']; + } + } + + return $state; +} + +/** + * Writes commits.json. + * + * @param array $state Commit tracking state. + * @return void + */ +function save_commits_state( $state ) { + $state = array_merge( default_commits_state(), $state ); + file_put_contents( + runner_commits_file_path(), + json_encode( $state, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ) . "\n" + ); +} + +/** + * SHAs already known to the runner (executed, pending, or in progress). + * + * @param array $state Commit tracking state. + * @return string[] + */ +function known_commit_shas( $state ) { + $testing = '' !== $state['testing_commit'] ? array( $state['testing_commit'] ) : array(); + + return array_values( + array_unique( + array_merge( $state['executed_commits'], $state['pending_commits'], $testing ) ) ); } +/** + * Fetches a URL body using cURL when available, otherwise file_get_contents(). + * + * @param string $url URL to fetch. + * @return string|false Response body, or false on failure. + */ +function fetch_url_contents( $url ) { + if ( function_exists( 'curl_init' ) ) { + $process = curl_init( $url ); + curl_setopt( $process, CURLOPT_TIMEOUT, 30 ); + curl_setopt( $process, CURLOPT_RETURNTRANSFER, true ); + curl_setopt( $process, CURLOPT_USERAGENT, 'WordPress PHPUnit Test Runner' ); + curl_setopt( + $process, + CURLOPT_HTTPHEADER, + array( + 'Accept: application/vnd.github+json', + ) + ); + + $response = curl_exec( $process ); + $status_code = (int) curl_getinfo( $process, CURLINFO_HTTP_CODE ); + curl_close( $process ); + + if ( 200 !== $status_code || false === $response ) { + return false; + } + + return $response; + } + + $context = stream_context_create( + array( + 'http' => array( + 'method' => 'GET', + 'header' => "User-Agent: WordPress PHPUnit Test Runner\r\nAccept: application/vnd.github+json\r\n", + 'timeout' => 30, + ), + ) + ); + + return file_get_contents( $url, false, $context ); +} + +/** + * Fetches recent wordpress-develop commit SHAs from the GitHub API (newest first). + * + * The GitHub API defaults to 30 results per page and caps at 100. + * + * @param int $per_page Number of commits to request. Default 30. + * @return string[] + */ +function fetch_wordpress_develop_commit_shas( $per_page = 30 ) { + $per_page = (int) $per_page; + if ( $per_page < 1 ) { + $per_page = 1; + } elseif ( $per_page > 100 ) { + $per_page = 100; + } + + $url = 'https://api.github.com/repos/WordPress/wordpress-develop/commits?per_page=' . $per_page; + $response = fetch_url_contents( $url ); + + if ( false === $response ) { + log_message( 'Warning: Could not fetch wordpress-develop commits from GitHub API.' ); + return array(); + } + + $decoded = json_decode( $response, true ); + if ( ! is_array( $decoded ) ) { + return array(); + } + + $shas = array(); + foreach ( $decoded as $commit ) { + if ( ! empty( $commit['sha'] ) ) { + $shas[] = $commit['sha']; + } + } + + return $shas; +} + +/** + * Resolves the current wordpress-develop HEAD SHA via git ls-remote. + * + * @return string SHA or an empty string on failure. + */ +function fetch_wordpress_develop_head_sha() { + $output = array(); + $retval = 0; + exec( 'git ls-remote https://github.com/WordPress/wordpress-develop.git HEAD', $output, $retval ); + if ( 0 !== $retval || empty( $output[0] ) ) { + return ''; + } + + $parts = preg_split( '/\s+/', trim( $output[0] ) ); + return isset( $parts[0] ) ? $parts[0] : ''; +} + +/** + * Queues untested wordpress-develop commits and returns the SHA to test next. + * + * When $fetch_history is true, the last 30 commits are considered. When false, + * only the most recent commit is considered. Already tested SHAs are skipped + * using commits.json in both cases. + * + * @param bool $fetch_history Whether to queue the last 30 commits. + * @return string SHA to test, `HEAD` when the SHA could not be resolved, or an empty string when there is nothing new to test. + */ +function select_commit_to_test( $fetch_history ) { + $state = load_commits_state(); + + if ( '' !== $state['testing_commit'] ) { + log_message( 'Resuming tests for wordpress-develop commit ' . $state['testing_commit'] ); + return $state['testing_commit']; + } + + $per_page = $fetch_history ? 30 : 1; + $remote = fetch_wordpress_develop_commit_shas( $per_page ); + + if ( empty( $remote ) ) { + $head = fetch_wordpress_develop_head_sha(); + if ( '' !== $head ) { + $remote = array( $head ); + } + } + + if ( empty( $remote ) ) { + log_message( 'Warning: Could not resolve wordpress-develop commits; falling back to cloned HEAD.' ); + return 'HEAD'; + } + + $candidates = $fetch_history ? array_reverse( $remote ) : array( $remote[0] ); + $known = known_commit_shas( $state ); + + foreach ( $candidates as $candidate ) { + if ( ! in_array( $candidate, $known, true ) ) { + $state['pending_commits'][] = $candidate; + $known[] = $candidate; + } + } + + $state['pending_commits'] = array_values( array_unique( $state['pending_commits'] ) ); + + if ( empty( $state['pending_commits'] ) ) { + save_commits_state( $state ); + return ''; + } + + $sha = array_shift( $state['pending_commits'] ); + $state['testing_commit'] = $sha; + save_commits_state( $state ); + log_message( 'Selected wordpress-develop commit ' . $sha ); + + return $sha; +} + +/** + * Moves the in-progress testing commit to executed_commits after a successful report. + * + * @return void + */ +function mark_testing_commit_executed() { + $state = load_commits_state(); + if ( '' === $state['testing_commit'] ) { + return; + } + + $state['executed_commits'][] = $state['testing_commit']; + $state['executed_commits'] = array_values( array_unique( $state['executed_commits'] ) ); + $state['testing_commit'] = ''; + save_commits_state( $state ); +} + /** * Executes a series of shell commands provided in the operations array. Each operation is logged before execution. * If any command fails (indicated by a non-zero return code), an error message is displayed. This function is @@ -344,6 +779,7 @@ function get_env_details() { } $env = array( + 'label' => trim( getenv( 'WPT_LABEL' ) ), 'php_version' => phpversion(), 'php_modules' => array(), 'gd_info' => $gd_info, diff --git a/prepare.php b/prepare.php index b87d428..9717ccb 100644 --- a/prepare.php +++ b/prepare.php @@ -22,6 +22,30 @@ */ $runner_vars = setup_runner_env_vars(); +/** + * Select the wordpress-develop commit to test. Already reported SHAs are skipped + * via commits.json. When WPT_COMMITS is on, the last 30 commits are queued. + */ +$commit_sha = select_commit_to_test( $runner_vars['WPT_COMMITS'] ); +if ( '' === $commit_sha ) { + log_message( 'No untested wordpress-develop commits to run. Skipping.' ); + exit( 0 ); +} + +if ( 'HEAD' === $commit_sha ) { + $resolved_sha = fetch_wordpress_develop_head_sha(); + if ( '' !== $resolved_sha ) { + $state = load_commits_state(); + if ( in_array( $resolved_sha, $state['executed_commits'], true ) ) { + log_message( 'wordpress-develop HEAD ' . $resolved_sha . ' was already tested. Skipping.' ); + exit( 0 ); + } + $state['testing_commit'] = $resolved_sha; + save_commits_state( $state ); + $commit_sha = $resolved_sha; + } +} + /** * Sets up the SSH private key for use in the test environment if provided. * The private key is expected to be in base64-encoded form in the environment variable 'WPT_SSH_PRIVATE_KEY_BASE64'. @@ -70,37 +94,7 @@ } } - -/** - * Performs a series of operations to set up the test environment. This includes creating a preparation directory, - * cloning the WordPress development repository, and preparing the environment with npm. - */ -// Prepare an array of shell commands to set up the testing environment. -perform_operations( - array( - - // Create the preparation directory if it doesn't exist. The '-p' flag creates intermediate directories as required. - 'mkdir -p ' . escapeshellarg( $runner_vars['WPT_PREPARE_DIR'] ), - - // Clone the WordPress develop repository from GitHub into the preparation directory. - // The '--depth=1' flag creates a shallow clone with a history truncated to the last commit. - 'git clone --depth=1 https://github.com/WordPress/wordpress-develop.git ' . escapeshellarg( $runner_vars['WPT_PREPARE_DIR'] ), - - // Change directory to the preparation directory, install npm dependencies, and build the project. - 'cd ' . escapeshellarg( $runner_vars['WPT_PREPARE_DIR'] ) . '; npm install && npm run build', - - ) -); - -// Log a message indicating the start of the variable replacement process for configuration. -log_message( 'Replacing variables in wp-tests-config.php' ); - -/** - * Reads the contents of the WordPress test configuration sample file. - * This file contains template placeholders that need to be replaced with actual values - * from environment variables to configure the WordPress test environment. - */ -$contents = file_get_contents( $runner_vars['WPT_PREPARE_DIR'] . '/wp-tests-config-sample.php' ); +$wpt_label = addslashes( $runner_vars['WPT_LABEL'] ); /** * Prepares a script to log system information relevant to the testing environment. @@ -126,6 +120,7 @@ \$imagick_info = Imagick::queryFormats(); } \$env = array( + 'label' => '$wpt_label', 'php_version' => phpversion(), 'php_modules' => array(), 'gd_info' => \$gd_info, @@ -207,147 +202,134 @@ function curl_selected_bits(\$k) { return in_array(\$k, array('version', 'ssl_ve // Prepend the logger script to the database settings identifier to ensure it gets included in the wp-tests-config.php file. $system_logger = $logger_replace_string . $system_logger; -// Define a string that will set the 'WP_PHP_BINARY' constant to the path of the PHP executable. -$php_binary_string = 'define( \'WP_PHP_BINARY\', \'' . $runner_vars['WPT_PHP_EXECUTABLE'] . '\' );'; +foreach ( $runner_vars['WPT_PHP_EXECUTABLES'] as $php ) { + $paths = get_php_run_paths( $runner_vars, $php ); -/** - * An associative array mapping configuration file placeholders to environment-specific values. - * This array is used in the subsequent str_replace operation to replace placeholders - * in the wp-tests-config-sample.php file with values from the environment or defaults if none are provided. - */ -$wpt_table_prefix = trim( getenv( 'WPT_TABLE_PREFIX' ) ); -$search_replace = array( - 'wptests_' => '' !== $wpt_table_prefix ? $wpt_table_prefix : 'wptests_', - 'youremptytestdbnamehere' => trim( getenv( 'WPT_DB_NAME' ) ), - 'yourusernamehere' => trim( getenv( 'WPT_DB_USER' ) ), - 'yourpasswordhere' => trim( getenv( 'WPT_DB_PASSWORD' ) ), - 'localhost' => trim( getenv( 'WPT_DB_HOST' ) ), - 'define( \'WP_PHP_BINARY\', \'php\' );' => $php_binary_string, - $logger_replace_string => $system_logger, -); + log_message( 'Preparing environment for PHP ' . $php['version'] . ' (' . $php['bin'] . ')' ); -// Replace the placeholders in the wp-tests-config-sample.php file content with actual values. -$contents = str_replace( array_keys( $search_replace ), array_values( $search_replace ), $contents ); + $clone_operations = array(); + if ( is_dir( $paths['prepare_dir'] ) ) { + $clone_operations[] = 'rm -rf ' . escapeshellarg( $paths['prepare_dir'] ); + } -// Write the modified content to the wp-tests-config.php file, which will be used by the test suite. -file_put_contents( $runner_vars['WPT_PREPARE_DIR'] . '/wp-tests-config.php', $contents ); + $clone_operations[] = 'mkdir -p ' . escapeshellarg( $paths['prepare_dir'] ); + $clone_operations[] = 'git clone --depth=1 https://github.com/WordPress/wordpress-develop.git ' . escapeshellarg( $paths['prepare_dir'] ); + $clone_operations[] = 'git -C ' . escapeshellarg( $paths['prepare_dir'] ) . ' config --add safe.directory ' . escapeshellarg( $paths['prepare_dir'] ); -/** - * Determines the PHP version of the test environment to ensure the correct version of PHPUnit is installed. - * It constructs a command that prints out the PHP version in a format compatible with PHPUnit's version requirements. - */ -$php_version_cmd = $runner_vars['WPT_PHP_EXECUTABLE'] . " -r \"print PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION . '.' . PHP_RELEASE_VERSION;\""; + if ( 'HEAD' !== $commit_sha ) { + $clone_operations[] = 'cd ' . escapeshellarg( $paths['prepare_dir'] ) . ' && git fetch --depth=1 origin ' . escapeshellarg( $commit_sha ) . ' && git checkout ' . escapeshellarg( $commit_sha ); + } -/** - * If an SSH connection string is provided, the command to determine the PHP version is modified - * to execute remotely over SSH. This is required if the test environment is not the local machine. - */ -if ( ! empty( $runner_vars['WPT_SSH_CONNECT'] ) ) { - // The PHP version check command is prefixed with the SSH command, including SSH options, - // and the connection string, ensuring the command is executed on the remote machine. - $php_version_cmd = 'ssh ' . $runner_vars['WPT_SSH_OPTIONS'] . ' ' . escapeshellarg( $runner_vars['WPT_SSH_CONNECT'] ) . ' ' . escapeshellarg( $php_version_cmd ); -} + $clone_operations[] = 'cd ' . escapeshellarg( $paths['prepare_dir'] ) . '; npm install && npm run build'; -// Initialize return value variable for the exec function call. -$retval = 0; + perform_operations( $clone_operations ); -/** - * Executes the constructed command to obtain the PHP version of the test environment. - * The output is stored in $env_php_version, and the return value of the command execution is stored in $retval. - */ -$env_php_version = exec( $php_version_cmd, $output, $retval ); + if ( 'HEAD' === $commit_sha ) { + $resolved_sha = trim( (string) exec( 'git --git-dir=' . escapeshellarg( $paths['prepare_dir'] . '/.git' ) . ' rev-parse HEAD' ) ); + if ( '' !== $resolved_sha ) { + $state = load_commits_state(); + if ( '' === $state['testing_commit'] ) { + $state['testing_commit'] = $resolved_sha; + save_commits_state( $state ); + } + $commit_sha = $resolved_sha; + } + } -// Check if the command execution was successful by inspecting the return value. -if ( 0 !== $retval ) { - error_message( 'Could not retrieve the environment PHP Version.' ); -} + log_message( 'Replacing variables in wp-tests-config.php' ); -// Log the obtained PHP version for confirmation and debugging purposes. -log_message( 'Environment PHP Version: ' . $env_php_version ); + $contents = file_get_contents( $paths['prepare_dir'] . '/wp-tests-config-sample.php' ); -/** - * Checks if the detected PHP version is below 7.2. - * The test runner requires PHP version 7.2 or above, and if the environment's PHP version - * is lower, it logs an error message and could terminate the script. - */ -if ( version_compare( $env_php_version, '7.2', '<' ) ) { - // Logs an error message indicating the test runner's incompatibility with PHP versions below 7.2. - error_message( 'The test runner is not compatible with PHP < 7.2.' ); -} + $php_binary_string = 'define( \'WP_PHP_BINARY\', \'' . $php['bin'] . '\' );'; -/** - * Use Composer to manage PHPUnit and its dependencies. - * This allows for better dependency management and compatibility. - */ + $wpt_table_prefix = trim( getenv( 'WPT_TABLE_PREFIX' ) ); + $wpt_table_prefix = '' !== $wpt_table_prefix ? $wpt_table_prefix : 'wptests_'; + if ( 'default' !== $php['version'] ) { + $wpt_table_prefix .= str_replace( '.', '_', $php['version'] ) . '_'; + } -// Check if Composer is installed and available in the PATH. -$composer_cmd = 'cd ' . escapeshellarg( $runner_vars['WPT_PREPARE_DIR'] ) . ' && '; -$retval = 0; -$composer_path = escapeshellarg( system( 'which composer', $retval ) ); + $search_replace = array( + 'wptests_' => $wpt_table_prefix, + 'youremptytestdbnamehere' => trim( getenv( 'WPT_DB_NAME' ) ), + 'yourusernamehere' => trim( getenv( 'WPT_DB_USER' ) ), + 'yourpasswordhere' => trim( getenv( 'WPT_DB_PASSWORD' ) ), + 'localhost' => trim( getenv( 'WPT_DB_HOST' ) ), + 'define( \'WP_PHP_BINARY\', \'php\' );' => $php_binary_string, + $logger_replace_string => $system_logger, + ); -if ( 0 === $retval ) { + $contents = str_replace( array_keys( $search_replace ), array_values( $search_replace ), $contents ); - // If Composer is available, prepare the command to use the Composer binary. - $composer_cmd .= $composer_path . ' '; + file_put_contents( $paths['prepare_dir'] . '/wp-tests-config.php', $contents ); -} else { + $php_version_cmd = $php['bin'] . " -r \"print PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION . '.' . PHP_RELEASE_VERSION;\""; - // If Composer is not available, download the Composer phar file. - log_message( 'Local Composer not found. Downloading latest stable ...' ); + if ( ! empty( $runner_vars['WPT_SSH_CONNECT'] ) ) { + $php_version_cmd = 'ssh ' . $runner_vars['WPT_SSH_OPTIONS'] . ' ' . escapeshellarg( $runner_vars['WPT_SSH_CONNECT'] ) . ' ' . escapeshellarg( $php_version_cmd ); + } - perform_operations( - array( - 'wget -O ' . escapeshellarg( $runner_vars['WPT_PREPARE_DIR'] . '/composer.phar' ) . ' https://getcomposer.org/composer-stable.phar', - ) - ); + $retval = 0; + $env_php_version = exec( $php_version_cmd, $output, $retval ); - // Update the command to use the downloaded Composer phar file. - $composer_cmd .= $runner_vars['WPT_PHP_EXECUTABLE'] . ' composer.phar '; -} + if ( 0 !== $retval ) { + error_message( 'Could not retrieve the environment PHP Version for ' . $php['version'] . '.' ); + } -// Set the PHP version for Composer to ensure compatibility and update dependencies. -perform_operations( - array( - $composer_cmd . 'config platform.php ' . escapeshellarg( $env_php_version ), - $composer_cmd . 'update', - ) -); + log_message( 'Environment PHP Version: ' . $env_php_version ); -/** - * If an SSH connection is configured, use rsync to transfer the prepared files to the remote test environment. - * The -r option for rsync enables recursive copying to handle directory structures. - * Additional rsync options may be included for more verbose output if debugging is enabled. - */ -if ( ! empty( $runner_vars['WPT_SSH_CONNECT'] ) ) { - // Initialize rsync options with recursive copying. - $rsync_options = '-r'; + if ( version_compare( $env_php_version, '7.2', '<' ) ) { + error_message( 'The test runner is not compatible with PHP < 7.2.' ); + } - // If debug mode is set to verbose, append 'v' to rsync options for verbose output. - if ( $runner_vars['WPT_DEBUG'] ) { - $rsync_options = $rsync_options . 'v'; + $composer_cmd = 'cd ' . escapeshellarg( $paths['prepare_dir'] ) . ' && '; + $retval = 0; + $composer_path = escapeshellarg( system( 'which composer', $retval ) ); + + if ( 0 === $retval ) { + $composer_cmd .= $composer_path . ' '; + } else { + log_message( 'Local Composer not found. Downloading latest stable ...' ); + + perform_operations( + array( + 'wget -O ' . escapeshellarg( $paths['prepare_dir'] . '/composer.phar' ) . ' https://getcomposer.org/composer-stable.phar', + ) + ); + + $composer_cmd .= $php['bin'] . ' composer.phar '; } - // Perform the rsync operation with the configured options and exclude patterns. - // This operation synchronizes the test environment with the prepared files, excluding - // version control directories and other non-essential files for test execution. perform_operations( array( - 'rsync ' . $rsync_options - . ' --exclude=".git/"' - . ' --exclude="node_modules/"' - . ' --exclude="composer.phar"' - . ' --exclude=".cache/"' - . ' --exclude=".devcontainer/"' - . ' --exclude=".github/"' - . ' --exclude="tools/"' - // Exclude all subdirectories in tests/ except phpunit/. - . ' --exclude="tests/*" --include="tests/phpunit/**"' - . ' -e "ssh ' . $runner_vars['WPT_SSH_OPTIONS'] . '" ' - . escapeshellarg( trailingslashit( $runner_vars['WPT_PREPARE_DIR'] ) ) - . ' ' . escapeshellarg( $runner_vars['WPT_SSH_CONNECT'] . ':' . $runner_vars['WPT_TEST_DIR'] ), + $composer_cmd . 'config platform.php ' . escapeshellarg( $env_php_version ), + $composer_cmd . 'update', ) ); + + if ( ! empty( $runner_vars['WPT_SSH_CONNECT'] ) ) { + $rsync_options = '-r'; + + if ( $runner_vars['WPT_DEBUG'] ) { + $rsync_options = $rsync_options . 'v'; + } + + perform_operations( + array( + 'rsync ' . $rsync_options + . ' --exclude=".git/"' + . ' --exclude="node_modules/"' + . ' --exclude="composer.phar"' + . ' --exclude=".cache/"' + . ' --exclude=".devcontainer/"' + . ' --exclude=".github/"' + . ' --exclude="tools/"' + . ' --exclude="tests/*" --include="tests/phpunit/**"' + . ' -e "ssh ' . $runner_vars['WPT_SSH_OPTIONS'] . '" ' + . escapeshellarg( trailingslashit( $paths['prepare_dir'] ) ) + . ' ' . escapeshellarg( $runner_vars['WPT_SSH_CONNECT'] . ':' . $paths['test_dir'] ), + ) + ); + } } -// Log a success message indicating that the environment has been prepared. log_message( 'Success: Prepared environment.' ); diff --git a/report.php b/report.php index 7a5a36d..b5f73fd 100644 --- a/report.php +++ b/report.php @@ -22,131 +22,75 @@ */ $runner_vars = setup_runner_env_vars(); -/** - * Retrieves the SVN revision number from the git repository log. - * Logs a message indicating the start of the SVN revision retrieval process. - * Executes a shell command that accesses the git directory specified by the - * WPT_PREPARE_DIR environment variable, retrieves the latest commit message, - * and extracts the SVN revision number using a combination of grep and cut commands. - */ -log_message( 'Getting SVN Revision' ); -$rev = exec( 'git --git-dir=' . escapeshellarg( $runner_vars['WPT_PREPARE_DIR'] ) . '/.git log -1 --pretty=%B | grep "git-svn-id:" | cut -d " " -f 2 | cut -d "@" -f 2' ); +skip_if_no_prepared_environment( $runner_vars ); -/** - * Retrieves the latest SVN commit message from the git repository log. - * Logs a message to indicate the retrieval of the SVN commit message. Executes a shell command - * that accesses the git directory specified by the WPT_PREPARE_DIR environment variable, - * fetches the latest commit message, and trims any whitespace from the message. - */ -log_message( 'Getting SVN message' ); -$message = trim( exec( 'git --git-dir=' . escapeshellarg( $runner_vars['WPT_PREPARE_DIR'] ) . '/.git log -1 --pretty=%B | head -1' ) ); +foreach ( $runner_vars['WPT_PHP_EXECUTABLES'] as $php ) { + $paths = get_php_run_paths( $runner_vars, $php ); -/** - * Prepares the file path for copying the junit.xml results. - * Logs a message indicating the start of the operation to copy junit.xml results. - * Constructs the file path to the junit.xml file(s) located in the test directory, - * making use of the WPT_TEST_DIR environment variable. The path is sanitized to be - * safely used in shell commands. - */ -log_message( 'Copying junit.xml results' ); -$junit_location = escapeshellarg( $runner_vars['WPT_TEST_DIR'] ) . '/tests/phpunit/build/logs/*'; -/** - * Modifies the junit.xml results file path for a remote location if an SSH connection is available. - * If the WPT_SSH_CONNECT environment variable is not empty, indicating that an SSH connection - * is configured, this snippet adapts the junit_location variable to include the necessary SSH - * command and options for accessing the remote file system. It concatenates SSH options with the - * remote path to ensure that the junit.xml results can be accessed or copied over SSH. - */ -if ( ! empty( $runner_vars['WPT_SSH_CONNECT'] ) ) { - $junit_location = '-e "ssh ' . $runner_vars['WPT_SSH_OPTIONS'] . '" ' . escapeshellarg( $runner_vars['WPT_SSH_CONNECT'] . ':' . $junit_location ); -} + log_message( 'Reporting results for PHP ' . $php['version'] . ' (' . $php['bin'] . ')' ); -/** - * Sets the options for the rsync command based on the debug mode. - * Initializes the rsync options with the recursive flag. If the debug mode is set to 'verbose', - * appends the 'v' flag to the rsync options to enable verbose output during the rsync operation, - * providing more detailed information about the file transfer process. - */ -$rsync_options = '-r'; + log_message( 'Getting SVN Revision' ); + $rev = exec( 'git --git-dir=' . escapeshellarg( $paths['prepare_dir'] ) . '/.git log -1 --pretty=%B | grep "git-svn-id:" | cut -d " " -f 2 | cut -d "@" -f 2' ); -if ( $runner_vars['WPT_DEBUG'] ) { - $rsync_options = $rsync_options . 'v'; -} + log_message( 'Getting SVN message' ); + $message = trim( exec( 'git --git-dir=' . escapeshellarg( $paths['prepare_dir'] ) . '/.git log -1 --pretty=%B | head -1' ) ); -/** - * Constructs the rsync command for executing the synchronization of junit.xml files. - * Concatenates the rsync command with the previously defined options and the source and - * destination paths. The destination path is sanitized for shell execution. This command is - * then passed to the `perform_operations` function, which executes the command to synchronize - * the junit.xml files from the source to the destination directory. - */ -$junit_exec = 'rsync ' . $rsync_options . ' ' . $junit_location . ' ' . escapeshellarg( $runner_vars['WPT_PREPARE_DIR'] ); -perform_operations( - array( - $junit_exec, - ) -); + log_message( 'Copying junit.xml results' ); + $junit_location = escapeshellarg( $paths['test_dir'] ) . '/tests/phpunit/build/logs/*'; -/** - * Processes and uploads the junit.xml file. - * First, a log message is recorded to indicate the start of processing the junit.xml file. - * Then, the contents of the junit.xml file are read from the prepared directory into a string. - * This XML string is then passed to a function that processes the XML data, presumably to prepare - * it for upload or to extract relevant test run information. - */ -log_message( 'Processing and uploading junit.xml' ); -$xml = file_get_contents( $runner_vars['WPT_PREPARE_DIR'] . '/junit.xml' ); -$results = process_junit_xml( $xml ); + if ( ! empty( $runner_vars['WPT_SSH_CONNECT'] ) ) { + $junit_location = '-e "ssh ' . $runner_vars['WPT_SSH_OPTIONS'] . '" ' . escapeshellarg( $runner_vars['WPT_SSH_CONNECT'] . ':' . $junit_location ); + } -/** - * Retrieves environment details from a JSON file or generates them if not available. - * Initializes the environment details string. If an 'env.json' file exists in the prepared - * directory, its contents are read into the environment details string. If the file doesn't - * exist but the prepared directory is the same as the test directory, the environment details - * are generated by calling a function that retrieves these details, then encoded into JSON format. - */ -$env = ''; -if ( file_exists( $runner_vars['WPT_PREPARE_DIR'] . '/env.json' ) ) { - $env = file_get_contents( $runner_vars['WPT_PREPARE_DIR'] . '/env.json' ); -} elseif ( $runner_vars['WPT_PREPARE_DIR'] === $runner_vars['WPT_TEST_DIR'] ) { - $env = json_encode( get_env_details(), JSON_PRETTY_PRINT ); -} + $rsync_options = '-r'; -/** - * Attempts to upload test results if an API key is available, otherwise logs the results locally. - * Checks if an API key for reporting is present. If so, it attempts to upload the test results - * using the `upload_results` function and processes the HTTP response. A success message is logged - * if the upload is successful, indicated by a 20x HTTP status code. If the upload fails, an error - * message is logged along with the HTTP status. If no API key is provided, it logs the test results - * and environment details locally. - */ -if ( ! empty( $runner_vars['WPT_REPORT_API_KEY'] ) ) { + if ( $runner_vars['WPT_DEBUG'] ) { + $rsync_options = $rsync_options . 'v'; + } - // Upload the results and capture the HTTP status and response body - list( $http_status, $response_body ) = upload_results( $results, $rev, $message, $env, $runner_vars['WPT_REPORT_API_KEY'] ); + $junit_exec = 'rsync ' . $rsync_options . ' ' . $junit_location . ' ' . escapeshellarg( $paths['prepare_dir'] ); + perform_operations( + array( + $junit_exec, + ) + ); + + log_message( 'Processing and uploading junit.xml' ); + $xml = file_get_contents( $paths['prepare_dir'] . '/junit.xml' ); + $results = process_junit_xml( $xml ); + + $env = ''; + if ( file_exists( $paths['prepare_dir'] . '/env.json' ) ) { + $env = file_get_contents( $paths['prepare_dir'] . '/env.json' ); + } elseif ( $paths['prepare_dir'] === $paths['test_dir'] ) { + $env = json_encode( get_env_details(), JSON_PRETTY_PRINT ); + } - // Decode the JSON response body - $response = json_decode( $response_body, true ); - if ( 20 == substr( $http_status, 0, 2 ) ) { // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual + if ( ! empty( $runner_vars['WPT_REPORT_API_KEY'] ) ) { - // Construct and log a success message with a link if provided in the response - $message = 'Results successfully uploaded'; - $message .= isset( $response['link'] ) ? ': ' . $response['link'] : ''; - log_message( $message ); + list( $http_status, $response_body ) = upload_results( $results, $rev, $message, $env, $runner_vars['WPT_REPORT_API_KEY'] ); - } else { + $response = json_decode( $response_body, true ); + if ( 20 == substr( $http_status, 0, 2 ) ) { // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual - // Construct and log an error message with additional details if provided in the response - $message = 'Error uploading results'; - $message .= isset( $response['message'] ) ? ': ' . $response['message'] : ''; - $message .= ' (HTTP status ' . (int) $http_status . ')'; - error_message( $message ); + $message = 'Results successfully uploaded'; + $message .= isset( $response['link'] ) ? ': ' . $response['link'] : ''; + log_message( $message ); - } -} else { + } else { - // Log the test results and environment details locally if no API key is provided - log_message( '[+] TEST RESULTS' . "\n\n" . $results . "\n\n" ); - log_message( '[+] ENVIRONMENT' . "\n\n" . $env . "\n\n" ); + $message = 'Error uploading results'; + $message .= isset( $response['message'] ) ? ': ' . $response['message'] : ''; + $message .= ' (HTTP status ' . (int) $http_status . ')'; + error_message( $message ); + + } + } else { + log_message( '[+] TEST RESULTS' . "\n\n" . $results . "\n\n" ); + log_message( '[+] ENVIRONMENT' . "\n\n" . $env . "\n\n" ); + + } } + +mark_testing_commit_executed(); diff --git a/test.php b/test.php index f5bd251..ffbc552 100644 --- a/test.php +++ b/test.php @@ -21,6 +21,8 @@ */ $runner_vars = setup_runner_env_vars(); +skip_if_no_prepared_environment( $runner_vars ); + // Uses the flavor (usually to test WordPress Multisite) $wpt_flavor_ini = trim( getenv( 'WPT_FLAVOR' ) ); switch ( $wpt_flavor_ini ) { @@ -57,26 +59,26 @@ } unset( $wpt_extratests_ini ); -/** - * Determines the PHPUnit command to execute the test suite. - * Retrieves the PHPUnit command from the environment variable 'WPT_PHPUNIT_CMD'. If the environment - * variable is not set or is empty, it constructs a default command using the PHP executable path and - * the test directory path from environment variables, appending parameters to the PHPUnit call to - * avoid reporting useless tests. - */ -$wpt_phpunit_cmd = trim( getenv( 'WPT_PHPUNIT_CMD' ) ); -if ( empty( $wpt_phpunit_cmd ) ) { - $wpt_phpunit_cmd = 'cd ' . escapeshellarg( $runner_vars['WPT_TEST_DIR'] ) . ' && ' . $runner_vars['WPT_PHP_EXECUTABLE'] . ' ./vendor/phpunit/phpunit/phpunit --dont-report-useless-tests' . $wpt_flavor_txt . $wpt_extratests_txt; -} +$custom_phpunit_cmd = trim( getenv( 'WPT_PHPUNIT_CMD' ) ); -// If an SSH connection string is provided, prepend the SSH command to the PHPUnit execution command. -if ( ! empty( $runner_vars['WPT_SSH_CONNECT'] ) ) { - $wpt_phpunit_cmd = 'ssh ' . $runner_vars['WPT_SSH_OPTIONS'] . ' ' . escapeshellarg( $runner_vars['WPT_SSH_CONNECT'] ) . ' ' . escapeshellarg( $wpt_phpunit_cmd ); -} +foreach ( $runner_vars['WPT_PHP_EXECUTABLES'] as $php ) { + $paths = get_php_run_paths( $runner_vars, $php ); -// Execute the PHPUnit command. -perform_operations( - array( - $wpt_phpunit_cmd, - ) -); + log_message( 'Running tests for PHP ' . $php['version'] . ' (' . $php['bin'] . ')' ); + + if ( '' !== $custom_phpunit_cmd && '' === $php['suffix'] ) { + $wpt_phpunit_cmd = $custom_phpunit_cmd; + } else { + $wpt_phpunit_cmd = 'cd ' . escapeshellarg( $paths['test_dir'] ) . ' && ' . $php['bin'] . ' ./vendor/phpunit/phpunit/phpunit --dont-report-useless-tests' . $wpt_flavor_txt . $wpt_extratests_txt; + } + + if ( ! empty( $runner_vars['WPT_SSH_CONNECT'] ) ) { + $wpt_phpunit_cmd = 'ssh ' . $runner_vars['WPT_SSH_OPTIONS'] . ' ' . escapeshellarg( $runner_vars['WPT_SSH_CONNECT'] ) . ' ' . escapeshellarg( $wpt_phpunit_cmd ); + } + + perform_operations( + array( + $wpt_phpunit_cmd, + ) + ); +}