From 4a98ebd78c86f7aca0d852c3c004ee157904bde4 Mon Sep 17 00:00:00 2001 From: Luca Tumedei Date: Wed, 5 Jun 2024 08:35:18 +0200 Subject: [PATCH] feat(Extensions) add back the Symlinker extension --- CHANGELOG.md | 5 + docker-compose.yml | 2 - docs/extensions.md | 51 +- src/Extension/Symlinker.php | 196 ++++ src/Project/ContentProject.php | 89 +- src/Project/PluginProject.php | 44 +- src/Project/ThemeProject.php | 10 +- ...fold_for_child_theme_correctly__0.snapshot | 14 +- ...lugin_with_non_plugin_php_file__0.snapshot | 14 +- ...or_plugin_with_plugin_php_file__0.snapshot | 14 +- ...d_scaffold_for_theme_correctly__0.snapshot | 14 +- .../WPBrowser/Extension/SymlinkerTest.php | 836 ++++++++++++++++++ 12 files changed, 1205 insertions(+), 84 deletions(-) create mode 100644 src/Extension/Symlinker.php create mode 100644 tests/unit/lucatume/WPBrowser/Extension/SymlinkerTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c77a3764..e2dbdd1f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/docker-compose.yml b/docker-compose.yml index b7a920ccd..c8fc4a268 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,3 @@ -version: "3.8" - services: database: container_name: wpbrowser_4_database diff --git a/docs/extensions.md b/docs/extensions.md index df3b2675c..ea04bf24d 100644 --- a/docs/extensions.md +++ b/docs/extensions.md @@ -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%' @@ -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 diff --git a/src/Extension/Symlinker.php b/src/Extension/Symlinker.php new file mode 100644 index 000000000..82efd4a20 --- /dev/null +++ b/src/Extension/Symlinker.php @@ -0,0 +1,196 @@ + + */ + 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."); + } + } +} diff --git a/src/Project/ContentProject.php b/src/Project/ContentProject.php index ce4772310..f635e4eda 100644 --- a/src/Project/ContentProject.php +++ b/src/Project/ContentProject.php @@ -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; @@ -25,50 +26,18 @@ abstract class ContentProject extends InitTemplate implements ProjectInterface { protected TestEnvironment $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|false */ abstract public static function parseDir(string $workDir): array|false; - 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 " . - "tests/_wordpress/wp-content/{$this->getProjectType()}s/$basename directory." - ); - $this->say( - "If your {$this->getProjectType()} requires additional plugins and themes, place them in the " . - 'tests/_wordpress/wp-content/plugins and ' . - 'tests/_wordpress/wp-content/themes directories.' - ); - }; - } - /** * @throws Throwable */ @@ -111,7 +80,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); @@ -147,6 +115,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%", @@ -161,7 +141,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; @@ -173,7 +154,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 " . + "tests/_wordpress/wp-content/{$this->getProjectType()}s/$basename 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 " . + "tests/_wordpress/wp-content/plugins and " . + "tests/_wordpress/wp-content/themes directories." + ); + $this->say( + "Read more about the Symlinker extension in the " . + "https://github.com/lucatume/wp-browser/blob/master/docs/extensions.md#symlinker file." + ); + }; + } + + abstract protected function scaffoldEndToEndActivationCest(): void; + + abstract protected function scaffoldIntegrationActivationTest(): void; + + abstract protected function getProjectType(): string; + + abstract protected function symlinkProjectInContentDir(string $wpRootDir): void; } diff --git a/src/Project/PluginProject.php b/src/Project/PluginProject.php index d3b43b37f..702ab7abf 100644 --- a/src/Project/PluginProject.php +++ b/src/Project/PluginProject.php @@ -20,18 +20,11 @@ class PluginProject extends ContentProject public const ERR_PLUGIN_NOT_FOUND = 1; private string $pluginFile; private string $pluginName; - private string $pluginDir; - - protected function getProjectType(): string - { - return 'plugin'; - } public function __construct(InputInterface $input, OutputInterface $output, protected string $workDir) { parent::__construct($input, $output); $pluginNameAndFile = self::parseDir($workDir); - $this->pluginDir = basename($this->workDir); if ($pluginNameAndFile === false) { throw new InvalidArgumentException( @@ -44,16 +37,6 @@ public function __construct(InputInterface $input, OutputInterface $output, prot $this->testEnvironment = new TestEnvironment(); } - public function getType(): string - { - return 'plugin'; - } - - public function getActivationString(): string - { - return basename($this->workDir) . '/' . basename($this->pluginFile); - } - /** * @return array{0: string, 1: string}|false */ @@ -89,9 +72,9 @@ public static function parseDir(string $workDir): array|false return [$realpath, $pluginName]; } - public function getName(): string + public function getType(): string { - return $this->pluginName; + return 'plugin'; } public function getPluginFilePathName(): string @@ -116,9 +99,11 @@ public function activate(string $wpRootDir, int $serverLocalhostPort): bool $this->say($activationResult->getFile() . ":" . $activationResult->getLine()); // @phpstan-ignore-next-line $dumpPath = Codecept::VERSION >= 5 ? 'tests/Support/Data/dump.sql' : 'tests/_data/dump.sql'; - $this->say('This might happen because the plugin has unmet dependencies; wp-browser configuration ' . + $this->say( + 'This might happen because the plugin has unmet dependencies; wp-browser configuration ' . 'will continue, but you will need to manually activate the plugin and update the dump in ' . - $dumpPath); + $dumpPath + ); return false; } @@ -127,6 +112,11 @@ public function activate(string $wpRootDir, int $serverLocalhostPort): bool return true; } + protected function getProjectType(): string + { + return 'plugin'; + } + protected function scaffoldEndToEndActivationCest(): void { // @phpstan-ignore-next-line @@ -169,6 +159,11 @@ public function test_it_deactivates_activates_correctly(EndToEndTester \$I): voi } } + public function getName(): string + { + return $this->pluginName; + } + protected function scaffoldIntegrationActivationTest(): void { $testCode = Strings::renderString( @@ -221,8 +216,13 @@ public function test_plugin_active(): void } } + public function getActivationString(): string + { + return basename($this->workDir) . '/' . basename($this->pluginFile); + } + protected function symlinkProjectInContentDir(string $wpRootDir): void { - FS::symlink($this->workDir, $wpRootDir . "/wp-content/plugins/" . $this->pluginDir); + FS::symlink($this->workDir, $wpRootDir . '/wp-content/plugins/' . basename($this->workDir)); } } diff --git a/src/Project/ThemeProject.php b/src/Project/ThemeProject.php index 9fb493cb1..ca3722923 100644 --- a/src/Project/ThemeProject.php +++ b/src/Project/ThemeProject.php @@ -88,11 +88,6 @@ public static function parseDir(string $workDir): array|false return [$name]; } - protected function symlinkProjectInContentDir(string $wpRootDir): void - { - FS::symlink($this->workDir, $wpRootDir . "/wp-content/themes/" . $this->basename); - } - /** * @throws WorkerException * @throws Throwable @@ -204,4 +199,9 @@ public function test_theme_active(): void throw new RuntimeException('Could not write tests/Integration/SampleTest.php.'); } } + + protected function symlinkProjectInContentDir(string $wpRootDir): void + { + FS::symlink($this->workDir, $wpRootDir . '/wp-content/themes/' . basename($this->workDir)); + } } diff --git a/tests/unit/Codeception/Template/__snapshots__/WpbrowserTest__should_scaffold_for_child_theme_correctly__0.snapshot b/tests/unit/Codeception/Template/__snapshots__/WpbrowserTest__should_scaffold_for_child_theme_correctly__0.snapshot index d3065ab51..644e13d3b 100644 --- a/tests/unit/Codeception/Template/__snapshots__/WpbrowserTest__should_scaffold_for_child_theme_correctly__0.snapshot +++ b/tests/unit/Codeception/Template/__snapshots__/WpbrowserTest__should_scaffold_for_child_theme_correctly__0.snapshot @@ -270,8 +270,8 @@ TEST_TABLE_PREFIX=test_ WORDPRESS_TABLE_PREFIX=wp_ # The URL and domain of the WordPress site used in end-to-end tests. -WORDPRESS_URL=http://localhost:59860 -WORDPRESS_DOMAIN=localhost:59860 +WORDPRESS_URL=http://localhost:32347 +WORDPRESS_DOMAIN=localhost:32347 WORDPRESS_ADMIN_PATH=/wp-admin # The username and password of the administrator user of the WordPress site used in end-to-end tests. @@ -280,10 +280,10 @@ WORDPRESS_ADMIN_PASSWORD=password # The host and port of the ChromeDriver server that will be used in end-to-end tests. CHROMEDRIVER_HOST=localhost -CHROMEDRIVER_PORT=21551 +CHROMEDRIVER_PORT=59028 # The port on which the PHP built-in server will serve the WordPress installation. -BUILTIN_SERVER_PORT=59860 +BUILTIN_SERVER_PORT=32347 <<< /tests/.env <<< @@ -310,6 +310,7 @@ extensions: - Codeception\Extension\RunFailed - lucatume\WPBrowser\Extension\ChromeDriverController - lucatume\WPBrowser\Extension\BuiltInServerController + - lucatume\WPBrowser\Extension\Symlinker config: lucatume\WPBrowser\Extension\ChromeDriverController: port: '%CHROMEDRIVER_PORT%' @@ -322,6 +323,11 @@ extensions: DB_ENGINE: sqlite DB_DIR: '%codecept_root_dir%/tests/Support/Data' DB_FILE: db.sqlite + lucatume\WPBrowser\Extension\Symlinker: + wpRootFolder: '%WORDPRESS_ROOT_DIR%' + plugins: { } + themes: + - . commands: - lucatume\WPBrowser\Command\RunOriginal - lucatume\WPBrowser\Command\RunAll diff --git a/tests/unit/Codeception/Template/__snapshots__/WpbrowserTest__should_scaffold_for_plugin_with_non_plugin_php_file__0.snapshot b/tests/unit/Codeception/Template/__snapshots__/WpbrowserTest__should_scaffold_for_plugin_with_non_plugin_php_file__0.snapshot index 4e155a908..dc1230795 100644 --- a/tests/unit/Codeception/Template/__snapshots__/WpbrowserTest__should_scaffold_for_plugin_with_non_plugin_php_file__0.snapshot +++ b/tests/unit/Codeception/Template/__snapshots__/WpbrowserTest__should_scaffold_for_plugin_with_non_plugin_php_file__0.snapshot @@ -265,8 +265,8 @@ TEST_TABLE_PREFIX=test_ WORDPRESS_TABLE_PREFIX=wp_ # The URL and domain of the WordPress site used in end-to-end tests. -WORDPRESS_URL=http://localhost:43626 -WORDPRESS_DOMAIN=localhost:43626 +WORDPRESS_URL=http://localhost:48989 +WORDPRESS_DOMAIN=localhost:48989 WORDPRESS_ADMIN_PATH=/wp-admin # The username and password of the administrator user of the WordPress site used in end-to-end tests. @@ -275,10 +275,10 @@ WORDPRESS_ADMIN_PASSWORD=password # The host and port of the ChromeDriver server that will be used in end-to-end tests. CHROMEDRIVER_HOST=localhost -CHROMEDRIVER_PORT=35449 +CHROMEDRIVER_PORT=10540 # The port on which the PHP built-in server will serve the WordPress installation. -BUILTIN_SERVER_PORT=43626 +BUILTIN_SERVER_PORT=48989 <<< /tests/.env <<< @@ -305,6 +305,7 @@ extensions: - Codeception\Extension\RunFailed - lucatume\WPBrowser\Extension\ChromeDriverController - lucatume\WPBrowser\Extension\BuiltInServerController + - lucatume\WPBrowser\Extension\Symlinker config: lucatume\WPBrowser\Extension\ChromeDriverController: port: '%CHROMEDRIVER_PORT%' @@ -317,6 +318,11 @@ extensions: DB_ENGINE: sqlite DB_DIR: '%codecept_root_dir%/tests/Support/Data' DB_FILE: db.sqlite + lucatume\WPBrowser\Extension\Symlinker: + wpRootFolder: '%WORDPRESS_ROOT_DIR%' + plugins: + - . + themes: { } commands: - lucatume\WPBrowser\Command\RunOriginal - lucatume\WPBrowser\Command\RunAll diff --git a/tests/unit/Codeception/Template/__snapshots__/WpbrowserTest__should_scaffold_for_plugin_with_plugin_php_file__0.snapshot b/tests/unit/Codeception/Template/__snapshots__/WpbrowserTest__should_scaffold_for_plugin_with_plugin_php_file__0.snapshot index 28262e513..745075eca 100644 --- a/tests/unit/Codeception/Template/__snapshots__/WpbrowserTest__should_scaffold_for_plugin_with_plugin_php_file__0.snapshot +++ b/tests/unit/Codeception/Template/__snapshots__/WpbrowserTest__should_scaffold_for_plugin_with_plugin_php_file__0.snapshot @@ -270,8 +270,8 @@ TEST_TABLE_PREFIX=test_ WORDPRESS_TABLE_PREFIX=wp_ # The URL and domain of the WordPress site used in end-to-end tests. -WORDPRESS_URL=http://localhost:14326 -WORDPRESS_DOMAIN=localhost:14326 +WORDPRESS_URL=http://localhost:51713 +WORDPRESS_DOMAIN=localhost:51713 WORDPRESS_ADMIN_PATH=/wp-admin # The username and password of the administrator user of the WordPress site used in end-to-end tests. @@ -280,10 +280,10 @@ WORDPRESS_ADMIN_PASSWORD=password # The host and port of the ChromeDriver server that will be used in end-to-end tests. CHROMEDRIVER_HOST=localhost -CHROMEDRIVER_PORT=34581 +CHROMEDRIVER_PORT=41985 # The port on which the PHP built-in server will serve the WordPress installation. -BUILTIN_SERVER_PORT=14326 +BUILTIN_SERVER_PORT=51713 <<< /tests/.env <<< @@ -310,6 +310,7 @@ extensions: - Codeception\Extension\RunFailed - lucatume\WPBrowser\Extension\ChromeDriverController - lucatume\WPBrowser\Extension\BuiltInServerController + - lucatume\WPBrowser\Extension\Symlinker config: lucatume\WPBrowser\Extension\ChromeDriverController: port: '%CHROMEDRIVER_PORT%' @@ -322,6 +323,11 @@ extensions: DB_ENGINE: sqlite DB_DIR: '%codecept_root_dir%/tests/Support/Data' DB_FILE: db.sqlite + lucatume\WPBrowser\Extension\Symlinker: + wpRootFolder: '%WORDPRESS_ROOT_DIR%' + plugins: + - . + themes: { } commands: - lucatume\WPBrowser\Command\RunOriginal - lucatume\WPBrowser\Command\RunAll diff --git a/tests/unit/Codeception/Template/__snapshots__/WpbrowserTest__should_scaffold_for_theme_correctly__0.snapshot b/tests/unit/Codeception/Template/__snapshots__/WpbrowserTest__should_scaffold_for_theme_correctly__0.snapshot index 7e4d61fd6..b8056a07b 100644 --- a/tests/unit/Codeception/Template/__snapshots__/WpbrowserTest__should_scaffold_for_theme_correctly__0.snapshot +++ b/tests/unit/Codeception/Template/__snapshots__/WpbrowserTest__should_scaffold_for_theme_correctly__0.snapshot @@ -270,8 +270,8 @@ TEST_TABLE_PREFIX=test_ WORDPRESS_TABLE_PREFIX=wp_ # The URL and domain of the WordPress site used in end-to-end tests. -WORDPRESS_URL=http://localhost:8431 -WORDPRESS_DOMAIN=localhost:8431 +WORDPRESS_URL=http://localhost:10603 +WORDPRESS_DOMAIN=localhost:10603 WORDPRESS_ADMIN_PATH=/wp-admin # The username and password of the administrator user of the WordPress site used in end-to-end tests. @@ -280,10 +280,10 @@ WORDPRESS_ADMIN_PASSWORD=password # The host and port of the ChromeDriver server that will be used in end-to-end tests. CHROMEDRIVER_HOST=localhost -CHROMEDRIVER_PORT=44085 +CHROMEDRIVER_PORT=43861 # The port on which the PHP built-in server will serve the WordPress installation. -BUILTIN_SERVER_PORT=8431 +BUILTIN_SERVER_PORT=10603 <<< /tests/.env <<< @@ -310,6 +310,7 @@ extensions: - Codeception\Extension\RunFailed - lucatume\WPBrowser\Extension\ChromeDriverController - lucatume\WPBrowser\Extension\BuiltInServerController + - lucatume\WPBrowser\Extension\Symlinker config: lucatume\WPBrowser\Extension\ChromeDriverController: port: '%CHROMEDRIVER_PORT%' @@ -322,6 +323,11 @@ extensions: DB_ENGINE: sqlite DB_DIR: '%codecept_root_dir%/tests/Support/Data' DB_FILE: db.sqlite + lucatume\WPBrowser\Extension\Symlinker: + wpRootFolder: '%WORDPRESS_ROOT_DIR%' + plugins: { } + themes: + - . commands: - lucatume\WPBrowser\Command\RunOriginal - lucatume\WPBrowser\Command\RunAll diff --git a/tests/unit/lucatume/WPBrowser/Extension/SymlinkerTest.php b/tests/unit/lucatume/WPBrowser/Extension/SymlinkerTest.php new file mode 100644 index 000000000..634d06596 --- /dev/null +++ b/tests/unit/lucatume/WPBrowser/Extension/SymlinkerTest.php @@ -0,0 +1,836 @@ + __DIR__, + ], []); + + $this->assertInstanceOf(Symlinker::class, $symlinker); + } + + public function test_throw_if_wp_root_folder_is_not_set(): void + { + $this->expectException(ModuleConfigException::class); + $this->expectExceptionMessage('The `wpRootFolder` configuration parameter must be set.'); + + $symlinker = new Symlinker([ + ], []); + $symlinker->onModuleInit(new SuiteEvent()); + } + + public function test_throw_if_wp_root_folder_does_not_point_to_a_valid_installation(): void + { + $symlinker = new Symlinker([ + 'wpRootFolder' => __DIR__, + ], []); + + $this->expectException(ModuleConfigException::class); + $this->expectExceptionMessage('The `wpRootFolder` does not point to a valid WordPress installation.'); + + $this->assertInIsolation(static function () use ($symlinker) { + $symlinker->onModuleInit(new SuiteEvent()); + }); + } + + public function test_throw_if_plugins_are_not_array(): void + { + $this->expectException(ModuleConfigException::class); + $this->expectExceptionMessage('The `plugins` configuration parameter must be an array.'); + + $symlinker = new Symlinker([ + 'wpRootFolder' => __DIR__, + 'plugins' => 'not-an-array', + ], []); + $symlinker->onModuleInit(new SuiteEvent()); + } + + public function test_throw_if_themes_are_not_array(): void + { + $this->expectException(ModuleConfigException::class); + $this->expectExceptionMessage('The `themes` configuration parameter must be an array.'); + + $symlinker = new Symlinker([ + 'wpRootFolder' => __DIR__, + 'themes' => 'not-an-array', + ], []); + $symlinker->onModuleInit(new SuiteEvent()); + } + + public function test_without_plugins_or_themes(): void + { + $workingDir = FS::tmpDir('symlinker_'); + $wpRoot = FS::tmpDir('symlinker_'); + Installation::scaffold($wpRoot); + + $symlinker = new Symlinker([ + 'wpRootFolder' => $wpRoot, + ], []); + + $this->assertInIsolation(static function () use ($symlinker, $workingDir) { + chdir($workingDir); + + Assert::assertSame($workingDir, getcwd()); + + $symlinker->onModuleInit(new SuiteEvent()); + $symlinker->afterSuite(new SuiteEvent()); + }); + } + + public function test_throws_if_plugin_file_does_not_exist(): void + { + $wpRoot = FS::tmpDir('symlinker_', [ + 'wp-content' => [ + 'plugins' => [], + 'themes' => [] + ] + ]); + + $this->expectException(ModuleConfigException::class); + $this->expectExceptionMessage('Plugin file not-a-file/plugin.php does not exist.'); + + + $symlinker = new Symlinker([ + 'wpRootFolder' => $wpRoot, + 'plugins' => [ + 'not-a-file/plugin.php', + ], + ], []); + } + + public function test_throws_if_theme_is_not_a_directory(): void + { + $wpRoot = FS::tmpDir('symlinker_', [ + 'wp-content' => [ + 'plugins' => [], + 'themes' => [] + ] + ]); + + $this->expectException(ModuleConfigException::class); + $this->expectExceptionMessage('Theme directory not-a-directory does not exist.'); + + $symlinker = new Symlinker([ + 'wpRootFolder' => $wpRoot, + 'themes' => [ + 'not-a-directory', + ], + ], []); + } + + public function test_with_relative_paths(): void + { + $workingDir = FS::tmpDir('symlinker_', [ + 'vendor' => [ + 'acme' => [ + 'plugin-1' => [ + 'plugin-1.php' => <<< PHP + [ + 'main.php' => <<< PHP + [ + 'style.css' => <<< CSS + /* + Theme Name: Theme 1 + */ + CSS, + 'index.php' => '', + 'functions.php' => <<< PHP + [ + 'style.css' => <<< CSS + /* + Theme Name: Theme 2 + */ + CSS, + 'index.php' => '', + 'functions.php' => <<< PHP + assertInIsolation(static function () use ($workingDir, $wpRoot) { + chdir($workingDir); + + Assert::assertSame($workingDir, getcwd()); + + $symlinker = new Symlinker([ + 'wpRootFolder' => $wpRoot, + 'cleanupAfterSuite' => true, + 'plugins' => [ + 'vendor/acme/plugin-1', + 'vendor/acme/plugin-2' + ], + 'themes' => [ + 'vendor/acme/theme-1', + 'vendor/acme/theme-2' + ] + ], []); + + Assert::assertFileDoesNotExist($wpRoot . '/wp-content/plugins/plugin-1/plugin-1.php'); + Assert::assertFileDoesNotExist($wpRoot . '/wp-content/plugins/plugin-2/main.php'); + Assert::assertFileDoesNotExist($wpRoot . '/wp-content/themes/theme-1/style.css'); + Assert::assertFileDoesNotExist($wpRoot . '/wp-content/themes/theme-1/index.php'); + Assert::assertFileDoesNotExist($wpRoot . '/wp-content/themes/theme-1/functions.php'); + Assert::assertFileDoesNotExist($wpRoot . '/wp-content/themes/theme-2/style.css'); + Assert::assertFileDoesNotExist($wpRoot . '/wp-content/themes/theme-2/index.php'); + Assert::assertFileDoesNotExist($wpRoot . '/wp-content/themes/theme-2/functions.php'); + + $symlinker->onModuleInit(new SuiteEvent()); + + Assert::assertFileExists($wpRoot . '/wp-content/plugins/plugin-1/plugin-1.php'); + Assert::assertFileExists($wpRoot . '/wp-content/plugins/plugin-2/main.php'); + Assert::assertFileExists($wpRoot . '/wp-content/themes/theme-1/style.css'); + Assert::assertFileExists($wpRoot . '/wp-content/themes/theme-1/index.php'); + Assert::assertFileExists($wpRoot . '/wp-content/themes/theme-1/functions.php'); + Assert::assertFileExists($wpRoot . '/wp-content/themes/theme-2/style.css'); + Assert::assertFileExists($wpRoot . '/wp-content/themes/theme-2/index.php'); + Assert::assertFileExists($wpRoot . '/wp-content/themes/theme-2/functions.php'); + + $symlinker->afterSuite(new SuiteEvent()); + + Assert::assertFileDoesNotExist($wpRoot . '/wp-content/plugins/plugin-1/plugin-1.php'); + Assert::assertFileDoesNotExist($wpRoot . '/wp-content/plugins/plugin-2/main.php'); + Assert::assertFileDoesNotExist($wpRoot . '/wp-content/themes/theme-1/style.css'); + Assert::assertFileDoesNotExist($wpRoot . '/wp-content/themes/theme-1/index.php'); + Assert::assertFileDoesNotExist($wpRoot . '/wp-content/themes/theme-1/functions.php'); + Assert::assertFileDoesNotExist($wpRoot . '/wp-content/themes/theme-2/style.css'); + Assert::assertFileDoesNotExist($wpRoot . '/wp-content/themes/theme-2/index.php'); + Assert::assertFileDoesNotExist($wpRoot . '/wp-content/themes/theme-2/functions.php'); + }); + } + + public function test_with_absolute_paths(): void + { + $workingDir = FS::tmpDir('symlinker_', [ + 'vendor' => [ + 'acme' => [ + 'plugin-1' => [ + 'plugin-1.php' => <<< PHP + [ + 'main.php' => <<< PHP + [ + 'style.css' => <<< CSS + /* + Theme Name: Theme 1 + */ + CSS, + 'index.php' => '', + 'functions.php' => <<< PHP + [ + 'style.css' => <<< CSS + /* + Theme Name: Theme 2 + */ + CSS, + 'index.php' => '', + 'functions.php' => <<< PHP + assertInIsolation(static function () use ($workingDir, $wpRoot) { + chdir($workingDir); + + Assert::assertSame($workingDir, getcwd()); + + $symlinker = new Symlinker([ + 'wpRootFolder' => $wpRoot, + 'cleanupAfterSuite' => true, + 'plugins' => [ + $workingDir . '/vendor/acme/plugin-1', + $workingDir . '/vendor/acme/plugin-2' + ], + 'themes' => [ + $workingDir . '/vendor/acme/theme-1', + $workingDir . '/vendor/acme/theme-2' + ] + ], []); + + Assert::assertFileDoesNotExist($wpRoot . '/wp-content/plugins/plugin-1/plugin-1.php'); + Assert::assertFileDoesNotExist($wpRoot . '/wp-content/plugins/plugin-2/main.php'); + Assert::assertFileDoesNotExist($wpRoot . '/wp-content/themes/theme-1/style.css'); + Assert::assertFileDoesNotExist($wpRoot . '/wp-content/themes/theme-1/index.php'); + Assert::assertFileDoesNotExist($wpRoot . '/wp-content/themes/theme-1/functions.php'); + Assert::assertFileDoesNotExist($wpRoot . '/wp-content/themes/theme-2/style.css'); + Assert::assertFileDoesNotExist($wpRoot . '/wp-content/themes/theme-2/index.php'); + Assert::assertFileDoesNotExist($wpRoot . '/wp-content/themes/theme-2/functions.php'); + + $symlinker->onModuleInit(new SuiteEvent()); + + Assert::assertFileExists($wpRoot . '/wp-content/plugins/plugin-1/plugin-1.php'); + Assert::assertFileExists($wpRoot . '/wp-content/plugins/plugin-2/main.php'); + Assert::assertFileExists($wpRoot . '/wp-content/themes/theme-1/style.css'); + Assert::assertFileExists($wpRoot . '/wp-content/themes/theme-1/index.php'); + Assert::assertFileExists($wpRoot . '/wp-content/themes/theme-1/functions.php'); + Assert::assertFileExists($wpRoot . '/wp-content/themes/theme-2/style.css'); + Assert::assertFileExists($wpRoot . '/wp-content/themes/theme-2/index.php'); + Assert::assertFileExists($wpRoot . '/wp-content/themes/theme-2/functions.php'); + + $symlinker->afterSuite(new SuiteEvent()); + + Assert::assertFileDoesNotExist($wpRoot . '/wp-content/plugins/plugin-1/plugin-1.php'); + Assert::assertFileDoesNotExist($wpRoot . '/wp-content/plugins/plugin-2/main.php'); + Assert::assertFileDoesNotExist($wpRoot . '/wp-content/themes/theme-1/style.css'); + Assert::assertFileDoesNotExist($wpRoot . '/wp-content/themes/theme-1/index.php'); + Assert::assertFileDoesNotExist($wpRoot . '/wp-content/themes/theme-1/functions.php'); + Assert::assertFileDoesNotExist($wpRoot . '/wp-content/themes/theme-2/style.css'); + Assert::assertFileDoesNotExist($wpRoot . '/wp-content/themes/theme-2/index.php'); + Assert::assertFileDoesNotExist($wpRoot . '/wp-content/themes/theme-2/functions.php'); + }); + } + + public function test_will_not_cleanup_after_suite_by_default(): void + { + $workingDir = FS::tmpDir('symlinker_', [ + 'vendor' => [ + 'acme' => [ + 'plugin-1' => [ + 'plugin-1.php' => <<< PHP + [ + 'main.php' => <<< PHP + [ + 'style.css' => <<< CSS + /* + Theme Name: Theme 1 + */ + CSS, + 'index.php' => '', + 'functions.php' => <<< PHP + [ + 'style.css' => <<< CSS + /* + Theme Name: Theme 2 + */ + CSS, + 'index.php' => '', + 'functions.php' => <<< PHP + assertInIsolation(static function () use ($workingDir, $wpRoot) { + chdir($workingDir); + + Assert::assertSame($workingDir, getcwd()); + + $symlinker = new Symlinker([ + 'wpRootFolder' => $wpRoot, + 'plugins' => [ + $workingDir . '/vendor/acme/plugin-1', + $workingDir . '/vendor/acme/plugin-2' + ], + 'themes' => [ + $workingDir . '/vendor/acme/theme-1', + $workingDir . '/vendor/acme/theme-2' + ] + ], []); + + Assert::assertFileDoesNotExist($wpRoot . '/wp-content/plugins/plugin-1/plugin-1.php'); + Assert::assertFileDoesNotExist($wpRoot . '/wp-content/plugins/plugin-2/main.php'); + Assert::assertFileDoesNotExist($wpRoot . '/wp-content/themes/theme-1/style.css'); + Assert::assertFileDoesNotExist($wpRoot . '/wp-content/themes/theme-1/index.php'); + Assert::assertFileDoesNotExist($wpRoot . '/wp-content/themes/theme-1/functions.php'); + Assert::assertFileDoesNotExist($wpRoot . '/wp-content/themes/theme-2/style.css'); + Assert::assertFileDoesNotExist($wpRoot . '/wp-content/themes/theme-2/index.php'); + Assert::assertFileDoesNotExist($wpRoot . '/wp-content/themes/theme-2/functions.php'); + + $symlinker->onModuleInit(new SuiteEvent()); + + Assert::assertFileExists($wpRoot . '/wp-content/plugins/plugin-1/plugin-1.php'); + Assert::assertFileExists($wpRoot . '/wp-content/plugins/plugin-2/main.php'); + Assert::assertFileExists($wpRoot . '/wp-content/themes/theme-1/style.css'); + Assert::assertFileExists($wpRoot . '/wp-content/themes/theme-1/index.php'); + Assert::assertFileExists($wpRoot . '/wp-content/themes/theme-1/functions.php'); + Assert::assertFileExists($wpRoot . '/wp-content/themes/theme-2/style.css'); + Assert::assertFileExists($wpRoot . '/wp-content/themes/theme-2/index.php'); + Assert::assertFileExists($wpRoot . '/wp-content/themes/theme-2/functions.php'); + + $symlinker->afterSuite(new SuiteEvent()); + + Assert::assertFileExists($wpRoot . '/wp-content/plugins/plugin-1/plugin-1.php'); + Assert::assertFileExists($wpRoot . '/wp-content/plugins/plugin-2/main.php'); + Assert::assertFileExists($wpRoot . '/wp-content/themes/theme-1/style.css'); + Assert::assertFileExists($wpRoot . '/wp-content/themes/theme-1/index.php'); + Assert::assertFileExists($wpRoot . '/wp-content/themes/theme-1/functions.php'); + Assert::assertFileExists($wpRoot . '/wp-content/themes/theme-2/style.css'); + Assert::assertFileExists($wpRoot . '/wp-content/themes/theme-2/index.php'); + Assert::assertFileExists($wpRoot . '/wp-content/themes/theme-2/functions.php'); + }); + } + + public function test_will_not_cleanup_after_suite_if_configured_not_to(): void + { + $workingDir = FS::tmpDir('symlinker_', [ + 'vendor' => [ + 'acme' => [ + 'plugin-1' => [ + 'plugin-1.php' => <<< PHP + [ + 'main.php' => <<< PHP + [ + 'style.css' => <<< CSS + /* + Theme Name: Theme 1 + */ + CSS, + 'index.php' => '', + 'functions.php' => <<< PHP + [ + 'style.css' => <<< CSS + /* + Theme Name: Theme 2 + */ + CSS, + 'index.php' => '', + 'functions.php' => <<< PHP + assertInIsolation(static function () use ($workingDir, $wpRoot) { + chdir($workingDir); + + Assert::assertSame($workingDir, getcwd()); + + $symlinker = new Symlinker([ + 'wpRootFolder' => $wpRoot, + 'cleanupAfterSuite' => false, + 'plugins' => [ + $workingDir . '/vendor/acme/plugin-1', + $workingDir . '/vendor/acme/plugin-2' + ], + 'themes' => [ + $workingDir . '/vendor/acme/theme-1', + $workingDir . '/vendor/acme/theme-2' + ] + ], []); + + Assert::assertFileDoesNotExist($wpRoot . '/wp-content/plugins/plugin-1/plugin-1.php'); + Assert::assertFileDoesNotExist($wpRoot . '/wp-content/plugins/plugin-2/main.php'); + Assert::assertFileDoesNotExist($wpRoot . '/wp-content/themes/theme-1/style.css'); + Assert::assertFileDoesNotExist($wpRoot . '/wp-content/themes/theme-1/index.php'); + Assert::assertFileDoesNotExist($wpRoot . '/wp-content/themes/theme-1/functions.php'); + Assert::assertFileDoesNotExist($wpRoot . '/wp-content/themes/theme-2/style.css'); + Assert::assertFileDoesNotExist($wpRoot . '/wp-content/themes/theme-2/index.php'); + Assert::assertFileDoesNotExist($wpRoot . '/wp-content/themes/theme-2/functions.php'); + + $symlinker->onModuleInit(new SuiteEvent()); + + Assert::assertFileExists($wpRoot . '/wp-content/plugins/plugin-1/plugin-1.php'); + Assert::assertFileExists($wpRoot . '/wp-content/plugins/plugin-2/main.php'); + Assert::assertFileExists($wpRoot . '/wp-content/themes/theme-1/style.css'); + Assert::assertFileExists($wpRoot . '/wp-content/themes/theme-1/index.php'); + Assert::assertFileExists($wpRoot . '/wp-content/themes/theme-1/functions.php'); + Assert::assertFileExists($wpRoot . '/wp-content/themes/theme-2/style.css'); + Assert::assertFileExists($wpRoot . '/wp-content/themes/theme-2/index.php'); + Assert::assertFileExists($wpRoot . '/wp-content/themes/theme-2/functions.php'); + + $symlinker->afterSuite(new SuiteEvent()); + + Assert::assertFileExists($wpRoot . '/wp-content/plugins/plugin-1/plugin-1.php'); + Assert::assertFileExists($wpRoot . '/wp-content/plugins/plugin-2/main.php'); + Assert::assertFileExists($wpRoot . '/wp-content/themes/theme-1/style.css'); + Assert::assertFileExists($wpRoot . '/wp-content/themes/theme-1/index.php'); + Assert::assertFileExists($wpRoot . '/wp-content/themes/theme-1/functions.php'); + Assert::assertFileExists($wpRoot . '/wp-content/themes/theme-2/style.css'); + Assert::assertFileExists($wpRoot . '/wp-content/themes/theme-2/index.php'); + Assert::assertFileExists($wpRoot . '/wp-content/themes/theme-2/functions.php'); + }); + } + + public function test_will_leave_existing_symlinks_in_place(): void + { + $workingDir = FS::tmpDir('symlinker_', [ + 'vendor' => [ + 'acme' => [ + 'plugin-1' => [ + 'plugin-1.php' => <<< PHP + [ + 'main.php' => <<< PHP + [ + 'style.css' => <<< CSS + /* + Theme Name: Theme 1 + */ + CSS, + 'index.php' => '', + 'functions.php' => <<< PHP + [ + 'style.css' => <<< CSS + /* + Theme Name: Theme 2 + */ + CSS, + 'index.php' => '', + 'functions.php' => <<< PHP + assertInIsolation(static function () use ($workingDir, $wpRoot) { + chdir($workingDir); + + Assert::assertSame($workingDir, getcwd()); + + $symlinker = new Symlinker([ + 'wpRootFolder' => $wpRoot, + 'cleanupAfterSuite' => true, + 'plugins' => [ + $workingDir . '/vendor/acme/plugin-1', + $workingDir . '/vendor/acme/plugin-2' + ], + 'themes' => [ + $workingDir . '/vendor/acme/theme-1', + $workingDir . '/vendor/acme/theme-2' + ] + ], []); + + if (!( + symlink($workingDir . '/vendor/acme/plugin-1', $wpRoot . '/wp-content/plugins/plugin-1') + && symlink($workingDir . '/vendor/acme/plugin-2', $wpRoot . '/wp-content/plugins/plugin-2') + && symlink($workingDir . '/vendor/acme/theme-1', $wpRoot . '/wp-content/themes/theme-1') + && symlink($workingDir . '/vendor/acme/theme-2', $wpRoot . '/wp-content/themes/theme-2')) + ) { + throw new \RuntimeException('Could not create symlinks in ' . $wpRoot); + } + + Assert::assertFileExists($wpRoot . '/wp-content/plugins/plugin-1/plugin-1.php'); + Assert::assertFileExists($wpRoot . '/wp-content/plugins/plugin-2/main.php'); + Assert::assertFileExists($wpRoot . '/wp-content/themes/theme-1/style.css'); + Assert::assertFileExists($wpRoot . '/wp-content/themes/theme-1/index.php'); + Assert::assertFileExists($wpRoot . '/wp-content/themes/theme-1/functions.php'); + Assert::assertFileExists($wpRoot . '/wp-content/themes/theme-2/style.css'); + Assert::assertFileExists($wpRoot . '/wp-content/themes/theme-2/index.php'); + Assert::assertFileExists($wpRoot . '/wp-content/themes/theme-2/functions.php'); + + $symlinker->onModuleInit(new SuiteEvent()); + + Assert::assertFileExists($wpRoot . '/wp-content/plugins/plugin-1/plugin-1.php'); + Assert::assertFileExists($wpRoot . '/wp-content/plugins/plugin-2/main.php'); + Assert::assertFileExists($wpRoot . '/wp-content/themes/theme-1/style.css'); + Assert::assertFileExists($wpRoot . '/wp-content/themes/theme-1/index.php'); + Assert::assertFileExists($wpRoot . '/wp-content/themes/theme-1/functions.php'); + Assert::assertFileExists($wpRoot . '/wp-content/themes/theme-2/style.css'); + Assert::assertFileExists($wpRoot . '/wp-content/themes/theme-2/index.php'); + Assert::assertFileExists($wpRoot . '/wp-content/themes/theme-2/functions.php'); + + $symlinker->afterSuite(new SuiteEvent()); + + Assert::assertFileExists($wpRoot . '/wp-content/plugins/plugin-1/plugin-1.php'); + Assert::assertFileExists($wpRoot . '/wp-content/plugins/plugin-2/main.php'); + Assert::assertFileExists($wpRoot . '/wp-content/themes/theme-1/style.css'); + Assert::assertFileExists($wpRoot . '/wp-content/themes/theme-1/index.php'); + Assert::assertFileExists($wpRoot . '/wp-content/themes/theme-1/functions.php'); + Assert::assertFileExists($wpRoot . '/wp-content/themes/theme-2/style.css'); + Assert::assertFileExists($wpRoot . '/wp-content/themes/theme-2/index.php'); + Assert::assertFileExists($wpRoot . '/wp-content/themes/theme-2/functions.php'); + }); + } + + public function test_will_throw_if_link_found_not_pointing_to_same_target(): void + { + $workingDir = FS::tmpDir('symlinker_', [ + 'vendor' => [ + 'acme' => [ + 'plugin-1' => [ + 'plugin-1.php' => <<< PHP + [ + 'main.php' => <<< PHP + expectException(ModuleException::class); + $this->expectExceptionMessage( + "Could not symlink plugin $workingDir/vendor/acme/plugin-2 to $wpRoot/wp-content/plugins/plugin-2: link already exists and target is $otherDir." + ); + + $this->assertInIsolation(static function () use ($workingDir, $wpRoot, $otherDir) { + chdir($workingDir); + + Assert::assertSame($workingDir, getcwd()); + + $symlinker = new Symlinker([ + 'wpRootFolder' => $wpRoot, + 'cleanupAfterSuite' => true, + 'plugins' => [ + $workingDir . '/vendor/acme/plugin-1', + $workingDir . '/vendor/acme/plugin-2' + ], + ], []); + + if (!( + symlink($workingDir . '/vendor/acme/plugin-1', $wpRoot . '/wp-content/plugins/plugin-1') + && symlink($otherDir, $wpRoot . '/wp-content/plugins/plugin-2') + )) { + throw new \RuntimeException('Could not create symlinks in ' . $wpRoot); + } + + $symlinker->onModuleInit(new SuiteEvent()); + }); + } + + public function test_allows_the_dot_as_relative_path(): void + { + $workingDir = FS::tmpDir('symlinker_', [ + 'plugin.php' => <<< PHP + assertInIsolation(static function () use ($workingDir, $wpRoot) { + chdir($workingDir); + + Assert::assertSame($workingDir, getcwd()); + + $symlinker = new Symlinker([ + 'wpRootFolder' => $wpRoot, + 'plugins' => [ + '.', + ] + ], []); + + $workDirBasename = basename($workingDir); + + Assert::assertFileDoesNotExist($wpRoot . "/wp-content/plugins/{$workDirBasename}/plugin.php"); + + $symlinker->onModuleInit(new SuiteEvent()); + + Assert::assertFileExists($wpRoot . "/wp-content/plugins/{$workDirBasename}/plugin.php"); + Assert::assertTrue(is_link($wpRoot . "/wp-content/plugins/{$workDirBasename}")); + Assert::assertEquals($workingDir, readlink($wpRoot . "/wp-content/plugins/{$workDirBasename}")); + }); + } +}