diff --git a/composer.json b/composer.json index 935009eeb..cfa8a950b 100644 --- a/composer.json +++ b/composer.json @@ -111,7 +111,10 @@ } }, "scripts": { - "lint": "@phpcs", + "lint": [ + "@phpcs", + "@phpstan" + ], "lint:fix": "@phpcbf", "merge": "monorepo-builder merge", "phpcbf": "phpcbf --standard=./phpcs.xml .", diff --git a/src/mantle/support/class-collection.php b/src/mantle/support/class-collection.php index 1d3e87b16..bde2fc646 100644 --- a/src/mantle/support/class-collection.php +++ b/src/mantle/support/class-collection.php @@ -583,8 +583,8 @@ public function has( $key ) { /** * Concatenate values of a given key as a string. * - * @param callable|string $value - * @param string|null $glue + * @param callable|string|null $value + * @param string|null $glue * @return string */ public function implode( $value, $glue = null ) { diff --git a/src/mantle/testing/class-installation-manager.php b/src/mantle/testing/class-installation-manager.php index 5cc396f50..d554467ef 100644 --- a/src/mantle/testing/class-installation-manager.php +++ b/src/mantle/testing/class-installation-manager.php @@ -57,26 +57,19 @@ public function before( ?callable $callback ) { * Define a callback to be invoked after installation. * * @param callable|null $callback Callback to invoke after installation. + * @param bool $append Whether to append the callback to the list or prepend it. * @return static */ - public function after( ?callable $callback ) { + public function after( ?callable $callback, bool $append = true ) { if ( is_callable( $callback ) ) { - $this->after_install_callbacks[] = $callback; + $append + ? $this->after_install_callbacks[] = $callback + : array_unshift( $this->after_install_callbacks, $callback ); } return $this; } - /** - * Define a callback to be invoked using the 'muplugins_loaded' hook. - * - * @param callable $callback Callback to invoke on 'muplugins_loaded'. - * @return static - */ - public function loaded( ?callable $callback ) { - return $this->on( 'muplugins_loaded', $callback ); - } - /** * Define a callback for a specific WordPress hook. * @@ -94,6 +87,48 @@ public function on( string $hook, ?callable $callback, int $priority = 10, int $ return $this; } + /** + * Define a callback to be invoked using the 'muplugins_loaded' hook. + * + * @param callable $callback Callback to invoke on 'muplugins_loaded'. + * @return static + */ + public function loaded( ?callable $callback ) { + return $this->on( 'muplugins_loaded', $callback ); + } + + /** + * Define a callback to be invoked on 'init'. + * + * @param callable $callback Callback to invoke on 'init'. + * @return static + */ + public function init( ?callable $callback ) { + return $this->loaded( + fn () => $this->on( 'init', $callback ) + ); + } + + /** + * Define the active theme to be set after the installation is loaded. + * + * @param string $theme Theme name. + * @return static + */ + public function theme( string $theme ) { + return $this->loaded( fn () => switch_theme( $theme ) ); + } + + /** + * Define the active plugins to be set after the installation is loaded. + * + * @param array $plugins Plugin files. + * @return static + */ + public function plugins( array $plugins ) { + return $this->loaded( fn () => update_option( 'active_plugins', $plugins ) ); + } + /** * Install the Mantle Testing Framework. * diff --git a/src/mantle/testing/class-utils.php b/src/mantle/testing/class-utils.php index 4548bde0d..e240be100 100644 --- a/src/mantle/testing/class-utils.php +++ b/src/mantle/testing/class-utils.php @@ -9,6 +9,8 @@ use Mantle\Support\Str; use Mantle\Testing\Doubles\Spy_REST_Server; + +use function Mantle\Support\Helpers\collect; use function Termwind\render; require_once __DIR__ . '/concerns/trait-output-messages.php'; @@ -255,10 +257,14 @@ public static function env( string $variable, $default ) { * `escapeshellarg()` doesn't fit here because the script is expecting * unquoted arguments. * - * @param string $string String to sanitize. + * @param string|bool $string String to sanitize. * @return string */ - public static function shell_safe( string $string ): string { + public static function shell_safe( string|bool $string ): string { + if ( is_bool( $string ) ) { + return $string ? 'true' : 'false'; + } + return empty( trim( $string ) ) ? "''" : $string; } @@ -269,18 +275,30 @@ public static function shell_safe( string $string ): string { * not install the WordPress database. * * @param string $directory Directory to install WordPress in. + * @param bool $install_vip_mu_plugins Whether to install VIP MU plugins. + * @param bool $install_object_cache Whether to install the object cache drop-in. */ - public static function install_wordpress( string $directory ): void { + public static function install_wordpress( string $directory, bool $install_vip_mu_plugins = false, bool $install_object_cache = false ) { + $branch = static::env( 'MANTLE_CI_BRANCH', 'HEAD' ); + $command = sprintf( - 'export WP_CORE_DIR=%s && curl -s %s | bash -s %s %s %s %s %s %s', + 'export WP_CORE_DIR=%s WP_MULTISITE=%s INSTALL_WP_TEST_DEBUG=%s && curl -s %s | bash -s %s', $directory, - 'https://raw.githubusercontent.com/alleyinteractive/mantle-ci/HEAD/install-wp-tests.sh', - static::shell_safe( defined( 'DB_NAME' ) ? DB_NAME : static::env( 'WP_DB_NAME', static::DEFAULT_DB_NAME ) ), - static::shell_safe( defined( 'DB_USER' ) ? DB_USER : static::env( 'WP_DB_USER', static::DEFAULT_DB_USER ) ), - static::shell_safe( defined( 'DB_PASSWORD' ) ? DB_PASSWORD : static::env( 'WP_DB_PASSWORD', static::DEFAULT_DB_PASSWORD ) ), - static::shell_safe( defined( 'DB_HOST' ) ? DB_HOST : static::env( 'WP_DB_HOST', static::DEFAULT_DB_HOST ) ), - static::shell_safe( static::env( 'WP_VERSION', 'latest' ) ), - static::shell_safe( static::env( 'WP_SKIP_DB_CREATE', 'false' ) ), + static::shell_safe( static::env( 'WP_MULTISITE', '0' ) ), + static::shell_safe( static::is_debug_mode() ), + "https://raw.githubusercontent.com/alleyinteractive/mantle-ci/{$branch}/install-wp-tests.sh", + collect( + [ + static::shell_safe( defined( 'DB_NAME' ) ? DB_NAME : static::env( 'WP_DB_NAME', static::DEFAULT_DB_NAME ) ), + static::shell_safe( defined( 'DB_USER' ) ? DB_USER : static::env( 'WP_DB_USER', static::DEFAULT_DB_USER ) ), + static::shell_safe( defined( 'DB_PASSWORD' ) ? DB_PASSWORD : static::env( 'WP_DB_PASSWORD', static::DEFAULT_DB_PASSWORD ) ), + static::shell_safe( defined( 'DB_HOST' ) ? DB_HOST : static::env( 'WP_DB_HOST', static::DEFAULT_DB_HOST ) ), + static::shell_safe( static::env( 'WP_VERSION', 'latest' ) ), + static::shell_safe( static::env( 'WP_SKIP_DB_CREATE', 'false' ) ), + static::shell_safe( $install_vip_mu_plugins ? 'true' : 'false' ), + static::shell_safe( $install_object_cache ? 'true' : 'false' ), + ] + )->implode( ' ' ), ); $retval = 0; @@ -299,6 +317,10 @@ public static function install_wordpress( string $directory ): void { * @return bool */ public static function is_debug_mode(): bool { + if ( defined( 'MANTLE_TESTING_DEBUG' ) && MANTLE_TESTING_DEBUG ) { + return true; + } + return ! empty( array_intersect( (array) ( $_SERVER['argv'] ?? [] ), // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized diff --git a/src/mantle/testing/concerns/trait-rsync-installation.php b/src/mantle/testing/concerns/trait-rsync-installation.php index 776110696..4bc55defa 100644 --- a/src/mantle/testing/concerns/trait-rsync-installation.php +++ b/src/mantle/testing/concerns/trait-rsync-installation.php @@ -25,6 +25,8 @@ * the theme to a WordPress installation without needing to run a bash script. * * After the rsync is complete, PHPUnit will be rerun from the new location. + * + * @mixin \Mantle\Testing\Installation_Manager */ trait Rsync_Installation { use Conditionable; @@ -44,13 +46,27 @@ trait Rsync_Installation { protected ?string $rsync_from = null; /** - * Subdirectory from the parent folder being rsynced to the previous working + * Subdirectory from the parent folder being rsync-ed to the previous working * directory. * * @var string */ protected ?string $rsync_subdir = ''; + /** + * Flag to install the VIP MU plugins. + * + * @var boolean + */ + protected bool $install_vip_mu_plugins = false; + + /** + * Flag to install a Memcache object cache drop-in. + * + * @var boolean + */ + protected bool $install_object_cache = false; + /** * Exclusions to be used when rsyncing the codebase. * @@ -58,13 +74,14 @@ trait Rsync_Installation { */ protected array $rsync_exclusions = [ '.buddy-tests', - '.composer', + '.buddy', '.composer', '.git', '.npm', '.phpcs', '.turbo', 'node_modules', + 'phpstan.neon', ]; /** @@ -131,6 +148,61 @@ public function maybe_rsync_wp_content(): static { return $this->maybe_rsync( '/', dirname( getcwd(), 3 ) ); } + /** + * Attempt to install VIP's built mu-plugins into the codebase. + * + * Will only be applied if the codebase is not already within a WordPress and + * is being rsync-ed to one. + * + * @param bool $install Install VIP's built mu-plugins into the codebase. + * @return static + */ + public function with_vip_mu_plugins( bool $install = true ): static { + if ( $this->is_within_wordpress_install() ) { + return $this; + } + + $this->rsync_exclusions[] = 'mu-plugins'; + + $this->install_vip_mu_plugins = $install; + + return $this; + } + + /** + * Attempt to install the object cache drop-in into the codebase. + * + * Will only be applied if the codebase is not already within a WordPress and + * is being rsync-ed to one. + * + * @param bool $install Install the object cache drop-in into the codebase. + * @return static + */ + public function with_object_cache( bool $install = true ): static { + if ( $this->is_within_wordpress_install() ) { + return $this; + } + + // Check if Memcached is installed. + if ( ! class_exists( \Memcached::class ) ) { + // Allow the object cache to be forcefully required. + if ( Utils::env( 'MANTLE_REQUIRE_OBJECT_CACHE', false ) ) { + Utils::error( 'Memcached is not installed. Cannot install object cache. Exiting...' ); + exit( 1 ); + } + + Utils::error( 'Memcached is not installed. Cannot install object cache. Skipping...' ); + + return $this; + } + + $this->rsync_exclusions[] = 'object-cache.php'; + + $this->install_object_cache = $install; + + return $this; + } + /** * Maybe rsync the codebase as a plugin within WordPress. * @@ -236,7 +308,11 @@ protected function perform_rsync_testsuite() { exit( 1 ); } - Utils::install_wordpress( $base_install_path ); + Utils::install_wordpress( + directory: $base_install_path, + install_vip_mu_plugins: $this->install_vip_mu_plugins, + install_object_cache: $this->install_object_cache, + ); Utils::success( "WordPress installed at {$base_install_path}", diff --git a/src/mantle/testing/wordpress-bootstrap.php b/src/mantle/testing/wordpress-bootstrap.php index 91a042155..d46f894d2 100644 --- a/src/mantle/testing/wordpress-bootstrap.php +++ b/src/mantle/testing/wordpress-bootstrap.php @@ -101,6 +101,11 @@ define( 'WP_MEMORY_LIMIT', -1 ); define( 'WP_MAX_MEMORY_LIMIT', -1 ); +// Disable VIP GO cache purging during testing. +if ( ! defined( 'VIP_GO_DISABLE_CACHE_PURGING' ) ) { + define( 'VIP_GO_DISABLE_CACHE_PURGING', true ); +} + $PHP_SELF = '/index.php'; $GLOBALS['PHP_SELF'] = '/index.php'; $_SERVER['PHP_SELF'] = '/index.php'; diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 2508671c1..cb1efc318 100755 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -10,7 +10,10 @@ define( 'MANTLE_PHPUNIT_INCLUDES_PATH', __DIR__ . '/includes' ); define( 'MANTLE_PHPUNIT_FIXTURES_PATH', __DIR__ . '/fixtures' ); define( 'MANTLE_PHPUNIT_TEMPLATE_PATH', __DIR__ . '/template-parts' ); +define( 'MANTLE_TESTING_DEBUG', true ); \Mantle\Testing\manager() ->maybe_rsync_plugin() + ->with_object_cache() + ->with_vip_mu_plugins() ->install();