Skip to content

Commit

Permalink
Merge pull request #733 from lucatume/v4-theme-plugin-symlink
Browse files Browse the repository at this point in the history
Add back Symlinker ext, use in setup
  • Loading branch information
lucatume authored Jun 6, 2024
2 parents 51a5712 + 4a98ebd commit d67ba4c
Show file tree
Hide file tree
Showing 12 changed files with 1,205 additions and 84 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ This project adheres to [Semantic Versioning](http://semver.org/).

## [unreleased] Unreleased

### Added

- Re-added the `Symlinker` extension to allow for the symlinking of plugins and themes in place during tests.
- Update setup to use the `Symlinker` extension.

## [4.2.3] 2024-06-03;

### Fixed
Expand Down
2 changes: 0 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
version: "3.8"

services:
database:
container_name: wpbrowser_4_database
Expand Down
51 changes: 48 additions & 3 deletions docs/extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,9 +131,6 @@ extensions:
enabled:
- "lucatume\\WPBrowser\\Extension\\DockerComposeController"
config:
suites:
- EndToEnd
- WebApp
"lucatume\\WPBrowser\\Extension\\DockerComposeController":
compose-file: '%DOCKER_COMPOSE_FILE%'
env-file: '%DOCKER_COMPOSE_ENV_FILE%'
Expand Down Expand Up @@ -206,6 +203,54 @@ class RunAllTestsInSeparateProcesses extends WPTestCase {

Isolation support is based around monkey-patching the file at runtime. Look into the [`monkey:cache:clear`][3] and [`monkey:cache:path`][4] commands to manage the monkey-patching cache.

### `Symlinker`

This extension will symlink the plugins and themes specified in the `plugins` and `themes` configuration parameters to the WordPress installation plugins and themes directories, respectively.

The plugins and themes will be symlinked before each suite, and removed after each suite.

The extension can be configured with the following parameters:
* required
* `wpRootFolder` - the relative (to the current working directory) or absolute path to the WordPress installation root folder, the directory that contains the `wp-load.php` file.
* optional
* `cleanupAfterSuite` - default `false`, a boolean value to indicate if the symlinks created by the extension sshould be removed after the suite ran.
* `plugins`- a list of plugin **directories** to symlink to the WordPress installation plugins directory, if not set the plugin symlinking will be skipped.
* `themes`- a list of theme **directories** to symlink to the WordPress installation themes directory, if not set the theme symlinking will be skipped.

Example configuration symbolically linking the plugins and themes to the WordPress installation plugins and themes directories:

```yaml
extensions:
enabled:
- "lucatume\\WPBrowser\\Extension\\Symlinker"
config:
"lucatume\\WPBrowser\\Extension\\Symlinker":
wpRootFolder: /var/www/html
plugins:
- /home/plugins/plugin-1 # Absolute path to a plugin directory.
- vendor/acme/plugin-2 # Relative path to a plugin directory.
themes:
- /home/theme-1 # Absolute path to a theme directory.
- vendor/acme/theme-2 # Relative path to a theme directory.
```

The extension can access environment variables defined in the tests configuration file:

```yaml
extensions:
enabled:
- "lucatume\\WPBrowser\\Extension\\Symlinker"
config:
"lucatume\\WPBrowser\\Extension\\Symlinker":
wpRootFolder: '%WP_ROOT_FOLDER%'
plugins:
- '%PLUGIN_STORAGE%/plugin-1'
- '%PLUGIN_STORAGE%/plugin-2'
themes:
- '%THEME_STORAGE%/theme-1'
- '%THEME_STORAGE%/theme-2'
```

[1]: https://docs.docker.com
[2]: https://docs.phpunit.de/en/10.5/attributes.html#test-isolation
[3]: commands.md#monkeycacheclear
Expand Down
196 changes: 196 additions & 0 deletions src/Extension/Symlinker.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
<?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',
];

private string $wpRootFolder = '';
/**
* @var string[]
*/
private array $plugins = [];
/**
* @var string[]
*/
private array $themes = [];
private string $pluginsDir = '';
private string $themesDir = '';
/**
* @var string[]
*/
private array $unlinkTargets = [];
private bool $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.");
}
}
}
Loading

0 comments on commit d67ba4c

Please sign in to comment.