From 75d5a720c52d026f5b7e47c2a3ed5a622abffcb6 Mon Sep 17 00:00:00 2001 From: Luca Tumedei Date: Sun, 8 Sep 2024 16:04:15 +0200 Subject: [PATCH 1/4] fix(LoadSandbox) ensure defined db is used --- src/Module/WPLoader.php | 2 +- src/WordPress/LoadSandbox.php | 60 ++++++++++++++++++++++++++++++++++- 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/src/Module/WPLoader.php b/src/Module/WPLoader.php index e9ca48129..c89e03f69 100644 --- a/src/Module/WPLoader.php +++ b/src/Module/WPLoader.php @@ -620,7 +620,7 @@ public function _loadWordPress(?bool $loadOnly = null): void if ($loadOnly) { Dispatcher::dispatch(self::EVENT_BEFORE_LOADONLY, $this); $loadSandbox = new LoadSandbox($this->installation->getWpRootDir(), $this->config['domain']); - $loadSandbox->load(); + $loadSandbox->load($this->db); Dispatcher::dispatch(self::EVENT_AFTER_LOADONLY, $this); } else { $this->installAndBootstrapInstallation(); diff --git a/src/WordPress/LoadSandbox.php b/src/WordPress/LoadSandbox.php index a7bae3e2b..76e2f1434 100644 --- a/src/WordPress/LoadSandbox.php +++ b/src/WordPress/LoadSandbox.php @@ -2,6 +2,11 @@ namespace lucatume\WPBrowser\WordPress; +use lucatume\WPBrowser\Utils\MonkeyPatch; +use lucatume\WPBrowser\Utils\Property; +use lucatume\WPBrowser\WordPress\Database\DatabaseInterface; +use lucatume\WPBrowser\WordPress\Database\MysqlDatabase; + class LoadSandbox { private string $wpRootDir; @@ -19,7 +24,7 @@ public function __construct(string $wpRootDir, private string $domain) /** * @throws InstallationException */ - public function load(): void + public function load(?DatabaseInterface $db = null): void { $this->setUpServerVars(); PreloadFilters::addFilter('wp_fatal_error_handler_enabled', [$this, 'returnFalse'], 100); @@ -28,9 +33,62 @@ public function load(): void // Setting the `chunk_size` to `0` means the function will only be called when the output buffer is closed. ob_start([$this, 'obCallback'], 0); + // ISSUE #753 + if ($db instanceof MysqlDatabase) { + // Define the `DB_` constants. + define('DB_NAME', $db->getDbName()); + define('DB_USER', $db->getDbUser()); + define('DB_PASSWORD', $db->getDbPassword()); + define('DB_HOST', $db->getDbHost()); + + // Silence errors about the redeclaration of the `DB_` constants. + $previousErrorHandler = set_error_handler(callback: static function ($errno, $errstr) { + if ($errno === E_USER_ERROR && str_contains($errstr, 'Cannot redeclare') && str_contains($errstr, 'DB_')) { + return true; + } + + return false; + }); + + if(class_exists('\Roots\WPConfig\Config')){ + $configArray = new class implements \ArrayAccess { + private $configMap = []; + + public function offsetExists( $offset ) { + return isset( $this->configMap[ $offset ] ); + } + + public function offsetGet( $offset ) { + return $this->configMap[ $offset ]; + } + + public function offsetSet( $offset, $value ) { + if ( in_array( $offset, [ 'DB_NAME', 'DB_USER', 'DB_PASSWORD', 'DB_HOST' ], true ) ) { + return; + } + $this->configMap[ $offset ] = $value; + } + + public function offsetUnset( $offset ) { + unset( $this->configMap[ $offset ] ); + } + }; + + Property::setPrivateProperties( '\Roots\WPConfig\Config', [ + 'configMap' => $configArray + ] ); + } + // END ISSUE #753 + // Exceptions thrown during loading are not wrapped on purpose to remove debug overhead. include_once $this->wpRootDir . '/wp-load.php'; + // ISSUE #753 + if (!empty($previousErrorHandler)) { + set_error_handler($previousErrorHandler); + } + // END ISSUE #753 + ob_end_clean(); // If this is reached, then WordPress has loaded correctly. remove_filter('wp_fatal_error_handler_enabled', [$this, 'returnFalse'], 100); From aad84dfa0e9ee3150dc7ed40ecc2925599f8d80b Mon Sep 17 00:00:00 2001 From: Luca Tumedei Date: Tue, 10 Sep 2024 08:50:01 +0200 Subject: [PATCH 2/4] fix(Modules/WPLoader) authoritatively set DB_ constants in loadOnly:true --- docs/modules/WPLoader.md | 21 +++++++- src/Module/WPLoader.php | 2 + src/WordPress/LoadSandbox.php | 37 +------------ tests/_support/Traits/InstallationMocks.php | 21 +++++--- .../WPBrowser/Module/WPLoaderLoadOnlyTest.php | 54 +++++++++++++++++++ 5 files changed, 90 insertions(+), 45 deletions(-) diff --git a/docs/modules/WPLoader.md b/docs/modules/WPLoader.md index 87a38311e..ab142524f 100644 --- a/docs/modules/WPLoader.md +++ b/docs/modules/WPLoader.md @@ -1,7 +1,5 @@ ## WPLoader module -// @todo update this - A module to load WordPress and make its code available in tests. Depending on the value of the `loadOnly` configuration parameter, the module will behave differently: @@ -22,6 +20,11 @@ will: * take care of running any test method in a database transaction rolled back after each test * manage and clean up the global environment and context between tests +!!! note + + The module will set the environment variable `WPBROWSER_LOAD_ONLY=0` when running in this mode. This environment variable + can be used to detect whether WordPress is being loaded by WPBrowser and in which mode. + When used in this mode, the module supports the following configuration parameters: * `loadOnly` - `false` to load WordPress and run tests in a controlled environment. @@ -288,6 +291,11 @@ your site to run tests using the default configuration based on PHP built-in ser The module will load WordPress from the location specified by the `wpRootFolder` parameter, relying on [the WPDb module](WPDb.md) to manage the database state. +!!! note + + The module will set the environment variable `WPBROWSER_LOAD_ONLY=1` when running in this mode. This environment variable + can be used to detect whether WordPress is being loaded by WPBrowser and in which mode. + When used in this mode, the module supports the following configuration parameters: * `loadOnly` - `true` to load WordPress and make it available in the context of tests. @@ -300,6 +308,15 @@ When used in this mode, the module supports the following configuration paramete parameters to specify the database connection details. * `domain` - the domain to use when loading WordPress. Equivalent to defining the `WP_TESTS_DOMAIN` constant. +!!! warning + + The module will define the `DB_NAME`, `DB_USER`, `DB_PASSWORD` and `DB_HOST` constants in the context of loading WordPress. + This is done to allow the WordPress database connection to be configured using the `dbUrl` configuration parameter. + **The module will silence the warnings about the redeclaration of these constants**, but in some cases with stricter error + checking (e.g. Bedrock) this may not be enough. In those cases, you can use the `WPBROWSER_LOAD_ONLY` environment + variable to detect whether WordPress is being loaded by WPBrowser and in which mode and configured your installation + accordingly. + The following is an example of the module configuration to run end-to-end tests on the site served at `http://localhost:8080` URL and served from the `/var/wordpress` directory: diff --git a/src/Module/WPLoader.php b/src/Module/WPLoader.php index c89e03f69..3ef869c8f 100644 --- a/src/Module/WPLoader.php +++ b/src/Module/WPLoader.php @@ -618,11 +618,13 @@ public function _loadWordPress(?bool $loadOnly = null): void $this->loadConfigFiles(); if ($loadOnly) { + putenv('WPBROWSER_LOAD_ONLY=1'); Dispatcher::dispatch(self::EVENT_BEFORE_LOADONLY, $this); $loadSandbox = new LoadSandbox($this->installation->getWpRootDir(), $this->config['domain']); $loadSandbox->load($this->db); Dispatcher::dispatch(self::EVENT_AFTER_LOADONLY, $this); } else { + putenv('WPBROWSER_LOAD_ONLY=0'); $this->installAndBootstrapInstallation(); } diff --git a/src/WordPress/LoadSandbox.php b/src/WordPress/LoadSandbox.php index 76e2f1434..a8dad5794 100644 --- a/src/WordPress/LoadSandbox.php +++ b/src/WordPress/LoadSandbox.php @@ -33,61 +33,28 @@ public function load(?DatabaseInterface $db = null): void // Setting the `chunk_size` to `0` means the function will only be called when the output buffer is closed. ob_start([$this, 'obCallback'], 0); - // ISSUE #753 if ($db instanceof MysqlDatabase) { - // Define the `DB_` constants. define('DB_NAME', $db->getDbName()); define('DB_USER', $db->getDbUser()); define('DB_PASSWORD', $db->getDbPassword()); define('DB_HOST', $db->getDbHost()); - // Silence errors about the redeclaration of the `DB_` constants. + // Silence errors about the redeclaration of the above `DB_` constants. $previousErrorHandler = set_error_handler(callback: static function ($errno, $errstr) { - if ($errno === E_USER_ERROR && str_contains($errstr, 'Cannot redeclare') && str_contains($errstr, 'DB_')) { + if ($errno === E_WARNING && preg_match('/^Constant DB_(NAME|USER|PASSWORD|HOST) already defined/i', $errstr)) { return true; } return false; }); - - if(class_exists('\Roots\WPConfig\Config')){ - $configArray = new class implements \ArrayAccess { - private $configMap = []; - - public function offsetExists( $offset ) { - return isset( $this->configMap[ $offset ] ); - } - - public function offsetGet( $offset ) { - return $this->configMap[ $offset ]; - } - - public function offsetSet( $offset, $value ) { - if ( in_array( $offset, [ 'DB_NAME', 'DB_USER', 'DB_PASSWORD', 'DB_HOST' ], true ) ) { - return; - } - $this->configMap[ $offset ] = $value; - } - - public function offsetUnset( $offset ) { - unset( $this->configMap[ $offset ] ); - } - }; - - Property::setPrivateProperties( '\Roots\WPConfig\Config', [ - 'configMap' => $configArray - ] ); } - // END ISSUE #753 // Exceptions thrown during loading are not wrapped on purpose to remove debug overhead. include_once $this->wpRootDir . '/wp-load.php'; - // ISSUE #753 if (!empty($previousErrorHandler)) { set_error_handler($previousErrorHandler); } - // END ISSUE #753 ob_end_clean(); // If this is reached, then WordPress has loaded correctly. diff --git a/tests/_support/Traits/InstallationMocks.php b/tests/_support/Traits/InstallationMocks.php index 241d52d05..f9d102f7f 100644 --- a/tests/_support/Traits/InstallationMocks.php +++ b/tests/_support/Traits/InstallationMocks.php @@ -12,12 +12,17 @@ trait InstallationMocks /** * @return array{0: string, 1: string} */ - private function makeMockConfiguredInstallation(string $phpExtra = ''): array + private function makeMockConfiguredInstallation(string $phpExtra = '', array $overrides = []): array { - $dbUser = Env::get('WORDPRESS_DB_USER'); - $dbPassword = Env::get('WORDPRESS_DB_PASSWORD'); - $dbLocalhostPort = Env::get('WORDPRESS_DB_LOCALHOST_PORT'); - $dbName = Env::get('WORDPRESS_DB_NAME'); + $dbUser = $overrides['dbUser'] ?? Env::get('WORDPRESS_DB_USER'); + $dbPassword = $overrides['dbPassword'] ?? Env::get('WORDPRESS_DB_PASSWORD'); + if(!isset($overrides['dbHost'])){ + $dbLocalhostPort = $overrides['dbLocalhostPort'] ?? Env::get('WORDPRESS_DB_LOCALHOST_PORT'); + $dbHost = '127.0.0.1:' . $dbLocalhostPort; + } else { + $dbHost = $overrides['dbHost']; + } + $dbName = $overrides['dbName'] ?? Env::get('WORDPRESS_DB_NAME'); $wpRootFolder = FS::tmpDir('wploader_', [ 'wp-includes' => [ 'version.php' => <<< PHP @@ -34,7 +39,7 @@ private function makeMockConfiguredInstallation(string $phpExtra = ''): array define('DB_NAME', '$dbName'); define('DB_USER', '$dbUser'); define('DB_PASSWORD', '$dbPassword'); - define('DB_HOST', '127.0.0.1:$dbLocalhostPort'); + define('DB_HOST', '$dbHost'); define('DB_CHARSET', 'utf8'); define('DB_COLLATE', ''); global \$table_prefix; @@ -53,10 +58,10 @@ private function makeMockConfiguredInstallation(string $phpExtra = ''): array 'wp-load.php' => 'assertTrue($module->_didLoadWordPress()); }); } + + public function testWillDefineDBConstantsWhenLoadOnlyTrue(): void{ + [$wpRootFolder] = $this->makeMockConfiguredInstallation('', [ + 'dbUser' => 'production_user', + 'dbPassword' => 'production_password', + 'dbHost' => '10.0.0.1:8876', + 'dbName' => 'test_db', + ]); + file_put_contents($wpRootFolder . '/wp-load.php', ' $testDbUrl, + 'wpRootFolder' => $wpRootFolder, + 'loadOnly' => true, + ]); + + Fork::executeClosure(function () use ($testDbName, $testDbHost, $testDbPassword, $testDbUser, $module) { + // WordPress' functions are stubbed by wordpress-stubs in unit tests: override them to do something. + $did_actions = []; + uopz_set_return('do_action', static function ($action) use (&$did_actions) { + $did_actions[$action] = true; + }, true); + uopz_set_return('did_action', static function ($action) use (&$did_actions) { + return isset($did_actions[$action]); + }, true); + // Partial mocking the function that would load WordPress. + uopz_set_return(WPLoader::class, 'installAndBootstrapInstallation', function () { + return true; + }, true); + + $module->_initialize(); + $module->_beforeSuite(); + + $this->assertTrue($module->_didLoadWordPress()); + $this->assertEquals($testDbUser, DB_USER); + $this->assertEquals($testDbPassword, DB_PASSWORD); + $this->assertEquals($testDbHost, DB_HOST); + $this->assertEquals($testDbName, DB_NAME); + $this->assertEquals('1', getenv('WPBROWSER_LOAD_ONLY')); + }); + } } From 56b7a16980512703a7d99232717c8fe603e48d6c Mon Sep 17 00:00:00 2001 From: Luca Tumedei Date: Tue, 10 Sep 2024 16:55:45 +0200 Subject: [PATCH 3/4] refactor(LoadSandbox) terser code --- src/WordPress/LoadSandbox.php | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/WordPress/LoadSandbox.php b/src/WordPress/LoadSandbox.php index a8dad5794..ffa55dce7 100644 --- a/src/WordPress/LoadSandbox.php +++ b/src/WordPress/LoadSandbox.php @@ -41,11 +41,8 @@ public function load(?DatabaseInterface $db = null): void // Silence errors about the redeclaration of the above `DB_` constants. $previousErrorHandler = set_error_handler(callback: static function ($errno, $errstr) { - if ($errno === E_WARNING && preg_match('/^Constant DB_(NAME|USER|PASSWORD|HOST) already defined/i', $errstr)) { - return true; - } - - return false; + return $errno === E_WARNING + && preg_match('/^Constant DB_(NAME|USER|PASSWORD|HOST) already defined/i', $errstr); }); } From eb1b896a4a9c1d30be2d9ffbefd0dee731c92e16 Mon Sep 17 00:00:00 2001 From: Luca Tumedei Date: Wed, 11 Sep 2024 09:34:56 +0200 Subject: [PATCH 4/4] test(WPLoader) cover loading of config files in loadOnly:true --- docs/modules/WPLoader.md | 4 +- .../WPBrowser/Module/WPLoaderLoadOnlyTest.php | 74 ++++++++++++++++++- 2 files changed, 76 insertions(+), 2 deletions(-) diff --git a/docs/modules/WPLoader.md b/docs/modules/WPLoader.md index ab142524f..8e2c66143 100644 --- a/docs/modules/WPLoader.md +++ b/docs/modules/WPLoader.md @@ -48,7 +48,7 @@ When used in this mode, the module supports the following configuration paramete * `phpBinary` - the path to the PHP binary to use to run tests. Defaults to the `WP_PHP_BINARY` constant. * `language` - the language to use when loading WordPress. Equivalent to defining the `WPLANG` constant. * `configFile` - a configuration file, or a set of configuration files, to load before the tests to further customize - and control the WordPress testing environment. + and control the WordPress testing environment. This file(s) will be loaded before the WordPress installation is loaded. * `pluginsFolder` - the path to the plugins folder to use when loading WordPress. Equivalent to defining the `WP_PLUGIN_DIR` constant. If both this parameter and the `WP_PLUGIN_DIR` parameter are set, the `WP_PLUGIN_DIR` parameter will override the value of this one. @@ -307,6 +307,8 @@ When used in this mode, the module supports the following configuration paramete use a SQLite database. Alternatively, you can use the `dbName`, `dbUser`, `dbPassword`, `dbHost` configuration parameters to specify the database connection details. * `domain` - the domain to use when loading WordPress. Equivalent to defining the `WP_TESTS_DOMAIN` constant. +* `configFile` - a configuration file, or a set of configuration files, to load before the tests to further customize + and control the WordPress testing environment. This file(s) will be loaded before the WordPress installation is loaded. !!! warning diff --git a/tests/unit/lucatume/WPBrowser/Module/WPLoaderLoadOnlyTest.php b/tests/unit/lucatume/WPBrowser/Module/WPLoaderLoadOnlyTest.php index 8ce5cd79d..f3091ce37 100644 --- a/tests/unit/lucatume/WPBrowser/Module/WPLoaderLoadOnlyTest.php +++ b/tests/unit/lucatume/WPBrowser/Module/WPLoaderLoadOnlyTest.php @@ -8,7 +8,7 @@ use lucatume\WPBrowser\Tests\Traits\Fork; use lucatume\WPBrowser\Tests\Traits\InstallationMocks; use lucatume\WPBrowser\Utils\Env; -use lucatume\WPBrowser\WordPress\Database\MysqlDatabase; +use lucatume\WPBrowser\Utils\Filesystem as FS; class WPLoaderLoadOnlyTest extends Unit { @@ -133,4 +133,76 @@ public function testWillDefineDBConstantsWhenLoadOnlyTrue(): void{ $this->assertEquals('1', getenv('WPBROWSER_LOAD_ONLY')); }); } + + public function testWillLoadConfigFileWhenLoadOnlyTrue(): void{ + [$wpRootFolder, $dbUrl] = $this->makeMockConfiguredInstallation(); + $configDir = FS::tmpDir('config_', [ + 'test-config.php' => ' $dbUrl, + 'wpRootFolder' => $wpRootFolder, + 'loadOnly' => true, + 'configFile' => $configDir . '/test-config.php' + ]); + + Fork::executeClosure(function () use ($module) { + // WordPress' functions are stubbed by wordpress-stubs in unit tests: override them to do something. + $did_actions = []; + uopz_set_return('do_action', static function ($action) use (&$did_actions) { + $did_actions[$action] = true; + }, true); + uopz_set_return('did_action', static function ($action) use (&$did_actions) { + return isset($did_actions[$action]); + }, true); + // Partial mocking the function that would load WordPress. + uopz_set_return(WPLoader::class, 'installAndBootstrapInstallation', function () { + return true; + }, true); + + $module->_initialize(); + $module->_beforeSuite(); + + $this->assertTrue($module->_didLoadWordPress()); + $this->assertTrue(defined('TEST_CONFIG')); + }); + } + + public function testWillLoadMultipleConfigFilesWhenLoadOnlyTrue(): void{ + [$wpRootFolder, $dbUrl] = $this->makeMockConfiguredInstallation(); + $configDir = FS::tmpDir('config_', [ + 'test-config.php' => ' ' $dbUrl, + 'wpRootFolder' => $wpRootFolder, + 'loadOnly' => true, + 'configFile' => [$configDir . '/test-config.php', $configDir . '/test-config2.php'] + ]); + + Fork::executeClosure(function () use ($module) { + // WordPress' functions are stubbed by wordpress-stubs in unit tests: override them to do something. + $did_actions = []; + uopz_set_return('do_action', static function ($action) use (&$did_actions) { + $did_actions[$action] = true; + }, true); + uopz_set_return('did_action', static function ($action) use (&$did_actions) { + return isset($did_actions[$action]); + }, true); + // Partial mocking the function that would load WordPress. + uopz_set_return(WPLoader::class, 'installAndBootstrapInstallation', function () { + return true; + }, true); + + $module->_initialize(); + $module->_beforeSuite(); + + $this->assertTrue($module->_didLoadWordPress()); + $this->assertTrue(defined('TEST_CONFIG')); + $this->assertTrue(defined('TEST_CONFIG2')); + }); + } }