diff --git a/Makefile b/Makefile index fb0df3ba7..9517248a3 100644 --- a/Makefile +++ b/Makefile @@ -33,4 +33,3 @@ package: update_core_phpunit_includes test update_sqlite_plugin: bin/update_sqlite_plugin - diff --git a/bin/update_sqlite_plugin b/bin/update_sqlite_plugin index b2ad07d68..f21278f94 100755 --- a/bin/update_sqlite_plugin +++ b/bin/update_sqlite_plugin @@ -10,4 +10,5 @@ curl -L "$plugin_file_src" -o "$root_dir/includes/sqlite-database-integration.zi unzip -o "$root_dir/includes/sqlite-database-integration.zip" -d "$root_dir/includes/" # Remove the zip file rm "$root_dir/includes/sqlite-database-integration.zip" +git apply "${root_dir}"/config/patches/sqlite-database-integration/db.copy.patch diff --git a/config/patches/sqlite-database-integration/db.copy.patch b/config/patches/sqlite-database-integration/db.copy.patch new file mode 100644 index 000000000..2be8a3c90 --- /dev/null +++ b/config/patches/sqlite-database-integration/db.copy.patch @@ -0,0 +1,22 @@ +diff --git a/includes/sqlite-database-integration/db.copy b/includes/sqlite-database-integration/db.copy +index 0b0797e8..23caff78 100644 +--- a/includes/sqlite-database-integration/db.copy ++++ b/includes/sqlite-database-integration/db.copy +@@ -32,6 +32,17 @@ if ( ! defined( 'DATABASE_TYPE' ) ) { + if ( ! defined( 'DB_ENGINE' ) ) { + define( 'DB_ENGINE', 'sqlite' ); + } ++// Define SQLite main file constant to avoid having the plugin loaded automatically. ++if ( ! defined( 'SQLITE_MAIN_FILE' ) ) { ++ define( 'SQLITE_MAIN_FILE', '{SQLITE_MAIN_FILE}' ); ++} ++// Define DB_DIR and DB_FILE from env, if not already defined. ++if( ! defined( 'DB_DIR' ) && getenv( 'DB_DIR' ) ) { ++ define( 'DB_DIR', realpath( getenv( 'DB_DIR' ) ) ); ++} ++if( ! defined( 'DB_FILE' ) && getenv( 'DB_FILE' ) ) { ++ define( 'DB_FILE', getenv( 'DB_FILE' ) ); ++} + + // Require the implementation from the plugin. + require_once $sqlite_plugin_implementation_folder_path . '/wp-includes/sqlite/db.php'; diff --git a/includes/cli-server/router.php b/includes/cli-server/router.php index ce17b8f38..9a8648a5b 100644 --- a/includes/cli-server/router.php +++ b/includes/cli-server/router.php @@ -7,6 +7,8 @@ $root = $_SERVER['DOCUMENT_ROOT']; $path = '/'. ltrim( parse_url( urldecode( $_SERVER['REQUEST_URI'] ),PHP_URL_PATH ), '/' ); +define('DB_ENGINE', getenv('DB_ENGINE') ?: 'mysql'); + if ( file_exists( $root.$path ) ) { // Enforces trailing slash, keeping links tidy in the admin diff --git a/includes/sqlite-database-integration/db.copy b/includes/sqlite-database-integration/db.copy index 0b0797e80..e164139d8 100644 --- a/includes/sqlite-database-integration/db.copy +++ b/includes/sqlite-database-integration/db.copy @@ -26,11 +26,22 @@ if ( ! $sqlite_plugin_implementation_folder_path || ! file_exists( $sqlite_plugi // Constant for backward compatibility. if ( ! defined( 'DATABASE_TYPE' ) ) { - define( 'DATABASE_TYPE', 'sqlite' ); + define( 'DATABASE_TYPE', getenv( 'DATABASE_TYPE' ) ?: 'sqlite' ); } // Define SQLite constant. if ( ! defined( 'DB_ENGINE' ) ) { - define( 'DB_ENGINE', 'sqlite' ); + define( 'DB_ENGINE', getenv( 'DB_ENGINE' ) ?: 'sqlite' ); +} +// Define SQLite main file constant to avoid having the plugin loaded automatically. +if ( ! defined( 'SQLITE_MAIN_FILE' ) ) { + define( 'SQLITE_MAIN_FILE', '{SQLITE_MAIN_FILE}' ); +} +// Define DB_DIR and DB_FILE from env, if not already defined. +if( ! defined( 'DB_DIR' ) && getenv( 'DB_DIR' ) ) { + define( 'DB_DIR', realpath( getenv( 'DB_DIR' ) ) ); +} +if( ! defined( 'DB_FILE' ) && getenv( 'DB_FILE' ) ) { + define( 'DB_FILE', getenv( 'DB_FILE' ) ); } // Require the implementation from the plugin. diff --git a/src/Extension/BuiltInServerController.php b/src/Extension/BuiltInServerController.php index 19e6cd2e9..c16125441 100644 --- a/src/Extension/BuiltInServerController.php +++ b/src/Extension/BuiltInServerController.php @@ -22,10 +22,13 @@ public function start(OutputInterface $output): void return; } - [$port, $docRoot, $workers] = $this->parseConfig(); + [$port, $docRoot, $workers, $env] = $this->parseConfig(); $output->write("Starting PHP built-in server on port $port to serve $docRoot ..."); - $phpBuiltInServer = new PhpBuiltInServer($docRoot, $port, ['PHP_CLI_SERVER_WORKERS' => $workers]); + $env = array_merge([ + 'PHP_CLI_SERVER_WORKERS' => $workers, + ], $env); + $phpBuiltInServer = new PhpBuiltInServer($docRoot, $port, $env); $phpBuiltInServer->start(); $output->write(' ok', true); } @@ -66,13 +69,15 @@ public function getPrettyName(): string * port: int, * docroot: string, * workers: int, - * pid: int|null + * pid: int|null, + * url: string, + * env: array * } * @throws ExtensionException */ public function getInfo(): array { - [$port, $docRoot, $workers] = $this->parseConfig(); + [$port, $docRoot, $workers, $env] = $this->parseConfig(); $pidFile = $this->getPidFile(); return [ @@ -81,7 +86,8 @@ public function getInfo(): array 'port' => $port, 'docroot' => $docRoot, 'workers' => $workers, - 'url' => 'http://localhost:' . $port . '/' + 'url' => 'http://localhost:' . $port . '/', + 'env' => $env ]; } @@ -89,7 +95,8 @@ public function getInfo(): array * @return array{ * 0: int, * 1: string, - * 2: int + * 2: int, + * 3: array * } * @throws ExtensionException */ @@ -122,7 +129,21 @@ private function parseConfig(): array } /** @var array{workers?: number} $config */ $workers = (int)($config['workers'] ?? 5); - return array($port, $docRoot, $workers); + + if (isset($config['env']) && !is_array($config['env'])) { + throw new ExtensionException( + $this, + 'The "env" configuration option must be an array.' + ); + } + $env = $config['env'] ?? []; + $env = array_map( static function ( $value ): mixed { + return is_string( $value ) ? + str_replace( '%codecept_root_dir%', rtrim( codecept_root_dir(), '\\/' ), $value ) + : $value; + }, $env ); + + return [$port, $docRoot, $workers, $env]; } private function getPidFile(): string diff --git a/src/Extension/DockerComposeController.php b/src/Extension/DockerComposeController.php index 1fdb7e1ad..5527e52b0 100644 --- a/src/Extension/DockerComposeController.php +++ b/src/Extension/DockerComposeController.php @@ -78,7 +78,7 @@ public function stop(OutputInterface $output): void ); } $runningFile = $this->getRunningFile(); - if (!(is_file($runningFile) && unlink($runningFile))) { + if (is_file($runningFile) && !unlink($runningFile)) { throw new ExtensionException( $this, 'Failed to remove Docker Compose running file.' diff --git a/src/ManagedProcess/ChromeDriver.php b/src/ManagedProcess/ChromeDriver.php index 325f647e8..3fd13fc13 100644 --- a/src/ManagedProcess/ChromeDriver.php +++ b/src/ManagedProcess/ChromeDriver.php @@ -56,8 +56,9 @@ public function doStart(): void */ private function confirmStart(Process $process): void { + $start = time(); $output = $process->getOutput(); - for ($attempts = 0; $attempts < 30; $attempts++) { + while (time() < $start + 30) { if (str_contains($output, 'ChromeDriver was started successfully.')) { return; } diff --git a/src/Module/WPLoader.php b/src/Module/WPLoader.php index d74eb1853..cbb839ddb 100644 --- a/src/Module/WPLoader.php +++ b/src/Module/WPLoader.php @@ -158,6 +158,7 @@ class WPLoader extends Module private string $bootstrapOutput = ''; private string $installationOutput = ''; private bool $earlyExit = true; + private ?DatabaseInterface $db = null; public function _getBootstrapOutput(): string { @@ -291,17 +292,10 @@ public function _initialize(): void // Try and initialize the database connection now. $db->create(); + $db->setEnvVars(); + $this->db = $db; - $this->installation = new Installation($config['wpRootFolder'], $db); - - $dropInPathname = $this->installation->getContentDir('db.php'); - if ($db instanceof SQLiteDatabase && !is_file($dropInPathname)) { - throw new ModuleConfigException( - __CLASS__, - 'WPLoader is configured to use a SQLite database, but no db.php drop-in file ' . - "was found in the content folder ($dropInPathname)." - ); - } + $this->installation = new Installation( $config['wpRootFolder'], false ); // Update the config to the resolved path. $config['wpRootFolder'] = $this->installation->getWpRootDir(); @@ -311,7 +305,14 @@ public function _initialize(): void if ($installationState instanceof EmptyDir) { $wpRootDir = $this->installation->getWpRootDir(); Installation::scaffold($wpRootDir); - $this->installation = new Installation($wpRootDir, $db); + $this->installation = new Installation($wpRootDir); + } + + if ($db instanceof SqliteDatabase && !is_file($this->installation->getContentDir('db.php'))) { + Installation::placeSqliteMuPlugin( + $this->installation->getMuPluginsDir(), + $this->installation->getContentDir() + ); } $config['wpRootFolder'] = $this->installation->getWpRootDir(); @@ -511,17 +512,15 @@ private function installAndBootstrapInstallation(): void if ($this->config['theme']) { // Refresh the theme related options. update_site_option('allowedthemes', [$this->config['theme'] => true]); - $db = $this->installation->getDb(); - - if ($db === null) { + if ($this->db === null) { throw new ModuleException( __CLASS__, 'Could not get database instance from installation.' ); } - update_option('template', $db->getOption('template')); - update_option('stylesheet', $db->getOption('stylesheet')); + update_option('template', $this->db->getOption('template')); + update_option('stylesheet', $this->db->getOption('stylesheet')); } // Format for site-wide active plugins is `[ 'plugin-slug/plugin.php' => timestamp ]`. @@ -536,23 +535,22 @@ private function installAndBootstrapInstallation(): void if ($this->config['theme']) { // Refresh the theme related options. - $db = $this->installation->getDb(); - - if ($db === null) { + if ($this->db === null) { throw new ModuleException( __CLASS__, 'Could not get database instance from installation.' ); } - update_option('template', $db->getOption('template')); - update_option('stylesheet', $db->getOption('stylesheet')); + update_option('template', $this->db->getOption('template')); + update_option('stylesheet', $this->db->getOption('stylesheet')); } return $plugins; }; PreloadFilters::addFilter('pre_option_active_plugins', $activate); } + $this->includeCorePHPUniteSuiteBootstrapFile(); Dispatcher::dispatch(self::EVENT_AFTER_INSTALL, $this); @@ -604,15 +602,20 @@ private function activatePluginsSwitchThemeInSeparateProcess(): void [$type, $name] = explode('::', $key, 2); $returnValue = $result->getReturnValue(); - if ($returnValue instanceof Throwable) { + if ($returnValue instanceof Throwable && !($returnValue instanceof InstallationException)) { // Not gift-wrapped in a ModuleException to make it easier to debug the issue. throw $returnValue; } - if ($result->getExitCode() !== 0) { + $error = $result->getExitCode() !== 0 || $returnValue instanceof InstallationException; + + if ($error) { + $reason = $returnValue instanceof InstallationException ? + $returnValue->getMessage() + : $result->getStdoutBuffer(); $message = $type === 'plugin' ? - "Failed to activate plugin $name: {$result->getStdoutBuffer()}" - : "Failed to switch theme $name: {$result->getStdoutBuffer()}"; + "Failed to activate plugin $name. $reason" + : "Failed to switch theme $name. $reason"; throw new ModuleException(__CLASS__, $message); } } @@ -734,6 +737,7 @@ public function getInstallation(): Installation */ private function checkInstallationToLoadOnly(): void { + // The installation must be at least configured: it might be installed by a dump. if (!$this->installation->isConfigured()) { $dir = $this->installation->getWpRootDir(); throw new ModuleException( @@ -765,7 +769,7 @@ private function disableUpdates(): void */ private function importDumps(): void { - $db = $this->installation->getDb(); + $db = $this->db; if (!$db instanceof DatabaseInterface) { throw new ModuleException( diff --git a/src/Process/Loop.php b/src/Process/Loop.php index 597b317ee..32e45211f 100644 --- a/src/Process/Loop.php +++ b/src/Process/Loop.php @@ -4,6 +4,7 @@ use Closure; use Codeception\Exception\ConfigurationException; +use Codeception\Util\Debug; use lucatume\WPBrowser\Process\Worker\Exited; use lucatume\WPBrowser\Process\Worker\Result; use lucatume\WPBrowser\Process\Worker\Running; @@ -55,6 +56,9 @@ public function __construct( private float $timeout = 30, array $options = [] ) { + if (Debug::isEnabled() || getenv('WPBROWSER_LOOP_DEBUG')) { + $this->timeout = 10 ** 10; + } $this->addWorkers($workers, $options); } diff --git a/src/Process/Protocol/Control.php b/src/Process/Protocol/Control.php index d359c9e66..778630d60 100644 --- a/src/Process/Protocol/Control.php +++ b/src/Process/Protocol/Control.php @@ -4,6 +4,7 @@ use Codeception\Configuration; use Codeception\Exception\ConfigurationException; +use lucatume\WPBrowser\Polyfills\Dotenv\Dotenv; class Control { @@ -15,7 +16,8 @@ class Control * codeceptionRootDir: string , * codeceptionConfig: array, * composerAutoloadPath: ?string, - * composerBinDir: ?string + * composerBinDir: ?string, + * env: array * } */ private array $control; @@ -28,7 +30,8 @@ class Control * codeceptionRootDir?: string, * codeceptionConfig?: array, * composerAutoloadPath?: ?string, - * composerBinDir?: ?string + * composerBinDir?: ?string, + * env?: array * } $controlArray * * @throws ConfigurationException @@ -55,7 +58,8 @@ public function __construct(array $controlArray) 'codeceptionConfig' => $codeceptionConfig, 'composerAutoloadPath' => (string)($controlArray['composerAutoloadPath'] ?? $GLOBALS['_composer_autoload_path'] ?? null), - 'composerBinDir' => (string)($controlArray['composerBinDir'] ?? $GLOBALS['_composer_bin_dir'] ?? null) + 'composerBinDir' => (string)($controlArray['composerBinDir'] ?? $GLOBALS['_composer_bin_dir'] ?? null), + 'env' => $controlArray['env'] ?? $this->getCurrentEnv(), ]; } @@ -67,7 +71,8 @@ public function __construct(array $controlArray) * codeceptionRootDir: string , * codeceptionConfig: array, * composerAutoloadPath: ?string, - * composerBinDir: ?string + * composerBinDir: ?string, + * env: array * } * @throws ConfigurationException */ @@ -154,6 +159,13 @@ public function apply(): void } chdir($control['cwd']); } + + if (!empty($control['env'])) { + foreach ($control['env'] as $key => $value) { + putenv("$key=$value"); + $_ENV[$key] = $value; + } + } } /** @@ -164,11 +176,27 @@ public function apply(): void * codeceptionRootDir: string , * codeceptionConfig: array, * composerAutoloadPath: ?string, - * composerBinDir: ?string + * composerBinDir: ?string, + * env: array * } */ public function toArray(): array { return $this->control; } + + /** + * @return array + */ + private function getCurrentEnv(): array + { + $currentEnv = getenv(); + + // @phpstan-ignore-next-line $_ENV is not always defined. + if (isset($_ENV)) { + $currentEnv = array_merge($currentEnv, $_ENV); + } + + return $currentEnv; + } } diff --git a/src/Process/Protocol/Request.php b/src/Process/Protocol/Request.php index 79afa8f2b..2be5e5638 100644 --- a/src/Process/Protocol/Request.php +++ b/src/Process/Protocol/Request.php @@ -17,7 +17,8 @@ class Request * codeceptionRootDir?: string, * codeceptionConfig?: array, * composerAutoloadPath?: ?string, - * composerBinDir?: ?string + * composerBinDir?: ?string, + * env?: array * } $controlArray * @throws ConfigurationException */ diff --git a/src/Process/StderrStream.php b/src/Process/StderrStream.php index 2f697065a..805cd1018 100644 --- a/src/Process/StderrStream.php +++ b/src/Process/StderrStream.php @@ -3,6 +3,7 @@ namespace lucatume\WPBrowser\Process; use CompileError; +use DateTime; use ErrorException; use lucatume\WPBrowser\Process\StderrStream\Error; use lucatume\WPBrowser\Process\StderrStream\TraceEntry; @@ -78,6 +79,7 @@ private function formatInvertedTraceError(Error $error, bool $isNumericStackTrac public function parse(string $stderrStreamContents, int $options = 0): void { + $now = new DateTime(); $len = strlen($stderrStreamContents); if ($len === 0) { @@ -91,12 +93,12 @@ public function parse(string $stderrStreamContents, int $options = 0): void $currentError = null; $isNumericStackTrace = false; - $typePattern = '/^\\[(?.+?) (?