diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 402e66d0..357741db 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -22,6 +22,7 @@ jobs: uses: alleyinteractive/.github/.github/workflows/php-tests.yml@main name: "P${{ matrix.php }} - ${{ matrix.wordpress }} ${{ matrix.multisite && 'Multisite' || '' }} - ${{ matrix.dependencies }}" with: + command: 'phpunit' dependency-versions: ${{ matrix.dependencies }} multisite: ${{ matrix.multisite }} php: ${{ matrix.php }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 099c52e9..38848a3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## Unreleased +## v1.1.0 - 2024-09-23 ### Added @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Allow the block factory to override text when generating blocks. - Added new `defer()` helper. - Added `Cache::flexible()` method to add SWR support to the cache. +- Added support for parallel unit testing with `brianium/paratest` (in beta). - Added dynamic creation of post type/taxonomy factories. - Added `Reset_Server` trait to reset the server between tests. - Add `with_https()` to control if the request being tested is over HTTPS. diff --git a/composer.json b/composer.json index d9d83697..b701143b 100644 --- a/composer.json +++ b/composer.json @@ -57,6 +57,7 @@ "phpstan/phpstan": "1.10.67", "phpunit/phpunit": "^9.3.3 || ^10.0.7 || ^11.0", "rector/rector": "^1.0", + "spatie/ray": "^1.41", "squizlabs/php_codesniffer": "^3.7", "symplify/monorepo-builder": "^10.3.3", "szepeviktor/phpstan-wordpress": "^1.3" @@ -156,5 +157,8 @@ "@rector", "@phpunit" ] + }, + "suggest": { + "brianium/paratest": "Run PHPUnit tests in parallel" } } diff --git a/monorepo-builder.php b/monorepo-builder.php index fefd1b91..45a2cac0 100644 --- a/monorepo-builder.php +++ b/monorepo-builder.php @@ -50,7 +50,7 @@ [ ComposerJsonSection::REQUIRE => [ 'alleyinteractive/composer-wordpress-autoloader' => '^1.0', - 'php' => '^8.1', + 'php' => '^8.2', ], ComposerJsonSection::REQUIRE_DEV => [ 'alleyinteractive/alley-coding-standards' => '^2.0', diff --git a/src/mantle/database/factory/concerns/trait-resolves-factories.php b/src/mantle/database/factory/concerns/trait-resolves-factories.php index 37066b9f..3ccf0400 100644 --- a/src/mantle/database/factory/concerns/trait-resolves-factories.php +++ b/src/mantle/database/factory/concerns/trait-resolves-factories.php @@ -8,7 +8,7 @@ namespace Mantle\Database\Factory\Concerns; use InvalidArgumentException; -use Mantle\Application\Application; +use Mantle\Contracts\Application; use Mantle\Container\Container; use Mantle\Database\Factory; use Mantle\Database\Model; @@ -152,9 +152,13 @@ public static function default_factory_name( string $model_name ): string { */ protected static function app_namespace(): string { try { - return str( - Container::get_instance()->make( Application::class )->get_namespace() - )->rtrim( '\\' )->append( '\\' ); + $container = Container::get_instance(); + + if ( $container instanceof Application ) { + return str( $container->get_namespace() )->rtrim( '\\' )->append( '\\' ); + } + + return 'App\\'; } catch ( \Throwable ) { return 'App\\'; } diff --git a/src/mantle/facade/class-cache.php b/src/mantle/facade/class-cache.php index 585d717c..5a7d4210 100644 --- a/src/mantle/facade/class-cache.php +++ b/src/mantle/facade/class-cache.php @@ -10,9 +10,9 @@ /** * Cache Facade * - * @method static mixed get(string $key, mixed $default = null) + * @method static mixed|null get(string $key, mixed $default = null) * @method static iterable get_multiple(iterable $keys, mixed $default = null) - * @method static mixed flexible(string $key, int|\DateInterval|\DateTimeInterface|null $stale, int|\DateInterval|\DateTimeInterface|null $expire, callable $callback) + * @method static mixed|null flexible(string $key, int|\DateInterval|\DateTimeInterface|null $stale, int|\DateInterval|\DateTimeInterface|null $expire, callable $callback) * @method static mixed|null pull(string $key, mixed $default = null) * @method static bool set(string $key, mixed $value, null|int|\DateInterval $ttl = null) * @method static bool set_multiple(iterable $values, null|int|\DateInterval|\DateTimeInterface $ttl = null) diff --git a/src/mantle/testing/class-utils.php b/src/mantle/testing/class-utils.php index 56e5d258..b0742835 100644 --- a/src/mantle/testing/class-utils.php +++ b/src/mantle/testing/class-utils.php @@ -236,6 +236,15 @@ public static function setup_configuration(): void { defined( 'WP_PHP_BINARY' ) || define( 'WP_PHP_BINARY', 'php' ); defined( 'WPLANG' ) || define( 'WPLANG', '' ); + // Setup the table prefix when running in parallel. + if ( static::is_parallel() && $token = static::parallel_token() ) { + $table_prefix .= "para_{$token}_"; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited + + if ( static::is_debug_mode() ) { + static::info( "Using parallel table prefix: {$table_prefix}" ); + } + } + // phpcs:enable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound } @@ -515,4 +524,29 @@ public static function handle_shutdown(): void { exit( 1 ); } + + /** + * Check if the current test run is parallel with paratest. + */ + public static function is_parallel(): bool { + return ! empty( static::parallel_token() ); + } + + /** + * Retrieve the parallel token for the current test run. + * + * @return string + */ + public static function parallel_token(): ?string { + return static::env( 'TEST_TOKEN', null ); + } + + /** + * Check if the current test run is the paratest bootstrap. + * + * The parallel token will not be set in the initial bootstrap. + */ + public static function is_parallel_bootstrap(): bool { + return empty( static::parallel_token() ) && isset( $_SERVER['SCRIPT_NAME'] ) && str_contains( (string) $_SERVER['SCRIPT_NAME'], 'paratest' ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + } } diff --git a/src/mantle/testing/composer.json b/src/mantle/testing/composer.json index 4fa2d201..83c72305 100644 --- a/src/mantle/testing/composer.json +++ b/src/mantle/testing/composer.json @@ -34,6 +34,7 @@ "nunomaduro/collision": "For better PHPUnit printing.", "mantle-framework/console": "Required to assert console commands.", "mantle-framework/testkit": "For running tests against a WordPress install without Mantle", + "brianium/paratest": "For running tests in parallel.", "phpunit/phpunit": "Required to use assertions and run tests." }, "config": { diff --git a/src/mantle/testing/wordpress-bootstrap.php b/src/mantle/testing/wordpress-bootstrap.php index 8111a546..0ca65939 100644 --- a/src/mantle/testing/wordpress-bootstrap.php +++ b/src/mantle/testing/wordpress-bootstrap.php @@ -143,6 +143,12 @@ define( 'WP_DEFAULT_THEME', Utils::env( 'WP_DEFAULT_THEME', 'default' ) ); } +// Bail early if this is this is a parallel test bootstrap: installation of +// WordPress is handled in each process. +if ( Utils::is_parallel_bootstrap() ) { + return; +} + $wp_theme_directories = []; $installing_wp = defined( 'WP_INSTALLING' ) && WP_INSTALLING; diff --git a/src/mantle/testkit/class-application.php b/src/mantle/testkit/class-application.php index ccee4a9f..aba55e86 100644 --- a/src/mantle/testkit/class-application.php +++ b/src/mantle/testkit/class-application.php @@ -104,6 +104,8 @@ class Application extends Container implements Application_Contract { * @param string $root_url Root URL of the application. */ public function __construct( string $base_path = '', string $root_url = null ) { + static::$instance = $this; + if ( empty( $base_path ) && defined( 'MANTLE_BASE_DIR' ) ) { $base_path = \MANTLE_BASE_DIR; } diff --git a/tests/Database/Factory/UnitTestingFactoryTest.php b/tests/Database/Factory/UnitTestingFactoryTest.php index 81e448bf..ded85d99 100644 --- a/tests/Database/Factory/UnitTestingFactoryTest.php +++ b/tests/Database/Factory/UnitTestingFactoryTest.php @@ -297,7 +297,9 @@ protected function shim_test( string $class_name, string $property ) { * @dataProvider dataprovider_factory */ #[DataProvider( 'dataprovider_factory' )] - public function test_dataprovider_factory( $post ) { + public function test_dataprovider_factory( callable $fn ) { + $post = $fn(); + $this->assertInstanceOf( \WP_Post::class, $post ); $this->assertStringContainsString( '