diff --git a/CHANGELOG.md b/CHANGELOG.md index 098cf792a..0741a124a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Changed - Better messaging when throwing due to disconnected database. +- Add the `WPLoader::beStrictAboutWpdbConnectionId` configuration parameter, defaults to `true`, to throw if db connection changes during setup before class. ### Fixed diff --git a/includes/core-phpunit/includes/abstract-testcase.php b/includes/core-phpunit/includes/abstract-testcase.php index 4f6702ba0..4e8491ab4 100644 --- a/includes/core-phpunit/includes/abstract-testcase.php +++ b/includes/core-phpunit/includes/abstract-testcase.php @@ -1,5 +1,7 @@ suppress_errors = false; $wpdb->show_errors = true; - if ( empty( lucatume\WPBrowser\Utils\Property::readPrivate( $wpdb, 'dbh' ) ) ) { + if ( WPTestCase::isStrictAboutWpdbConnectionId() && $wpdb->get_var( 'SELECT CONNECTION_ID()' ) !== WPTestCase::getWpdbConnectionId() ) { self::fail( 'The database connection went away. A `setUpBeforeClassMethod` likely closed the connection.' ); + } else { + $wpdb->db_connect(); } ini_set( 'display_errors', 1 ); diff --git a/src/Module/WPLoader.php b/src/Module/WPLoader.php index e498f4d52..1aa72b249 100644 --- a/src/Module/WPLoader.php +++ b/src/Module/WPLoader.php @@ -19,6 +19,7 @@ use lucatume\WPBrowser\Process\Loop; use lucatume\WPBrowser\Process\ProcessException; use lucatume\WPBrowser\Process\WorkerException; +use lucatume\WPBrowser\TestCase\WPTestCase; use lucatume\WPBrowser\Utils\Arr; use lucatume\WPBrowser\Utils\CorePHPUnit; use lucatume\WPBrowser\Utils\Db as DbUtils; @@ -127,6 +128,7 @@ class WPLoader extends Module * backupStaticAttributes?: bool, * backupStaticAttributesExcludeList?: array, * skipInstall?: bool, + * beStrictAboutWpdbConnectionId?: bool * } */ protected $config = [ @@ -164,7 +166,8 @@ class WPLoader extends Module 'backupGlobalsExcludeList' => [], 'backupStaticAttributes' => false, 'backupStaticAttributesExcludeList' => [], - 'skipInstall' => false + 'skipInstall' => false, + 'beStrictAboutWpdbConnectionId' => true ]; /** @@ -344,6 +347,14 @@ static function ($v) { ); } + if (isset($this->config['beStrictAboutWpdbConnectionId']) + && !is_bool($this->config['beStrictAboutWpdbConnectionId'])) { + throw new ModuleConfigException( + __CLASS__, + 'The `beStrictAboutWpdbConnectionId` configuration parameter must be a boolean.' + ); + } + parent::validateConfig(); } @@ -404,7 +415,8 @@ public function _initialize(): void * backupGlobalsExcludeList: string[], * backupStaticAttributes: bool, * backupStaticAttributesExcludeList: array, - * skipInstall: bool + * skipInstall: bool, + * beStrictAboutWpdbConnectionId: bool * } $config */ $config = $this->config; @@ -515,6 +527,8 @@ public function _initialize(): void // If the database does not already exist, then create it now. $db->create(); + WPTestCase::beStrictAboutWpdbConnectionId($config['beStrictAboutWpdbConnectionId']); + $this->loadWordPress(); } @@ -1030,6 +1044,7 @@ private function includeCorePHPUniteSuiteBootstrapFile(): void try { require $this->wpBootstrapFile; + WPTestCase::setWpdbConnectionId((string)$GLOBALS['wpdb']->get_var('SELECT CONNECTION_ID()')); } catch (Throwable $t) { // Not an early exit: Codeception will handle the Exception and print it. $this->earlyExit = false; diff --git a/src/TestCase/WPTestCase.php b/src/TestCase/WPTestCase.php index cc6cb43a2..14165f248 100644 --- a/src/TestCase/WPTestCase.php +++ b/src/TestCase/WPTestCase.php @@ -78,6 +78,14 @@ class WPTestCase extends Unit { use WPTestCasePHPUnitMethodsTrait; + /** + * @var bool + */ + public static $beStrictAboutWpdbConnectionId = true; + /** + * @var string|null + */ + private static $wpdbConnectionId; /** * @var string[]|null */ @@ -244,6 +252,22 @@ private static function getCoreTestCase(): WP_UnitTestCase return $coreTestCase; } + public static function isStrictAboutWpdbConnectionId(): bool + { + return self::$beStrictAboutWpdbConnectionId; + } + public static function beStrictAboutWpdbConnectionId(bool $beStrictAboutWpdbConnectionId): void + { + self::$beStrictAboutWpdbConnectionId = $beStrictAboutWpdbConnectionId; + } + public static function getWpdbConnectionId(): ?string + { + return self::$wpdbConnectionId; + } + public static function setWpdbConnectionId(string $wpdbConnectionId): void + { + self::$wpdbConnectionId = $wpdbConnectionId; + } protected function backupAdditionalGlobals(): void { if (isset($GLOBALS['_wp_registered_theme_features'])) { diff --git a/tests/unit/lucatume/WPBrowser/Module/WPTestCaseStrictTest.php b/tests/unit/lucatume/WPBrowser/Module/WPTestCaseStrictTest.php new file mode 100644 index 000000000..b280362a7 --- /dev/null +++ b/tests/unit/lucatume/WPBrowser/Module/WPTestCaseStrictTest.php @@ -0,0 +1,171 @@ +mockModuleContainer = new ModuleContainer(new Di(), $moduleContainerConfig); + return new WPLoader($this->mockModuleContainer, ($moduleConfig ?? $this->config)); + } + + public function nonBooleanVAluesProvider(): array + { + return [ + 'int' => [1], + 'float' => [1.1], + 'array' => [[]], + 'object' => [new \stdClass()], + 'true string' => ['true'], + 'false string' => ['false'], + ]; + } + + /** + * @dataProvider nonBooleanVAluesProvider + */ + public function test_will_throw_if_beStrictAboutWpdbConnectionId_is_not_boolean($value): void + { + $this->expectException(ModuleConfigException::class); + $this->expectExceptionMessage('The `beStrictAboutWpdbConnectionId` configuration parameter must be a boolean.'); + + $this->config = [ + 'wpRootFolder' => __DIR__, + 'dbUrl' => 'mysql://root:root@mysql:3306/wordpress', + 'beStrictAboutWpdbConnectionId' => $value + ]; + $this->module(); + } + + public function test_will_fail_if_db_connection_closed_during_setup_before_class(): void + { + $wpRootDir = FS::tmpDir('wploader_'); + $dbName = Random::dbName(); + $dbHost = Env::get('WORDPRESS_DB_HOST'); + $dbUser = Env::get('WORDPRESS_DB_USER'); + $dbPassword = Env::get('WORDPRESS_DB_PASSWORD'); + $db = new MysqlDatabase($dbName, $dbUser, $dbPassword, $dbHost); + Installation::scaffold($wpRootDir); + $db->create(); + $testcaseFile = $wpRootDir . '/BreakingTest.php'; + $testCaseFileContents = <<close(); + + parent::set_up_before_class(); + } + + public function test_something():void{ + \$this->assertTrue(true); + } + } +PHP; + if(!file_put_contents($testcaseFile, $testCaseFileContents, LOCK_EX)) { + throw new \RuntimeException('Could not write BreakingTest.php.'); + } + + // Run a test using the default value, strict. + $this->config = [ + 'wpRootFolder' => $wpRootDir, + 'dbUrl' => $db->getDbUrl() + ]; + $wpLoader = $this->module(); + + $this->assertInIsolation(static function () use ($wpLoader, $testcaseFile) { + $wpLoader->_initialize(); + $connectionId = WPTestCase::getWpdbConnectionId(); + Assert::assertnotEmpty($connectionId); + Assert::assertTrue(WPTestCase::isStrictAboutWpdbConnectionId()); + + require_once $testcaseFile; + + try { + \BreakingTest::setUpBeforeClass(); + } catch (\Throwable $e) { + Assert::assertNotSame($connectionId, $GLOBALS['wpdb']->get_var('SELECT CONNECTION_ID()')); + Assert::assertStringContainsString( + 'The database connection went away. A `setUpBeforeClassMethod` likely closed the connection', + $e->getMessage() + ); + return; + } + + Assert::fail('The test should have failed.'); + }); + + // Run a test in strict mode. + $this->config = [ + 'wpRootFolder' => $wpRootDir, + 'dbUrl' => $db->getDbUrl(), + 'beStrictAboutWpdbConnectionId' => true + ]; + $wpLoader = $this->module(); + + $this->assertInIsolation(static function () use ($wpLoader, $testcaseFile) { + $wpLoader->_initialize(); + $connectionId = WPTestCase::getWpdbConnectionId(); + Assert::assertnotEmpty($connectionId); + Assert::assertTrue(WPTestCase::isStrictAboutWpdbConnectionId()); + + require_once $testcaseFile; + + try { + \BreakingTest::setUpBeforeClass(); + } catch (\Throwable $e) { + Assert::assertNotSame($connectionId, $GLOBALS['wpdb']->get_var('SELECT CONNECTION_ID()')); + Assert::assertStringContainsString( + 'The database connection went away. A `setUpBeforeClassMethod` likely closed the connection', + $e->getMessage() + ); + return; + } + + Assert::fail('The test should have failed.'); + }); + + // Run a test in non-strict mode. + $this->config = [ + 'wpRootFolder' => $wpRootDir, + 'dbUrl' => $db->getDbUrl(), + 'beStrictAboutWpdbConnectionId' => false + ]; + $wpLoader = $this->module(); + + $this->assertInIsolation(static function () use ($wpLoader, $testcaseFile) { + $wpLoader->_initialize(); + $connectionId = WPTestCase::getWpdbConnectionId(); + Assert::assertFalse(WPTestCase::isStrictAboutWpdbConnectionId()); + + require_once $testcaseFile; + + \BreakingTest::setUpBeforeClass(); + Assert::assertNotSame($connectionId, $GLOBALS['wpdb']->get_var('SELECT CONNECTION_ID()')); + }); + } +}