Skip to content

Commit

Permalink
feat(Extensions) readd the Symlinker extension, use in setup
Browse files Browse the repository at this point in the history
  • Loading branch information
lucatume committed Jun 5, 2024
1 parent 8f62915 commit 8452ace
Show file tree
Hide file tree
Showing 9 changed files with 1,197 additions and 82 deletions.
208 changes: 208 additions & 0 deletions src/Extension/Symlinker.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
<?php

namespace lucatume\WPBrowser\Extension;

use Codeception\Event\SuiteEvent;
use Codeception\Events;
use Codeception\Exception\ModuleConfigException;
use Codeception\Exception\ModuleException;
use Codeception\Extension;
use lucatume\WPBrowser\WordPress\Installation;

class Symlinker extends Extension
{
/**
* @var array<string,string>
*/
protected static $events = [
Events::MODULE_INIT => 'onModuleInit',
Events::SUITE_AFTER => 'afterSuite',
];

/**
* @var string
*/
private $wpRootFolder = '';
/**
* @var string[]
*/
private $plugins = [];
/**
* @var string[]
*/
private $themes = [];
/**
* @var string
*/
private $pluginsDir = '';
/**
* @var string
*/
private $themesDir = '';
/**
* @var string[]
*/
private $unlinkTargets = [];
/**
* @var bool
*/
private $cleanupAfterSuite = false;

/**
* @throws ModuleConfigException
*/
public function _initialize(): void
{
parent::_initialize();
$wpRootFolder = $this->config['wpRootFolder'] ?? null;

if (empty($wpRootFolder) || !is_string($wpRootFolder) || !is_dir($wpRootFolder)) {
throw new ModuleConfigException($this, 'The `wpRootFolder` configuration parameter must be set.');
}

$plugins = $this->config['plugins'] ?? [];

if (!is_array($plugins)) {
throw new ModuleConfigException($this, 'The `plugins` configuration parameter must be an array.');
}

foreach ($plugins as $plugin) {
$realpath = realpath($plugin);

if (!$realpath) {
throw new ModuleConfigException($this, "Plugin file $plugin does not exist.");
}

$this->plugins[] = $realpath;
}

$themes = $this->config['themes'] ?? [];

if (!is_array($themes)) {
throw new ModuleConfigException($this, 'The `themes` configuration parameter must be an array.');
}

foreach ($themes as $theme) {
$realpath = realpath($theme);

if (!$realpath) {
throw new ModuleConfigException($this, "Theme directory $theme does not exist.");
}

$this->themes[] = $realpath;
}

$this->wpRootFolder = $wpRootFolder;

$this->cleanupAfterSuite = isset($this->config['cleanupAfterSuite']) ?
(bool)$this->config['cleanupAfterSuite']
: false;
}

/**
* @throws ModuleConfigException
* @throws ModuleException
*/
public function onModuleInit(SuiteEvent $event): void
{
try {
$installation = new Installation($this->wpRootFolder);
$this->pluginsDir = $installation->getPluginsDir();
$this->themesDir = $installation->getThemesDir();
} catch (\Throwable $e) {
throw new ModuleConfigException(
$this,
'The `wpRootFolder` does not point to a valid WordPress installation.'
);
}

foreach ($this->plugins as $plugin) {
$this->symlinkPlugin($plugin, $this->pluginsDir);
}

foreach ($this->themes as $theme) {
$this->symlinkTheme($theme, $this->themesDir);
}
}

/**
* @throws ModuleException
*/
private function symlinkPlugin(string $plugin, string $pluginsDir): void
{
$link = $pluginsDir . basename($plugin);

if (is_link($link)) {
$target = readlink($link);

if ($target && realpath($target) === $plugin) {
// Already existing, but not managed by the extension.
codecept_debug(
"[Symlinker] Found $link not managed by the extension: this will not be removed after the suite."
);
return;
}

throw new ModuleException(
$this,
"Could not symlink plugin $plugin to $link: link already exists and target is $target."
);
}

if (!symlink($plugin, $link)) {
throw new ModuleException($this, "Could not symlink plugin $plugin to $link.");
}

$this->unlinkTargets [] = $link;
codecept_debug("[Symlinker] Symlinked plugin $plugin to $link.");
}

/**
* @throws ModuleException
*/
private function symlinkTheme(string $theme, string $themesDir): void
{
$target = $theme;
$link = $themesDir . basename($theme);

if (is_link($link)) {
$target = readlink($link);

if ($target && realpath($target) === $theme) {
codecept_debug(
"[Symlinker] Found $link not managed by the extension: this will not be removed after the suite."
);
return;
}

throw new ModuleException(
$this,
"Could not symlink theme $theme to $link: link already exists and target is $target."
);
}

if (!symlink($target, $link)) {
throw new ModuleException($this, "Could not symlink theme $theme to $link.");
}

$this->unlinkTargets [] = $link;
codecept_debug("[Symlinker] Symlinked theme $theme to $link.");
}

/**
* @throws ModuleException
*/
public function afterSuite(SuiteEvent $event): void
{
if (!$this->cleanupAfterSuite) {
return;
}

foreach ($this->unlinkTargets as $target) {
if (!unlink($target)) {
throw new ModuleException($this, "Could not unlink $target.");
}
codecept_debug("[Symlinker] Unlinked $target.");
}
}
}
89 changes: 53 additions & 36 deletions src/Project/ContentProject.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use lucatume\WPBrowser\Exceptions\RuntimeException;
use lucatume\WPBrowser\Extension\BuiltInServerController;
use lucatume\WPBrowser\Extension\ChromeDriverController;
use lucatume\WPBrowser\Extension\Symlinker;
use lucatume\WPBrowser\Utils\ChromedriverInstaller;
use lucatume\WPBrowser\Utils\Codeception;
use lucatume\WPBrowser\Utils\Filesystem as FS;
Expand All @@ -28,50 +29,18 @@ abstract class ContentProject extends InitTemplate implements ProjectInterface
*/
protected $testEnvironment;

abstract protected function getProjectType(): string;

abstract public function getName(): string;

abstract public function getActivationString(): string;

abstract protected function symlinkProjectInContentDir(string $wpRootDir): void;

abstract public function activate(string $wpRootDir, int $serverLocalhostPort): bool;

/**
* @return array<string>|false
*/
abstract public static function parseDir(string $workDir);

abstract protected function scaffoldEndToEndActivationCest(): void;

abstract protected function scaffoldIntegrationActivationTest(): void;
abstract public function getActivationString(): string;

public function getTestEnv(): TestEnvironment
{
return $this->testEnvironment;
}

private function getAfterSuccessClosure(bool $activated): Closure
{
$basename = basename($this->workDir);
return function () use ($basename, $activated): void {
if ($activated) {
$this->scaffoldEndToEndActivationCest();
$this->scaffoldIntegrationActivationTest();
}
$this->say(
"The {$this->getProjectType()} has been linked into the " .
"<info>tests/_wordpress/wp-content/{$this->getProjectType()}s/$basename</info> directory."
);
$this->say(
"If your {$this->getProjectType()} requires additional plugins and themes, place them in the " .
'<info>tests/_wordpress/wp-content/plugins</info> and ' .
'<info>tests/_wordpress/wp-content/themes</info> directories.'
);
};
}

/**
* @throws Throwable
*/
Expand Down Expand Up @@ -114,7 +83,6 @@ public function setup(): void
$this->getName() . ' Test'
);

// Symlink the project into the WordPress plugins or themes directory.
$this->symlinkProjectInContentDir($wpRootDir);

$activated = $this->activate($wpRootDir, $serverLocalhostPort);
Expand Down Expand Up @@ -150,6 +118,18 @@ public function setup(): void
EOT;

$symlinkerConfig = [
'wpRootFolder' => '%WORDPRESS_ROOT_DIR%',
'plugins' => [],
'themes' => []
];

if ($this instanceof PluginProject) {
$symlinkerConfig['plugins'][] = '.';
} elseif ($this instanceof ThemeProject) {
$symlinkerConfig['themes'][] = '.';
}

$this->testEnvironment->extensionsEnabled = [
ChromeDriverController::class => [
'port' => "%CHROMEDRIVER_PORT%",
Expand All @@ -164,7 +144,8 @@ public function setup(): void
'DB_DIR' => '%codecept_root_dir%' . DIRECTORY_SEPARATOR . $dataDirRelativePath,
'DB_FILE' => 'db.sqlite'
]
]
],
Symlinker::class => $symlinkerConfig
];
$this->testEnvironment->customCommands[] = DevStart::class;
$this->testEnvironment->customCommands[] = DevStop::class;
Expand All @@ -176,7 +157,43 @@ public function setup(): void
DIRECTORY_SEPARATOR,
['%codecept_root_dir%', 'tests', '_wordpress', 'data', 'db.sqlite']
);

$this->testEnvironment->afterSuccess = $this->getAfterSuccessClosure($activated);
}

abstract public function getName(): string;

abstract public function activate(string $wpRootDir, int $serverLocalhostPort): bool;

private function getAfterSuccessClosure(bool $activated): Closure
{
$basename = basename($this->workDir);
return function () use ($basename, $activated): void {
if ($activated) {
$this->scaffoldEndToEndActivationCest();
$this->scaffoldIntegrationActivationTest();
}
$this->say(
"The {$this->getProjectType()} was symlinked the " .
"<info>tests/_wordpress/wp-content/{$this->getProjectType()}s/$basename</info> directory."
);
$this->say(
"If your {$this->getProjectType()} requires additional plugins and themes, add them to the 'plugins' " .
"and 'themes' section of the Symlinker extension or place them in the " .
"<info>tests/_wordpress/wp-content/plugins</info> and " .
"<info>tests/_wordpress/wp-content/themes</info> directories."
);
$this->say(
"Read more about the Symlinker extension in the " .
"<info>https://github.com/lucatume/wp-browser/blob/master/docs/extensions.md#symlinker</info> file."
);
};
}

abstract protected function scaffoldEndToEndActivationCest(): void;

abstract protected function scaffoldIntegrationActivationTest(): void;

abstract protected function getProjectType(): string;

abstract protected function symlinkProjectInContentDir(string $wpRootDir): void;
}
Loading

0 comments on commit 8452ace

Please sign in to comment.