Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add back Symlinker ext, use in setup #733

Merged
merged 1 commit into from
Jun 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading