Skip to content

Commit

Permalink
fix(Module/WPLoader) load WordPress in _beforeSuite method
Browse files Browse the repository at this point in the history
fixes #744

Change the code of the `WPLoader` module to load WordPress, whether the
`loadOnly` flag is set to `true` or `false`, in the `_beforeSuite`
method.

This removes the need, for the module, to rely on the dispatching,
subscribing and need to do so, to the main Codeception system through
the Codeception event dispatcher.

Kudos to @lxbdr for the proposed solution.
  • Loading branch information
lucatume committed Aug 22, 2024
1 parent 3014384 commit 684d46b
Show file tree
Hide file tree
Showing 4 changed files with 367 additions and 5 deletions.
27 changes: 23 additions & 4 deletions src/Module/WPLoader.php
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ class WPLoader extends Module
private bool $earlyExit = true;
private ?DatabaseInterface $db = null;
private ?CodeExecutionFactory $codeExecutionFactory = null;
private bool $didLoadWordPress = false;

public function _getBootstrapOutput(): string
{
Expand All @@ -189,6 +190,11 @@ public function _getInstallationOutput(): string
return $this->installationOutput;
}

public function _didLoadWordPress(): bool
{
return $this->didLoadWordPress;
}

protected function validateConfig(): void
{
// Coming from required fields, the values are now defined.
Expand Down Expand Up @@ -489,10 +495,6 @@ public function _initialize(): void
$this->checkInstallationToLoadOnly();
$this->debug('The WordPress installation will be loaded after all other modules have been initialized.');

Dispatcher::addListener(Events::SUITE_BEFORE, function (): void {
$this->_loadWordPress(true);
});

return;
}

Expand All @@ -506,6 +508,17 @@ public function _initialize(): void
$this->_loadWordPress();
}

/**
* @param array<string,mixed> $settings
*
* @return void
*/
public function _beforeSuite(array $settings = [])
{
parent::_beforeSuite($settings);
$this->_loadWordPress();
}

/**
* Returns the absolute path to the WordPress root folder or a path within it..
*
Expand Down Expand Up @@ -559,6 +572,10 @@ private function ensureDbModuleCompat(): void
*/
public function _loadWordPress(?bool $loadOnly = null): void
{
if ($this->didLoadWordPress) {
return;
}

$config = $this->config;
/** @var array{loadOnly: bool} $config */
$loadOnly = $loadOnly ?? $config['loadOnly'];
Expand All @@ -574,6 +591,8 @@ public function _loadWordPress(?bool $loadOnly = null): void
$this->installAndBootstrapInstallation();
}

$this->didLoadWordPress = true;

wp_cache_flush();

$this->factoryStore = new FactoryStore();
Expand Down
3 changes: 2 additions & 1 deletion src/WordPress/LoadSandbox.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ class_exists(InstallationException::class);
if (did_action('wp_loaded') >= 1) {
return true;
}
$reason = 'action wp_loaded not fired.';

if (count($this->redirects) > 0
&& $this->redirects[0][1] === 302
Expand Down Expand Up @@ -101,7 +102,7 @@ class_exists(InstallationException::class);
}

// We do not know what happened, throw and try to be helpful.
throw InstallationException::becauseWordPressFailedToLoad($bodyContent);
throw InstallationException::becauseWordPressFailedToLoad($bodyContent ?: $reason);
}

public function logRedirection(string $location, int $status): string
Expand Down
208 changes: 208 additions & 0 deletions tests/_support/Fork.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
<?php

declare(strict_types=1);

namespace lucatume\WPBrowser\Tests\Traits;

use lucatume\WPBrowser\Opis\Closure\SerializableClosure;
use lucatume\WPBrowser\Process\SerializableThrowable;

/**
* Class Fork.
*
* @since TBD
*
* @package lucatume\WPBrowser\Tests;
*/
class Fork
{
const DEFAULT_TERMINATOR = '__WPBROWSER_SEPARATOR__';
private \Closure $callback;
private bool $quiet = false;
/**
* @var int<0, max>
*/
private int $ipcSocketChunkSize = 2048;
private string $terminator = self::DEFAULT_TERMINATOR;

public static function executeClosure(
\Closure $callback,
bool $quiet = false,
int $ipcSocketChunkSize = 2048,
string $terminator = self::DEFAULT_TERMINATOR
): mixed {
return (new self($callback))
->setQuiet($quiet)
->setIpcSocketChunkSize($ipcSocketChunkSize)
->setTerminator($terminator)
->execute();
}

public function __construct(\Closure $callback)
{
$this->callback = $callback;
}

public function setQuiet(bool $quiet): self
{
$this->quiet = $quiet;
return $this;
}

public function execute(): mixed
{
if (!(function_exists('pcntl_fork') && function_exists('posix_kill'))) {
throw new \RuntimeException('pcntl and posix extensions missing.');
}

$sockets = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, STREAM_IPPROTO_IP);

if ($sockets === false) {
throw new \RuntimeException('Failed to create socket pair');
}

/** @var array{0: resource, 1: resource} $sockets */

$pid = pcntl_fork();
if ($pid === -1) {
throw new \RuntimeException('Failed to fork');
}


if ($pid === 0) {
$this->executeFork($sockets);
}

return $this->executeMain($pid, $sockets);
}

public function setIpcSocketChunkSize(int $ipcSocketChunkSize): self
{
if ($ipcSocketChunkSize < 0) {
throw new \InvalidArgumentException('ipcSocketChunkSize must be a positive integer');
}

$this->ipcSocketChunkSize = $ipcSocketChunkSize;
return $this;
}

public function setTerminator(string $terminator): self
{
$this->terminator = $terminator;
return $this;
}

/**
* @param array{0: resource, 1: resource} $sockets
*/
private function executeFork(array $sockets): void
{
fclose($sockets[1]);
$ipcSocket = $sockets[0];
$pid = getmypid();
$didWriteTerminator = false;
$terminator = $this->terminator;

if ($pid === false) {
die('Failed to get pid');
}

if ($this->quiet) {
fclose(STDOUT);
fclose(STDERR);
}

register_shutdown_function(static function () use ($pid, $ipcSocket, &$didWriteTerminator, $terminator) {
if (!$didWriteTerminator) {
fwrite($ipcSocket, $terminator);
$didWriteTerminator = true;
}
fclose($ipcSocket);
/** @noinspection PhpComposerExtensionStubsInspection */
posix_kill($pid, 9 /* SIGKILL */);
});

try {
$result = ($this->callback)();
$resultClosure = new SerializableClosure(static function () use ($result) {
return $result;
});
$resultPayload = serialize($resultClosure);
} catch (\Throwable $throwable) {
$resultPayload = serialize(new SerializableThrowable($throwable));
} finally {
if (!isset($resultPayload)) {
// Something went wrong.
fwrite($ipcSocket, serialize(null));
fwrite($ipcSocket, $this->terminator);
$didWriteTerminator = true;
/** @noinspection PhpComposerExtensionStubsInspection */
posix_kill($pid, 9 /* SIGKILL */);
}
}

$offset = 0;
while (true) {
$chunk = substr($resultPayload, $offset, $this->ipcSocketChunkSize);

if ($chunk === '') {
break;
}

fwrite($ipcSocket, $chunk);
$offset += $this->ipcSocketChunkSize;
}
fwrite($ipcSocket, $this->terminator);
$didWriteTerminator = true;
fclose($ipcSocket);

// Kill the child process now with a signal that will not run shutdown handlers.
/** @noinspection PhpComposerExtensionStubsInspection */
posix_kill($pid, 9 /* SIGKILL */);
}

/**
* @param array{0: resource, 1: resource} $sockets
* @throws \Throwable
*/
private function executeMain(int $pid, array $sockets): mixed
{
fclose($sockets[0]);
$resultPayload = '';

/** @noinspection PhpComposerExtensionStubsInspection */
while (pcntl_wait($status, 1 /* WNOHANG */) <= 0) {
$chunk = fread($sockets[1], $this->ipcSocketChunkSize);
$resultPayload .= $chunk;
}

while (!str_ends_with($resultPayload, $this->terminator)) {
$chunk = fread($sockets[1], $this->ipcSocketChunkSize);
$resultPayload .= $chunk;
}

fclose($sockets[1]);

if (str_ends_with($resultPayload, $this->terminator)) {
$resultPayload = substr($resultPayload, 0, -strlen($this->terminator));
}

try {
/** @var SerializableClosure|SerializableThrowable $unserializedPayload */
$unserializedPayload = @unserialize($resultPayload);
$result = $unserializedPayload instanceof SerializableThrowable ?
$unserializedPayload->getThrowable() : $unserializedPayload->getClosure()();
} catch (\Throwable $t) {
$result = $resultPayload;
}

if ($result instanceof \Throwable) {
throw $result;
}

/** @noinspection PhpComposerExtensionStubsInspection */
posix_kill($pid, 9 /* SIGKILL */);

return $result;
}
}
Loading

0 comments on commit 684d46b

Please sign in to comment.