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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .readme-partials/USING.md
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,8 @@ Docker is not used on Windows, when collecting code coverage, when Docker is not

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 are removed when the run ends, also when it is stopped with Ctrl+C or a termination signal (with the `pcntl` extension). Containers of a run that was killed outright are removed by the next run on the same host, which only removes those whose run is no longer going.

The containers run the same PHP version as the host, unless `WP_CLI_TEST_DOCKER_PHP` sets another one:
```bash
WP_CLI_TEST_DOCKER=1 WP_CLI_TEST_DOCKER_PHP=7.4 WP_CLI_TEST_PARALLEL=4 composer behat
Expand Down
98 changes: 94 additions & 4 deletions utils/behat-parallel.php
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,65 @@ function wp_cli_tests_docker_mounts( $base_dir, $vendor = null ) {
return $mounts;
}

/**
* Get the label value that marks the Docker containers of this run, by host and process.
*
* @return string
*/
function wp_cli_tests_docker_owner() {
return php_uname( 'n' ) . ':' . getmypid();
}

/**
* Get whether a process still runs.
*
* @param int $pid
* @return bool|null Null if that cannot be told.
*/
function wp_cli_tests_process_exists( $pid ) {
if ( function_exists( 'posix_kill' ) ) {
// A process of another user cannot be signalled, but exists.
return posix_kill( $pid, 0 ) || ( function_exists( 'posix_get_last_error' ) && 1 === posix_get_last_error() );
}

if ( is_dir( '/proc/self' ) ) {
return is_dir( "/proc/{$pid}" );
}

return null;
}

/**
* Remove the containers that earlier runs on this host left behind, such as when they were killed.
*
* Only containers labelled by a run whose process is gone are removed, so runs that are still
* going keep theirs, as do containers this script did not start.
*/
function wp_cli_tests_docker_remove_abandoned() {
exec( 'docker ps -a --filter label=wp-cli-tests.owner --format ' . escapeshellarg( '{{.ID}} {{.Label "wp-cli-tests.owner"}}' ) . ' 2>/dev/null', $lines, $exit_code );
if ( 0 !== $exit_code ) {
return;
}

$host = php_uname( 'n' );
$abandoned = [];
foreach ( $lines as $line ) {
$parts = explode( ' ', trim( $line ), 2 );
if ( 2 !== count( $parts ) || ! preg_match( '/^(.+):(\d+)$/', $parts[1], $matches ) || $host !== $matches[1] || getmypid() === (int) $matches[2] ) {
continue;
}

if ( false === wp_cli_tests_process_exists( (int) $matches[2] ) ) {
$abandoned[] = $parts[0];
}
}

if ( $abandoned ) {
exec( 'docker rm -f ' . implode( ' ', array_map( 'escapeshellarg', $abandoned ) ) . ' > /dev/null 2>&1' );
fwrite( STDERR, sprintf( "Removed %d Docker containers that earlier runs left behind.\n", count( $abandoned ) ) );
}
}

/**
* Start the containers of a worker and get the command that runs its scenarios.
*
Expand All @@ -464,10 +523,11 @@ function wp_cli_tests_docker_mounts( $base_dir, $vendor = null ) {
*/
function wp_cli_tests_docker_worker_command( $name, $php_image, $db_image, array $mounts, array $user_args, $scenario_list ) {
$network = '';
$label = ' --label ' . escapeshellarg( 'wp-cli-tests.owner=' . wp_cli_tests_docker_owner() );

if ( null !== $db_image ) {
exec(
'docker run -d --rm --name ' . escapeshellarg( "{$name}-db" )
'docker run -d --rm --name ' . escapeshellarg( "{$name}-db" ) . $label
. ' --tmpfs /var/lib/mysql'
. ' -e MYSQL_ROOT_PASSWORD=root -e MYSQL_USER=wp_cli_test -e MYSQL_PASSWORD=password1'
. ' ' . escapeshellarg( $db_image ) . ' ' . wp_cli_tests_docker_db_options( $db_image ) . ' 2>&1'
Expand All @@ -489,7 +549,7 @@ function wp_cli_tests_docker_worker_command( $name, $php_image, $db_image, array
}
}

$command = 'docker run --rm --name ' . escapeshellarg( $name ) . $network
$command = 'docker run --rm --name ' . escapeshellarg( $name ) . $label . $network
. ' --tmpfs /tmp:exec,mode=1777'
. ' -u ' . escapeshellarg( getmyuid() . ':' . getmygid() )
. ' -w ' . escapeshellarg( (string) getcwd() );
Expand Down Expand Up @@ -596,6 +656,28 @@ function wp_cli_tests_docker_worker_command( $name, $php_image, $db_image, array
}
}

if ( $docker ) {
wp_cli_tests_docker_remove_abandoned();

// Ending the run through exit() on Ctrl+C or a termination signal removes its containers.
if ( function_exists( 'pcntl_async_signals' ) ) {
pcntl_async_signals( true );
$exit_codes = [
SIGINT => 130,
SIGTERM => 143,
SIGHUP => 129,
];
foreach ( $exit_codes as $signal => $code ) {
pcntl_signal(
$signal,
static function () use ( $code ) {
exit( $code );
}
);
}
}
}

// Kept between runs, so that the workers' caches stay warm and failed scenarios can be inspected.
$base_dir = sys_get_temp_dir() . '/wp-cli-test-parallel-' . substr( md5( (string) getcwd() ), 0, 8 );

Expand All @@ -610,8 +692,16 @@ static function () use ( $name ) {
exec( 'docker rm -f ' . escapeshellarg( $name ) . ' ' . escapeshellarg( "{$name}-db" ) . ' > /dev/null 2>&1' );
}
);
passthru( wp_cli_tests_docker_worker_command( $name, (string) $php_image, $db_image, wp_cli_tests_docker_mounts( $base_dir, $vendor ), $user_args, null ), $exit_code );
exit( $exit_code );
// Waiting in PHP rather than in passthru() lets the signal handlers run.
$proc = proc_open( wp_cli_tests_docker_worker_command( $name, (string) $php_image, $db_image, wp_cli_tests_docker_mounts( $base_dir, $vendor ), $user_args, null ), [ STDIN, STDOUT, STDERR ], $pipes );
if ( ! is_resource( $proc ) ) {
exit( 1 );
}
do {
usleep( 200000 );
$status = proc_get_status( $proc );
} while ( $status['running'] );
exit( $status['exitcode'] );
}

require getcwd() . '/vendor/autoload.php';
Expand Down
Loading