diff --git a/docs/modules/WPCLI.md b/docs/modules/WPCLI.md index 2743eb098..30b4964a0 100644 --- a/docs/modules/WPCLI.md +++ b/docs/modules/WPCLI.md @@ -46,6 +46,7 @@ This module should be with [Cest][2] and [Cept][3] test cases. variable. * `packages-dir` - the directory to use to store the packages downloaded by the `wp package` command. Equivalent to setting the `WP_CLI_PACKAGES_DIR` environment variable. +* `bin` - the path to a custom WP-CLI binary. The following is an example of the module configuration to run WPCLI commands on the `/var/wordpress` directory: @@ -67,6 +68,16 @@ modules: throw: true ``` +The following configuration uses a custom WP-CLI binary: + +```yaml +modules: + enabled: + lucatume\WPBrowser\Module\WPCLI: + path: /var/wordpress + bin: /usr/local/bin/wp +``` + ## Methods The module provides the following methods: diff --git a/src/Module/WPCLI.php b/src/Module/WPCLI.php index e21b2ccfb..a8afe94f1 100644 --- a/src/Module/WPCLI.php +++ b/src/Module/WPCLI.php @@ -10,6 +10,7 @@ use Codeception\Exception\ModuleConfigException; use Codeception\Exception\ModuleException; use Codeception\Module; +use lucatume\WPBrowser\Exceptions\InvalidArgumentException; use lucatume\WPBrowser\Utils\Arr; use lucatume\WPBrowser\Utils\Filesystem; use lucatume\WPBrowser\WordPress\CliProcess; @@ -40,7 +41,7 @@ class WPCLI extends Module 'color' => true, 'no-color' => true, 'debug' => true, - 'quiet' => true + 'quiet' => true, ]; /** * @var array @@ -75,7 +76,8 @@ class WPCLI extends Module * cache-dir?: string, * config-path?: string, * custom-shell?: string, - * packages-dir?: string + * packages-dir?: string, + * bin?: string * } */ protected array $config = [ @@ -129,7 +131,8 @@ public function cli(string|array $command = ['core', 'version'], ?array $env = n * cache-dir?: string, * config-path?: string, * custom-shell?: string, - * packages-dir?: string + * packages-dir?: string, + * bin?: string * } $config */ $config = $this->config; @@ -141,7 +144,22 @@ public function cli(string|array $command = ['core', 'version'], ?array $env = n $command = $this->addStrictOptionsFromConfig($command); - $cliProcess = new CliProcess($command, $config['path'], $env, $input, $config['timeout']); + try { + $cliProcess = new CliProcess( + $command, + $config['path'], + $env, + $input, + $config['timeout'], + $config['bin'] ?? null + ); + } catch (\Exception $e) { + throw new ModuleConfigException( + __CLASS__, + $e->getMessage(), + $e + ); + } $this->debugSection('WPCLI command', $cliProcess->getCommandLine()); @@ -553,7 +571,7 @@ private function validatePath(): void /** * @return array{ - * WP_CLI_CACHE_DIR: string, + * WP_CLI_CACHE_DIR?: string, * WP_CLI_CONFIG_PATH?: string, * WP_CLI_CUSTOM_SHELL?: string, * WP_CLI_PACKAGES_DIR?: string, @@ -569,12 +587,16 @@ private function getDefaultEnv(): array * cache-dir?: non-empty-string, * config-path?: non-empty-string, * custom-shell?: non-empty-string, - * packages-dir?: non-empty-string + * packages-dir?: non-empty-string, + * bin?: string * } $config Validated config. */ $config = $this->config; - $cacheDir = $config['cache-dir'] ?? (Filesystem::cacheDir() . '/wp-cli'); - $env['WP_CLI_CACHE_DIR'] = Filesystem::mkdirp($cacheDir); + + if (empty($config['bin'])) { + $cacheDir = $config['cache-dir'] ?? (Filesystem::cacheDir() . '/wp-cli'); + $env['WP_CLI_CACHE_DIR'] = Filesystem::mkdirp($cacheDir); + } if (isset($config['config-path'])) { $env['WP_CLI_CONFIG_PATH'] = $config['config-path']; diff --git a/src/WordPress/CliProcess.php b/src/WordPress/CliProcess.php index 60aeefe4f..9ac7a936d 100644 --- a/src/WordPress/CliProcess.php +++ b/src/WordPress/CliProcess.php @@ -3,8 +3,10 @@ namespace lucatume\WPBrowser\WordPress; use lucatume\WPBrowser\Adapters\Symfony\Component\Process\Process; +use lucatume\WPBrowser\Exceptions\InvalidArgumentException; use lucatume\WPBrowser\Exceptions\RuntimeException; use lucatume\WPBrowser\Utils\Download; +use lucatume\WPBrowser\Utils\Filesystem; use lucatume\WPBrowser\Utils\Filesystem as FS; class CliProcess extends Process @@ -22,9 +24,30 @@ public function __construct( ?string $cwd = null, ?array $env = null, $input = null, - ?float $timeout = 60 + ?float $timeout = 60, + ?string $bin = null ) { - $wpCliPhar = self::getWpCliPharPath(); + if ($bin === null) { + $wpCliPhar = self::getWpCliPharPath(); + } else { + try { + $binAbsolutePath = Filesystem::resolvePath($bin); + } catch (\Exception $e) { + throw new InvalidArgumentException( + 'Failed to resolve custom binary path: does it exist?', + $e->getCode(), + $e + ); + } + + if ($binAbsolutePath === false || !is_executable($binAbsolutePath)) { + throw new InvalidArgumentException( + 'WPCLI bin not found or not executable: ' . $binAbsolutePath + ); + } + $wpCliPhar = $binAbsolutePath; + } + array_unshift($command, PHP_BINARY, $wpCliPhar); parent::__construct($command, $cwd, $env, $input, $timeout); } diff --git a/tests/_data/bins/not-executable b/tests/_data/bins/not-executable new file mode 100644 index 000000000..9bfabb530 --- /dev/null +++ b/tests/_data/bins/not-executable @@ -0,0 +1,4 @@ +#!/usr/bin/env bash + +echo "This binary was never modded to be executable" +exit 0 diff --git a/tests/_data/bins/wp-cli-custom-bin b/tests/_data/bins/wp-cli-custom-bin new file mode 100755 index 000000000..de9e73bc9 --- /dev/null +++ b/tests/_data/bins/wp-cli-custom-bin @@ -0,0 +1,4 @@ +#!/usr/bin/env php +homeBackup = $_SERVER['HOME']; + } + $this->wpCliCacheDir = Filesystem::cacheDir() . '/wp-cli'; + $this->wpCliCacheDirBackup = dirname($this->wpCliCacheDir) . '/wp-cli-cache-dir-backup'; + if (is_dir($this->wpCliCacheDirBackup)) { + Filesystem::rrmdir($this->wpCliCacheDirBackup); + } + if (is_dir($this->wpCliCacheDir)) { + rename($this->wpCliCacheDir, $this->wpCliCacheDirBackup); + } + $this->assertDirectoryDoesNotExist($this->wpCliCacheDir); + } + + public function tearDown(): void + { + parent::tearDown(); + if ($this->homeBackup !== null) { + $_SERVER['HOME'] = $this->homeBackup; + } + if (is_dir($this->wpCliCacheDir)) { + Filesystem::rrmdir($this->wpCliCacheDir); + } + if (is_dir($this->wpCliCacheDirBackup)) { + rename($this->wpCliCacheDirBackup, $this->wpCliCacheDir); + } + } + + public function test_configuration_allows_custom_binary(): void + { + $binary = codecept_data_dir('bins/wp-cli-custom-bin'); + $moduleContainer = new ModuleContainer(new Di(), []); + + $module = new WPCLI($moduleContainer, [ + 'path' => 'var/wordpress', + 'bin' => $binary, + ]); + $module->cli(['core', 'version']); + + $this->assertEquals( + 'Hello from wp-cli custom binary', + $module->grabLastShellOutput() + ); + $this->assertDirectoryDoesNotExist($this->wpCliCacheDir); + } + + public function test_configuration_supports_tilde_for_home_in_custom_binary(): void + { + $_SERVER['HOME'] = codecept_data_dir(); + $binary = '~/bins/wp-cli-custom-bin'; + $binaryPath = codecept_data_dir('bins/wp-cli-custom-bin'); + $moduleContainer = new ModuleContainer(new Di(), []); + // Sanity check. + $this->assertEquals(rtrim(codecept_data_dir(), '\\/'), Filesystem::homeDir()); + + $module = new WPCLI($moduleContainer, [ + 'path' => 'var/wordpress', + 'bin' => $binary, + ]); + $module->cli(['core', 'version']); + + $this->assertEquals( + 'Hello from wp-cli custom binary', + $module->grabLastShellOutput() + ); + $this->assertDirectoryDoesNotExist($this->wpCliCacheDir); + } + + public function test_throws_if_custom_binary_does_not_exist(): void + { + $binary = codecept_data_dir('bins/not-a-bin'); + $moduleContainer = new ModuleContainer(new Di(), []); + + $this->expectException(ModuleConfigException::class); + + $module = new WPCLI($moduleContainer, [ + 'path' => 'var/wordpress', + 'bin' => $binary, + ]); + $module->cli(['core', 'version']); + } + + public function test_throws_if_custom_binary_is_not_executable(): void + { + $binary = codecept_data_dir('bins/not-executable'); + $moduleContainer = new ModuleContainer(new Di(), []); + + $this->expectException(ModuleConfigException::class); + + $module = new WPCLI($moduleContainer, [ + 'path' => 'var/wordpress', + 'bin' => $binary, + ]); + $module->cli(['core', 'version']); + } +} diff --git a/tests/unit/lucatume/WPBrowser/WordPress/CliProcessTest.php b/tests/unit/lucatume/WPBrowser/WordPress/CliProcessTest.php new file mode 100644 index 000000000..17e9633b9 --- /dev/null +++ b/tests/unit/lucatume/WPBrowser/WordPress/CliProcessTest.php @@ -0,0 +1,75 @@ +homeBackup = $_SERVER['HOME']; + } + } + + public function tearDown(): void + { + parent::tearDown(); + if ($this->homeBackup !== null) { + $_SERVER['HOME'] = $this->homeBackup; + } + } + + public function test_construct_with_custom_binary(): void + { + $binary = codecept_data_dir('bins/wp-cli-custom-bin'); + + $cliProcess = new CliProcess(['core', 'version'], null, null, null, null, $binary); + + $this->assertEquals( + escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg($binary) . " 'core' 'version'", + $cliProcess->getCommandLine() + ); + } + + public function test_throws_if_custom_binary_does_not_exist(): void + { + $binary = codecept_data_dir('bins/not-a-bin'); + + $this->expectException(InvalidArgumentException::class); + + $cliProcess = new CliProcess(['core', 'version'], null, null, null, null, $binary); + } + + public function test_throws_if_custom_binary_is_not_executable(): void + { + $binary = codecept_data_dir('bins/not-executable'); + + $this->expectException(InvalidArgumentException::class); + + $cliProcess = new CliProcess(['core', 'version'], null, null, null, null, $binary); + } + + public function test_tilde_for_home_dir_is_supported_in_custom_binary_path(): void + { + $_SERVER['HOME'] = codecept_data_dir(); + $binary = '~/bins/wp-cli-custom-bin'; + $binaryAbsolutePath = codecept_data_dir('bins/wp-cli-custom-bin'); + // Sanity check. + $this->assertEquals(rtrim(codecept_data_dir(), '\\/'), Filesystem::homeDir()); + + $cliProcess = new CliProcess(['core', 'version'], null, null, null, null, $binary); + + $this->assertEquals( + escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg($binaryAbsolutePath) . " 'core' 'version'", + $cliProcess->getCommandLine() + ); + } +}