From 8452aceab2a0bb409f72dd2bbd49cbc2e0be80ab Mon Sep 17 00:00:00 2001 From: Luca Tumedei Date: Wed, 5 Jun 2024 10:17:26 +0200 Subject: [PATCH] feat(Extensions) readd the Symlinker extension, use in setup --- src/Extension/Symlinker.php | 208 +++++ src/Project/ContentProject.php | 89 +- src/Project/PluginProject.php | 47 +- 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 | 869 ++++++++++++++++++ 9 files changed, 1197 insertions(+), 82 deletions(-) create mode 100644 src/Extension/Symlinker.php create mode 100644 tests/unit/lucatume/WPBrowser/Extension/SymlinkerTest.php diff --git a/src/Extension/Symlinker.php b/src/Extension/Symlinker.php new file mode 100644 index 000000000..f88176370 --- /dev/null +++ b/src/Extension/Symlinker.php @@ -0,0 +1,208 @@ + + */ + 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."); + } + } +} diff --git a/src/Project/ContentProject.php b/src/Project/ContentProject.php index 2b093cb4d..75ae48022 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; @@ -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|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 " . - "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 */ @@ -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); @@ -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%", @@ -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; @@ -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 " . + "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 289e223a8..444ea9fda 100644 --- a/src/Project/PluginProject.php +++ b/src/Project/PluginProject.php @@ -30,22 +30,12 @@ class PluginProject extends ContentProject * @var string */ private $pluginName; - /** - * @var string - */ - private $pluginDir; - - protected function getProjectType(): string - { - return 'plugin'; - } public function __construct(InputInterface $input, OutputInterface $output, string $workDir) { $this->workDir = $workDir; parent::__construct($input, $output); $pluginNameAndFile = self::parseDir($workDir); - $this->pluginDir = basename($this->workDir); if ($pluginNameAndFile === false) { throw new InvalidArgumentException( @@ -58,16 +48,6 @@ public function __construct(InputInterface $input, OutputInterface $output, stri $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 */ @@ -103,9 +83,9 @@ public static function parseDir(string $workDir) return [$realpath, $pluginName]; } - public function getName(): string + public function getType(): string { - return $this->pluginName; + return 'plugin'; } public function getPluginFilePathName(): string @@ -130,9 +110,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; } @@ -141,6 +123,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 @@ -184,6 +171,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( @@ -237,8 +229,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 28bc2cfa9..35015bf95 100644 --- a/src/Project/ThemeProject.php +++ b/src/Project/ThemeProject.php @@ -99,11 +99,6 @@ public static function parseDir(string $workDir) return [$name]; } - protected function symlinkProjectInContentDir(string $wpRootDir): void - { - FS::symlink($this->workDir, $wpRootDir . "/wp-content/themes/" . $this->basename); - } - /** * @throws WorkerException * @throws Throwable @@ -217,4 +212,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..a1f08949c --- /dev/null +++ b/tests/unit/lucatume/WPBrowser/Extension/SymlinkerTest.php @@ -0,0 +1,869 @@ + __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' => << [ + 'main.php' => << [ + 'style.css' => << '', + 'functions.php' => << [ + 'style.css' => << '', + 'functions.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::assertFileNotExists($wpRoot . '/wp-content/plugins/plugin-1/plugin-1.php'); + Assert::assertFileNotExists($wpRoot . '/wp-content/plugins/plugin-2/main.php'); + Assert::assertFileNotExists($wpRoot . '/wp-content/themes/theme-1/style.css'); + Assert::assertFileNotExists($wpRoot . '/wp-content/themes/theme-1/index.php'); + Assert::assertFileNotExists($wpRoot . '/wp-content/themes/theme-1/functions.php'); + Assert::assertFileNotExists($wpRoot . '/wp-content/themes/theme-2/style.css'); + Assert::assertFileNotExists($wpRoot . '/wp-content/themes/theme-2/index.php'); + Assert::assertFileNotExists($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::assertFileNotExists($wpRoot . '/wp-content/plugins/plugin-1/plugin-1.php'); + Assert::assertFileNotExists($wpRoot . '/wp-content/plugins/plugin-2/main.php'); + Assert::assertFileNotExists($wpRoot . '/wp-content/themes/theme-1/style.css'); + Assert::assertFileNotExists($wpRoot . '/wp-content/themes/theme-1/index.php'); + Assert::assertFileNotExists($wpRoot . '/wp-content/themes/theme-1/functions.php'); + Assert::assertFileNotExists($wpRoot . '/wp-content/themes/theme-2/style.css'); + Assert::assertFileNotExists($wpRoot . '/wp-content/themes/theme-2/index.php'); + Assert::assertFileNotExists($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' => << [ + 'main.php' => << [ + 'style.css' => << '', + 'functions.php' => << [ + 'style.css' => << '', + 'functions.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::assertFileNotExists($wpRoot . '/wp-content/plugins/plugin-1/plugin-1.php'); + Assert::assertFileNotExists($wpRoot . '/wp-content/plugins/plugin-2/main.php'); + Assert::assertFileNotExists($wpRoot . '/wp-content/themes/theme-1/style.css'); + Assert::assertFileNotExists($wpRoot . '/wp-content/themes/theme-1/index.php'); + Assert::assertFileNotExists($wpRoot . '/wp-content/themes/theme-1/functions.php'); + Assert::assertFileNotExists($wpRoot . '/wp-content/themes/theme-2/style.css'); + Assert::assertFileNotExists($wpRoot . '/wp-content/themes/theme-2/index.php'); + Assert::assertFileNotExists($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::assertFileNotExists($wpRoot . '/wp-content/plugins/plugin-1/plugin-1.php'); + Assert::assertFileNotExists($wpRoot . '/wp-content/plugins/plugin-2/main.php'); + Assert::assertFileNotExists($wpRoot . '/wp-content/themes/theme-1/style.css'); + Assert::assertFileNotExists($wpRoot . '/wp-content/themes/theme-1/index.php'); + Assert::assertFileNotExists($wpRoot . '/wp-content/themes/theme-1/functions.php'); + Assert::assertFileNotExists($wpRoot . '/wp-content/themes/theme-2/style.css'); + Assert::assertFileNotExists($wpRoot . '/wp-content/themes/theme-2/index.php'); + Assert::assertFileNotExists($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' => << [ + 'main.php' => << [ + 'style.css' => << '', + 'functions.php' => << [ + 'style.css' => << '', + 'functions.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::assertFileNotExists($wpRoot . '/wp-content/plugins/plugin-1/plugin-1.php'); + Assert::assertFileNotExists($wpRoot . '/wp-content/plugins/plugin-2/main.php'); + Assert::assertFileNotExists($wpRoot . '/wp-content/themes/theme-1/style.css'); + Assert::assertFileNotExists($wpRoot . '/wp-content/themes/theme-1/index.php'); + Assert::assertFileNotExists($wpRoot . '/wp-content/themes/theme-1/functions.php'); + Assert::assertFileNotExists($wpRoot . '/wp-content/themes/theme-2/style.css'); + Assert::assertFileNotExists($wpRoot . '/wp-content/themes/theme-2/index.php'); + Assert::assertFileNotExists($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' => << [ + 'main.php' => << [ + 'style.css' => << '', + 'functions.php' => << [ + 'style.css' => << '', + 'functions.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::assertFileNotExists($wpRoot . '/wp-content/plugins/plugin-1/plugin-1.php'); + Assert::assertFileNotExists($wpRoot . '/wp-content/plugins/plugin-2/main.php'); + Assert::assertFileNotExists($wpRoot . '/wp-content/themes/theme-1/style.css'); + Assert::assertFileNotExists($wpRoot . '/wp-content/themes/theme-1/index.php'); + Assert::assertFileNotExists($wpRoot . '/wp-content/themes/theme-1/functions.php'); + Assert::assertFileNotExists($wpRoot . '/wp-content/themes/theme-2/style.css'); + Assert::assertFileNotExists($wpRoot . '/wp-content/themes/theme-2/index.php'); + Assert::assertFileNotExists($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' => << [ + 'main.php' => << [ + 'style.css' => << '', + 'functions.php' => << [ + 'style.css' => << '', + 'functions.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' => << [ + 'main.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' => <<assertInIsolation(static function () use ($workingDir, $wpRoot) { + chdir($workingDir); + + Assert::assertSame($workingDir, getcwd()); + + $symlinker = new Symlinker([ + 'wpRootFolder' => $wpRoot, + 'plugins' => [ + '.', + ] + ], []); + + $workDirBasename = basename($workingDir); + + Assert::assertFileNotExists($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}")); + }); + } +}