From da8db94a6ef7dcc58c616fc4c9459c5182085240 Mon Sep 17 00:00:00 2001 From: "Peter J. Herrel" Date: Fri, 11 Aug 2017 08:40:37 +0200 Subject: [PATCH] update package framework, require WP-CLI 1.3.0 closes #91, closes #92 --- README.md | 2 +- command.php | 4 +- composer.json | 5 +- composer.lock | 39 ++-- features/bootstrap/FeatureContext.php | 251 +++++++++++++++++++++++--- features/bootstrap/Process.php | 21 ++- features/bootstrap/ProcessRun.php | 29 +++ features/bootstrap/utils.php | 98 ++++++++-- features/steps/given.php | 45 ++++- features/steps/then.php | 11 +- src/Commands/Doctor.php | 8 +- 11 files changed, 433 insertions(+), 80 deletions(-) diff --git a/README.md b/README.md index c8b4cc0..349af5d 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Quick links: [Installation](#installation) | [Using](#using) | [Contributing](#c ## Installation -Installing this package requires WP-CLI v1.2.1 or greater. Update to the latest stable release with `wp cli update`. +Installing this package requires WP-CLI v1.3.0 or greater. Update to the latest stable release with `wp cli update`. Once you've done so, you can install this package with `wp package install git@github.com:diggy/polylang-cli.git`. ## Using diff --git a/command.php b/command.php index ec1f3d1..f776a2b 100644 --- a/command.php +++ b/command.php @@ -13,8 +13,8 @@ WP_CLI::error( sprintf( 'This WP-CLI package requires PHP version %s or higher.', '5.5' ) ); } - if ( version_compare( WP_CLI_VERSION, '1.2.1', '<' ) ) { - WP_CLI::error( sprintf( 'This WP-CLI package requires WP-CLI version %s or higher. Please visit %s', '1.2.1', 'https://wp-cli.org/#updating' ) ); + if ( version_compare( WP_CLI_VERSION, '1.3.0', '<' ) ) { + WP_CLI::error( sprintf( 'This WP-CLI package requires WP-CLI version %s or higher. Please visit %s', '1.3.0', 'https://wp-cli.org/#updating' ) ); } # api, cli diff --git a/composer.json b/composer.json index 28b5d5e..59e5baf 100644 --- a/composer.json +++ b/composer.json @@ -13,11 +13,12 @@ } ], "minimum-stability": "dev", + "prefer-stable": true, "autoload": { "files": ["command.php"] }, "require": { - "wp-cli/wp-cli": "~1.2.1" + "wp-cli/wp-cli": "~1.3.0" }, "require-dev": { "behat/behat": "~2.5" @@ -86,7 +87,7 @@ "Development" ], "installation": { - "body": "Installing this package requires WP-CLI v1.2.1 or greater. Update to the latest stable release with `wp cli update`. \nOnce you've done so, you can install this package with `wp package install git@github.com:diggy/polylang-cli.git`." + "body": "Installing this package requires WP-CLI v1.3.0 or greater. Update to the latest stable release with `wp cli update`. \nOnce you've done so, you can install this package with `wp package install git@github.com:diggy/polylang-cli.git`." }, "development": { "body": "### Behat Tests\nTo run the Behat tests for polylang-cli, `cd` into the package directory and run `$ ./vendor/bin/behat --expand` from the command line. To run a specific group of tests use the `tags` parameter; e.g.: `$ ./vendor/bin/behat --expand --tags @pll-lang`" diff --git a/composer.lock b/composer.lock index 474770a..0232bea 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "content-hash": "4e48f4cec95314856fba510a46a808e4", + "content-hash": "60ec914270e12d18cce10db8e352d9a2", "packages": [ { "name": "composer/ca-bundle", @@ -71,12 +71,12 @@ "source": { "type": "git", "url": "https://github.com/composer/composer.git", - "reference": "5a16b25879b5828e9ba00c516e293e80038d91c7" + "reference": "82c27a68bc5cb76f3d00b82c27496e3cdbb6d4ff" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/composer/zipball/5a16b25879b5828e9ba00c516e293e80038d91c7", - "reference": "5a16b25879b5828e9ba00c516e293e80038d91c7", + "url": "https://api.github.com/repos/composer/composer/zipball/82c27a68bc5cb76f3d00b82c27496e3cdbb6d4ff", + "reference": "82c27a68bc5cb76f3d00b82c27496e3cdbb6d4ff", "shasum": "" }, "require": { @@ -140,7 +140,7 @@ "dependency", "package" ], - "time": "2017-08-08 16:40:52" + "time": "2017-08-09 14:23:46" }, { "name": "composer/semver", @@ -880,12 +880,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "51a04a05fdea674ad591ee448d1c860e6191e6e9" + "reference": "fbeea992f0d30e3c400694c85f60c9bfd10454a3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/51a04a05fdea674ad591ee448d1c860e6191e6e9", - "reference": "51a04a05fdea674ad591ee448d1c860e6191e6e9", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/fbeea992f0d30e3c400694c85f60c9bfd10454a3", + "reference": "fbeea992f0d30e3c400694c85f60c9bfd10454a3", "shasum": "" }, "require": { @@ -935,7 +935,7 @@ ], "description": "Symfony DependencyInjection Component", "homepage": "https://symfony.com", - "time": "2017-08-08 21:33:56" + "time": "2017-08-10 14:42:21" }, { "name": "symfony/event-dispatcher", @@ -1415,12 +1415,12 @@ "source": { "type": "git", "url": "https://github.com/wp-cli/checksum-command.git", - "reference": "64536b68de9348b4f5b4c0e75d8c0e5b020a16c0" + "reference": "88ccde2e82de5c2232842a9fccc2e6ade232dec6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/wp-cli/checksum-command/zipball/64536b68de9348b4f5b4c0e75d8c0e5b020a16c0", - "reference": "64536b68de9348b4f5b4c0e75d8c0e5b020a16c0", + "url": "https://api.github.com/repos/wp-cli/checksum-command/zipball/88ccde2e82de5c2232842a9fccc2e6ade232dec6", + "reference": "88ccde2e82de5c2232842a9fccc2e6ade232dec6", "shasum": "" }, "require-dev": { @@ -1458,7 +1458,7 @@ ], "description": "Verify WordPress core checksums.", "homepage": "https://github.com/wp-cli/checksum-command", - "time": "2017-08-04 12:20:23" + "time": "2017-08-09 23:34:29" }, { "name": "wp-cli/config-command", @@ -2717,16 +2717,16 @@ }, { "name": "wp-cli/wp-cli", - "version": "v1.2.1", + "version": "v1.3.0", "source": { "type": "git", "url": "https://github.com/wp-cli/wp-cli.git", - "reference": "9a9b6a63e08e7827669677faa97312972c0f1da2" + "reference": "4ab0d99da0ad5e6ca39453ff5c82d4f06aecb086" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/wp-cli/wp-cli/zipball/9a9b6a63e08e7827669677faa97312972c0f1da2", - "reference": "9a9b6a63e08e7827669677faa97312972c0f1da2", + "url": "https://api.github.com/repos/wp-cli/wp-cli/zipball/4ab0d99da0ad5e6ca39453ff5c82d4f06aecb086", + "reference": "4ab0d99da0ad5e6ca39453ff5c82d4f06aecb086", "shasum": "" }, "require": { @@ -2746,7 +2746,7 @@ "symfony/process": "^2.1|^3.0", "symfony/translation": "^2.7|^3.0", "symfony/yaml": "^2.7|^3.0", - "wp-cli/autoload-splitter": "^0.1", + "wp-cli/autoload-splitter": "^0.1.5", "wp-cli/cache-command": "^1.0", "wp-cli/checksum-command": "^1.0", "wp-cli/config-command": "^1.0", @@ -2775,7 +2775,6 @@ "require-dev": { "behat/behat": "2.5.*", "phpunit/phpunit": "3.7.*", - "pyrech/composer-changelogs": "dev-php53 as 1.5.1", "roave/security-advisories": "dev-master" }, "suggest": { @@ -2812,7 +2811,7 @@ "cli", "wordpress" ], - "time": "2017-06-06T12:14:53+00:00" + "time": "2017-08-08T14:28:58+00:00" } ], "packages-dev": [ diff --git a/features/bootstrap/FeatureContext.php b/features/bootstrap/FeatureContext.php index e5b8176..44f8767 100644 --- a/features/bootstrap/FeatureContext.php +++ b/features/bootstrap/FeatureContext.php @@ -44,8 +44,31 @@ */ class FeatureContext extends BehatContext implements ClosuredContextInterface { - private static $cache_dir, $suite_cache_dir; + /** + * The current working directory for scenarios that have a "Given a WP install" or "Given an empty directory" step. Variable RUN_DIR. Lives until the end of the scenario. + */ + private static $run_dir; + + /** + * Where WordPress core is downloaded to for caching, and which is copied to RUN_DIR during a "Given a WP install" step. Lives until manually deleted. + */ + private static $cache_dir; + + /** + * The directory that the WP-CLI cache (WP_CLI_CACHE_DIR, normally "$HOME/.wp-cli/cache") is set to on a "Given an empty cache" step. + * Variable SUITE_CACHE_DIR. Lives until the end of the scenario (or until another "Given an empty cache" step within the scenario). + */ + private static $suite_cache_dir; + + /** + * Where the current WP-CLI source repository is copied to for Composer-based tests with a "Given a dependency on current wp-cli" step. + * Variable COMPOSER_LOCAL_REPOSITORY. Lives until the end of the suite. + */ + private static $composer_local_repository; + /** + * The test database settings. All but `dbname` can be set via environment variables. The database is dropped at the start of each scenario and created on a "Given a WP install" step. + */ private static $db_settings = array( 'dbname' => 'wp_cli_test', 'dbuser' => 'wp_cli_test', @@ -53,13 +76,24 @@ class FeatureContext extends BehatContext implements ClosuredContextInterface { 'dbhost' => '127.0.0.1', ); + /** + * Array of background process ids started by the current scenario. Used to terminate them at the end of the scenario. + */ private $running_procs = array(); + /** + * Array of variables available as {VARIABLE_NAME}. Some are always set: CORE_CONFIG_SETTINGS, SRC_DIR, CACHE_DIR, WP_VERSION-version-latest. Some are step-dependent: + * RUN_DIR, SUITE_CACHE_DIR, COMPOSER_LOCAL_REPOSITORY, PHAR_PATH. Scenarios can define their own variables using "Given save" steps. Variables are reset for each scenario. + */ public $variables = array(); + /** + * The current feature file and scenario line number as '.'. Used in RUN_DIR and SUITE_CACHE_DIR directory names. Set at the start of each scenario. + */ + private static $temp_dir_infix; + /** * Get the environment variables required for launched `wp` processes - * @beforeSuite */ private static function get_process_env_variables() { // Ensure we're using the expected `wp` binary @@ -76,13 +110,21 @@ private static function get_process_env_variables() { if ( $term = getenv( 'TERM' ) ) { $env['TERM'] = $term; } + if ( $php_args = getenv( 'WP_CLI_PHP_ARGS' ) ) { + $env['WP_CLI_PHP_ARGS'] = $php_args; + } + if ( $travis_build_dir = getenv( 'TRAVIS_BUILD_DIR' ) ) { + $env['TRAVIS_BUILD_DIR'] = $travis_build_dir; + } return $env; } - // We cache the results of `wp core download` to improve test performance - // Ideally, we'd cache at the HTTP layer for more reliable tests + /** + * We cache the results of `wp core download` to improve test performance. + * Ideally, we'd cache at the HTTP layer for more reliable tests. + */ private static function cache_wp_files() { - self::$cache_dir = sys_get_temp_dir() . '/wp-cli-test core-download-cache'; + self::$cache_dir = sys_get_temp_dir() . '/wp-cli-test-core-download-cache'; if ( is_readable( self::$cache_dir . '/wp-config-sample.php' ) ) return; @@ -112,8 +154,9 @@ public static function prepare( SuiteEvent $event ) { * @AfterSuite */ public static function afterSuite( SuiteEvent $event ) { - if ( self::$suite_cache_dir ) { - Process::create( Utils\esc_cmd( 'rm -r %s', self::$suite_cache_dir ), null, self::get_process_env_variables() )->run(); + if ( self::$composer_local_repository ) { + self::remove_dir( self::$composer_local_repository ); + self::$composer_local_repository = null; } } @@ -122,24 +165,39 @@ public static function afterSuite( SuiteEvent $event ) { */ public function beforeScenario( $event ) { $this->variables['SRC_DIR'] = realpath( __DIR__ . '/../..' ); + + // Used in the names of the RUN_DIR and SUITE_CACHE_DIR directories. + self::$temp_dir_infix = null; + if ( $file = self::get_event_file( $event, $line ) ) { + self::$temp_dir_infix = basename( $file ) . '.' . $line; + } } /** * @AfterScenario */ public function afterScenario( $event ) { - if ( isset( $this->variables['RUN_DIR'] ) ) { + + if ( self::$run_dir ) { // remove altered WP install, unless there's an error - if ( $event->getResult() < 4 && 0 === strpos( $this->variables['RUN_DIR'], sys_get_temp_dir() ) ) { - $this->proc( Utils\esc_cmd( 'rm -rf %s', $this->variables['RUN_DIR'] ) )->run(); + if ( $event->getResult() < 4 ) { + self::remove_dir( self::$run_dir ); } + self::$run_dir = null; } - // Remove WP-CLI package directory + // Remove WP-CLI package directory if any. Set to `wp package path` by package-command and scaffold-package-command features, and by cli-info.feature. if ( isset( $this->variables['PACKAGE_PATH'] ) ) { - $this->proc( Utils\esc_cmd( 'rm -rf %s', $this->variables['PACKAGE_PATH'] ) )->run(); + self::remove_dir( $this->variables['PACKAGE_PATH'] ); } + // Remove SUITE_CACHE_DIR if any. + if ( self::$suite_cache_dir ) { + self::remove_dir( self::$suite_cache_dir ); + self::$suite_cache_dir = null; + } + + // Remove any background processes. foreach ( $this->running_procs as $proc ) { $status = proc_get_status( $proc ); self::terminate_proc( $status['pid'] ); @@ -173,22 +231,37 @@ private static function terminate_proc( $master_pid ) { } } + /** + * Create a temporary WP_CLI_CACHE_DIR. Exposed as SUITE_CACHE_DIR in "Given an empty cache" step. + */ public static function create_cache_dir() { - self::$suite_cache_dir = sys_get_temp_dir() . '/' . uniqid( "wp-cli-test-suite-cache-", TRUE ); + if ( self::$suite_cache_dir ) { + self::remove_dir( self::$suite_cache_dir ); + } + self::$suite_cache_dir = sys_get_temp_dir() . '/' . uniqid( 'wp-cli-test-suite-cache-' . self::$temp_dir_infix . '-', TRUE ); mkdir( self::$suite_cache_dir ); return self::$suite_cache_dir; } /** * Initializes context. - * Every scenario gets it's own context object. + * Every scenario gets its own context object. * * @param array $parameters context parameters (set them up through behat.yml) */ public function __construct( array $parameters ) { + if ( getenv( 'WP_CLI_TEST_DBUSER' ) ) { + self::$db_settings['dbuser'] = getenv( 'WP_CLI_TEST_DBUSER' ); + } + + if ( false !== getenv( 'WP_CLI_TEST_DBPASS' ) ) { + self::$db_settings['dbpass'] = getenv( 'WP_CLI_TEST_DBPASS' ); + } + if ( getenv( 'WP_CLI_TEST_DBHOST' ) ) { self::$db_settings['dbhost'] = getenv( 'WP_CLI_TEST_DBHOST' ); } + $this->drop_db(); $this->set_cache_dir(); $this->variables['CORE_CONFIG_SETTINGS'] = Utils\assoc_args_to_str( self::$db_settings ); @@ -202,6 +275,9 @@ public function getHookDefinitionResources() { return array(); } + /** + * Replace {VARIABLE_NAME}. Note that variable names can only contain uppercase letters and underscores (no numbers). + */ public function replace_variables( $str ) { $ret = preg_replace_callback( '/\{([A-Z_]+)\}/', array( $this, '_replace_var' ), $str ); if ( false !== strpos( $str, '{WP_VERSION-' ) ) { @@ -210,6 +286,9 @@ public function replace_variables( $str ) { return $ret; } + /** + * Replace variables callback. + */ private function _replace_var( $matches ) { $cmd = $matches[0]; @@ -220,7 +299,9 @@ private function _replace_var( $matches ) { return $cmd; } - // Substitute "{WP_VERSION-version-latest}" variables. + /** + * Substitute "{WP_VERSION-version-latest}" variables. + */ private function _replace_wp_versions( $str ) { static $wp_versions = null; if ( null === $wp_versions ) { @@ -249,9 +330,27 @@ private function _replace_wp_versions( $str ) { return strtr( $str, $wp_versions ); } + /** + * Get the file and line number for the current behat event. + */ + private static function get_event_file( $event, &$line ) { + if ( method_exists( $event, 'getScenario' ) ) { + $scenario_feature = $event->getScenario(); + } elseif ( method_exists( $event, 'getFeature' ) ) { + $scenario_feature = $event->getFeature(); + } else { + return null; + } + $line = $scenario_feature->getLine(); + return $scenario_feature->getFile(); + } + + /** + * Create the RUN_DIR directory, unless already set for this scenario. + */ public function create_run_dir() { if ( !isset( $this->variables['RUN_DIR'] ) ) { - $this->variables['RUN_DIR'] = sys_get_temp_dir() . '/' . uniqid( "wp-cli-test-run-", TRUE ); + self::$run_dir = $this->variables['RUN_DIR'] = sys_get_temp_dir() . '/' . uniqid( 'wp-cli-test-run-' . self::$temp_dir_infix . '-', TRUE ); mkdir( $this->variables['RUN_DIR'] ); } } @@ -293,21 +392,21 @@ public function download_phar( $version = 'same' ) { . uniqid( 'wp-cli-download-', true ) . '.phar'; - Process::create( \WP_CLI\Utils\esc_cmd( - 'curl -sSL %s > %s', + Process::create( Utils\esc_cmd( + 'curl -sSfL %1$s > %2$s && chmod +x %2$s', $download_url, $this->variables['PHAR_PATH'] ) )->run_check(); - - Process::create( \WP_CLI\Utils\esc_cmd( - 'chmod +x %s', - $this->variables['PHAR_PATH'] - ) )->run_check(); } + /** + * CACHE_DIR is a cache for downloaded test data such as images. Lives until manually deleted. + */ private function set_cache_dir() { $path = sys_get_temp_dir() . '/wp-cli-test-cache'; - $this->proc( Utils\esc_cmd( 'mkdir -p %s', $path ) )->run_check(); + if ( ! file_exists( $path ) ) { + mkdir( $path ); + } $this->variables['CACHE_DIR'] = $path; } @@ -375,6 +474,20 @@ public function move_files( $src, $dest ) { rename( $this->variables['RUN_DIR'] . "/$src", $this->variables['RUN_DIR'] . "/$dest" ); } + /** + * Remove a directory (recursive). + */ + public static function remove_dir( $dir ) { + Process::create( Utils\esc_cmd( 'rm -rf %s', $dir ) )->run_check(); + } + + /** + * Copy a directory (recursive). Destination directory must exist. + */ + public static function copy_dir( $src_dir, $dest_dir ) { + Process::create( Utils\esc_cmd( "cp -r %s/* %s", $src_dir, $dest_dir ) )->run_check(); + } + public function add_line_to_wp_config( &$wp_config_code, $line ) { $token = "/* That's all, stop editing!"; @@ -388,20 +501,25 @@ public function download_wp( $subdir = '' ) { mkdir( $dest_dir ); } - $this->proc( Utils\esc_cmd( "cp -r %s/* %s", self::$cache_dir, $dest_dir ) )->run_check(); + self::copy_dir( self::$cache_dir, $dest_dir ); // disable emailing mkdir( $dest_dir . '/wp-content/mu-plugins' ); copy( __DIR__ . '/../extra/no-mail.php', $dest_dir . '/wp-content/mu-plugins/no-mail.php' ); - copy( __DIR__ . '/../extra/init.php', $dest_dir . '/wp-content/mu-plugins/init.php' ); } - public function create_config( $subdir = '' ) { + public function create_config( $subdir = '', $extra_php = false ) { $params = self::$db_settings; + // Replaces all characters that are not alphanumeric or an underscore into an underscore. $params['dbprefix'] = $subdir ? preg_replace( '#[^a-zA-Z\_0-9]#', '_', $subdir ) : 'wp_'; $params['skip-salts'] = true; + + if( false !== $extra_php ) { + $params['extra-php'] = $extra_php; + } + $this->proc( 'wp core config', $params, $subdir )->run_check(); } @@ -423,4 +541,83 @@ public function install_wp( $subdir = '' ) { $this->proc( 'wp core install', $install_args, $subdir )->run_check(); } + + public function install_wp_with_composer( $vendor_directory = 'vendor' ) { + $this->create_run_dir(); + $this->create_db(); + + $yml_path = $this->variables['RUN_DIR'] . "/wp-cli.yml"; + file_put_contents( $yml_path, 'path: wordpress' ); + + $this->composer_command( 'init --name="wp-cli/composer-test" --type="project" --no-interaction' ); + $this->composer_command( 'config vendor-dir ' . $vendor_directory ); + $this->composer_command( 'require johnpbloch/wordpress --optimize-autoloader --no-interaction' ); + + $config_extra_php = "require_once dirname(__DIR__) . '/" . $vendor_directory . "/autoload.php';"; + $this->create_config( 'wordpress', $config_extra_php ); + + $install_args = array( + 'url' => 'http://localhost:8080', + 'title' => 'WP CLI Site with both WordPress and wp-cli as Composer dependencies', + 'admin_user' => 'admin', + 'admin_email' => 'admin@example.com', + 'admin_password' => 'password1' + ); + + $this->proc( 'wp core install', $install_args )->run_check(); + } + + public function composer_add_wp_cli_local_repository() { + if ( ! self::$composer_local_repository ) { + self::$composer_local_repository = sys_get_temp_dir() . '/' . uniqid( "wp-cli-composer-local-", TRUE ); + mkdir( self::$composer_local_repository ); + + $env = self::get_process_env_variables(); + $src = isset( $env['TRAVIS_BUILD_DIR'] ) ? $env['TRAVIS_BUILD_DIR'] : realpath( __DIR__ . '/../../' ); + + self::copy_dir( $src, self::$composer_local_repository . '/' ); + self::remove_dir( self::$composer_local_repository . '/.git' ); + self::remove_dir( self::$composer_local_repository . '/vendor' ); + } + $dest = self::$composer_local_repository . '/'; + $this->composer_command( "config repositories.wp-cli '{\"type\": \"path\", \"url\": \"$dest\", \"options\": {\"symlink\": false}}'" ); + $this->variables['COMPOSER_LOCAL_REPOSITORY'] = self::$composer_local_repository; + } + + public function composer_require_current_wp_cli() { + $this->composer_add_wp_cli_local_repository(); + $this->composer_command( 'require wp-cli/wp-cli:dev-master --optimize-autoloader --no-interaction' ); + } + + public function get_php_binary() { + if ( getenv( 'WP_CLI_PHP_USED' ) ) + return getenv( 'WP_CLI_PHP_USED' ); + + if ( getenv( 'WP_CLI_PHP' ) ) + return getenv( 'WP_CLI_PHP' ); + + if ( defined( 'PHP_BINARY' ) ) + return PHP_BINARY; + + return 'php'; + } + + public function start_php_server() { + $cmd = Utils\esc_cmd( '%s -S %s -t %s -c %s %s', + $this->get_php_binary(), + 'localhost:8080', + $this->variables['RUN_DIR'] . '/wordpress/', + get_cfg_var( 'cfg_file_path' ), + $this->variables['RUN_DIR'] . '/vendor/wp-cli/server-command/router.php' + ); + $this->background_proc( $cmd ); + } + + private function composer_command($cmd) { + if ( !isset( $this->variables['COMPOSER_PATH'] ) ) { + $this->variables['COMPOSER_PATH'] = exec('which composer'); + } + $this->proc( $this->variables['COMPOSER_PATH'] . ' ' . $cmd )->run_check(); + } + } diff --git a/features/bootstrap/Process.php b/features/bootstrap/Process.php index 74939c5..858e194 100644 --- a/features/bootstrap/Process.php +++ b/features/bootstrap/Process.php @@ -6,11 +6,27 @@ * Run a system process, and learn what happened. */ class Process { + /** + * @var string The full command to execute by the system. + */ + private $command; + + /** + * @var string|null The path of the working directory for the process or NULL if not specified (defaults to current working directory). + */ + private $cwd; + + /** + * @var array Environment variables to set when running the command. + */ + private $env; /** * @param string $command Command to execute. * @param string $cwd Directory to execute the command in. * @param array $env Environment variables to set when running the command. + * + * @return Process */ public static function create( $command, $cwd = null, $env = array() ) { $proc = new self; @@ -22,8 +38,6 @@ public static function create( $command, $cwd = null, $env = array() ) { return $proc; } - private $command, $cwd, $env; - private function __construct() {} /** @@ -54,7 +68,7 @@ public function run() { 'return_code' => proc_close( $proc ), 'command' => $this->command, 'cwd' => $cwd, - 'env' => $this->env + 'env' => $this->env, ) ); } @@ -66,6 +80,7 @@ public function run() { public function run_check() { $r = $this->run(); + // $r->STDERR is incorrect, but kept incorrect for backwards-compat if ( $r->return_code || !empty( $r->STDERR ) ) { throw new \RuntimeException( $r ); } diff --git a/features/bootstrap/ProcessRun.php b/features/bootstrap/ProcessRun.php index 4611cfb..aedc5f6 100644 --- a/features/bootstrap/ProcessRun.php +++ b/features/bootstrap/ProcessRun.php @@ -6,6 +6,35 @@ * Results of an executed command. */ class ProcessRun { + /** + * @var string The full command executed by the system. + */ + public $command; + + /** + * @var string Captured output from the process' STDOUT. + */ + public $stdout; + + /** + * @var string Captured output from the process' STDERR. + */ + public $stderr; + + /** + * @var string|null The path of the working directory for the process or NULL if not specified (defaults to current working directory). + */ + public $cwd; + + /** + * @var array Environment variables set for this process. + */ + public $env; + + /** + * @var int Exit code of the process. + */ + public $return_code; /** * @var array $props Properties of executed command. diff --git a/features/bootstrap/utils.php b/features/bootstrap/utils.php index e84b08f..1f37560 100644 --- a/features/bootstrap/utils.php +++ b/features/bootstrap/utils.php @@ -341,6 +341,8 @@ function pick_fields( $item, $fields ) { */ function launch_editor_for_input( $input, $filename = 'WP-CLI' ) { + check_proc_available( 'launch_editor_for_input' ); + $tmpdir = get_temp_dir(); do { @@ -415,6 +417,8 @@ function mysql_host_to_cli_args( $raw_host ) { } function run_mysql_command( $cmd, $assoc_args, $descriptors = null ) { + check_proc_available( 'run_mysql_command' ); + if ( !$descriptors ) $descriptors = array( STDIN, STDOUT, STDERR ); @@ -453,7 +457,7 @@ function mustache_render( $template_name, $data = array() ) { $template = file_get_contents( $template_name ); $m = new \Mustache_Engine( array( - 'escape' => function ( $val ) { return $val; } + 'escape' => function ( $val ) { return $val; }, ) ); return $m->render( $template, $data ); @@ -522,7 +526,7 @@ function is_windows() { function replace_path_consts( $source, $path ) { $replacements = array( '__FILE__' => "'$path'", - '__DIR__' => "'" . dirname( $path ) . "'" + '__DIR__' => "'" . dirname( $path ) . "'", ); $old = array_keys( $replacements ); @@ -568,7 +572,7 @@ function http_request( $method, $url, $data = null, $headers = array(), $options } } if ( empty( $options['verify'] ) ){ - WP_CLI::error_log( "Cannot find SSL certificate." ); + WP_CLI::error( "Cannot find SSL certificate." ); } } @@ -694,6 +698,37 @@ function get_flag_value( $assoc_args, $flag, $default = null ) { return isset( $assoc_args[ $flag ] ) ? $assoc_args[ $flag ] : $default; } +/** + * Get the home directory. + * + * @access public + * @category System + * + * @return string + */ +function get_home_dir() { + $home = getenv( 'HOME' ); + if ( ! $home ) { + // In Windows $HOME may not be defined + $home = getenv( 'HOMEDRIVE' ) . getenv( 'HOMEPATH' ); + } + + return rtrim( $home, '/\\' ); +} + +/** + * Appends a trailing slash. + * + * @access public + * @category System + * + * @param string $string What to add the trailing slash to. + * @return string String with trailing slash added. + */ +function trailingslashit( $string ) { + return rtrim( $string, '/\\' ) . '/'; +} + /** * Get the system's temp directory. Warns user if it isn't writable. * @@ -705,17 +740,15 @@ function get_flag_value( $assoc_args, $flag, $default = null ) { function get_temp_dir() { static $temp = ''; - $trailingslashit = function( $path ) { - return rtrim( $path ) . '/'; - }; - - if ( $temp ) - return $trailingslashit( $temp ); + if ( $temp ) { + return $temp; + } - if ( function_exists( 'sys_get_temp_dir' ) ) { - $temp = sys_get_temp_dir(); - } else if ( ini_get( 'upload_tmp_dir' ) ) { - $temp = ini_get( 'upload_tmp_dir' ); + // `sys_get_temp_dir()` introduced PHP 5.2.1. + if ( $try = sys_get_temp_dir() ) { + $temp = trailingslashit( $try ); + } elseif ( $try = ini_get( 'upload_tmp_dir' ) ) { + $temp = trailingslashit( $try ); } else { $temp = '/tmp/'; } @@ -724,7 +757,7 @@ function get_temp_dir() { \WP_CLI::warning( "Temp directory isn't writable: {$temp}" ); } - return $trailingslashit( $temp ); + return $temp; } /** @@ -741,18 +774,24 @@ function get_temp_dir() { * @return mixed */ function parse_ssh_url( $url, $component = -1 ) { - preg_match( '#^([^:/~]+)(:([\d]+))?((/|~)(.+))?$#', $url, $matches ); + preg_match( '#^((docker|docker\-compose|ssh):)?(([^@:]+)@)?([^:/~]+)(:([\d]*))?((/|~)(.+))?$#', $url, $matches ); $bits = array(); foreach( array( - 1 => 'host', - 3 => 'port', - 4 => 'path', + 2 => 'scheme', + 4 => 'user', + 5 => 'host', + 7 => 'port', + 8 => 'path', ) as $i => $key ) { if ( ! empty( $matches[ $i ] ) ) { $bits[ $key ] = $matches[ $i ]; } } switch ( $component ) { + case PHP_URL_SCHEME: + return isset( $bits['scheme'] ) ? $bits['scheme'] : null; + case PHP_URL_USER: + return isset( $bits['user'] ) ? $bits['user'] : null; case PHP_URL_HOST: return isset( $bits['host'] ) ? $bits['host'] : null; case PHP_URL_PATH: @@ -1005,3 +1044,26 @@ function force_env_on_nix_systems( $command ) { } return $command; } + +/** + * Check that `proc_open()` and `proc_close()` haven't been disabled. + * + * @param string $context Optional. If set will appear in error message. Default null. + * @param bool $return Optional. If set will return false rather than error out. Default false. + * + * @return bool + */ +function check_proc_available( $context = null, $return = false ) { + if ( ! function_exists( 'proc_open' ) || ! function_exists( 'proc_close' ) ) { + if ( $return ) { + return false; + } + $msg = 'The PHP functions `proc_open()` and/or `proc_close()` are disabled. Please check your PHP ini directive `disable_functions` or suhosin settings.'; + if ( $context ) { + WP_CLI::error( sprintf( "Cannot do '%s': %s", $context, $msg ) ); + } else { + WP_CLI::error( $msg ); + } + } + return true; +} diff --git a/features/steps/given.php b/features/steps/given.php index c785df1..e568bff 100644 --- a/features/steps/given.php +++ b/features/steps/given.php @@ -10,6 +10,22 @@ function ( $world ) { } ); +$steps->Given( '/^an? (empty|non-existent) ([^\s]+) directory$/', + function ( $world, $empty_or_nonexistent, $dir ) { + $dir = $world->replace_variables( $dir ); + if ( ! WP_CLI\Utils\is_path_absolute( $dir ) ) { + $dir = $world->variables['RUN_DIR'] . "/$dir"; + } + if ( 0 !== strpos( $dir, sys_get_temp_dir() ) ) { + throw new RuntimeException( sprintf( "Attempted to delete directory '%s' that is not in the temp directory '%s'. " . __FILE__ . ':' . __LINE__, $dir, sys_get_temp_dir() ) ); + } + $world->remove_dir( $dir ); + if ( 'empty' === $empty_or_nonexistent ) { + mkdir( $dir, 0777, true /*recursive*/ ); + } + } +); + $steps->Given( '/^an empty cache/', function ( $world ) { $world->variables['SUITE_CACHE_DIR'] = FeatureContext::create_cache_dir(); @@ -20,7 +36,10 @@ function ( $world ) { function ( $world, $path, PyStringNode $content ) { $content = (string) $content . "\n"; $full_path = $world->variables['RUN_DIR'] . "/$path"; - Process::create( \WP_CLI\utils\esc_cmd( 'mkdir -p %s', dirname( $full_path ) ) )->run_check(); + $dir = dirname( $full_path ); + if ( ! file_exists( $dir ) ) { + mkdir( $dir, 0777, true /*recursive*/ ); + } file_put_contents( $full_path, $content ); } ); @@ -62,6 +81,18 @@ function ( $world, $subdir ) { } ); +$steps->Given( '/^a WP install with Composer$/', + function ( $world ) { + $world->install_wp_with_composer(); + } +); + +$steps->Given( "/^a WP install with Composer and a custom vendor directory '([^\s]+)'$/", + function ( $world, $vendor_directory ) { + $world->install_wp_with_composer( $vendor_directory ); + } +); + $steps->Given( '/^a WP multisite (subdirectory|subdomain)?\s?install$/', function ( $world, $type = 'subdirectory' ) { $world->install_wp(); @@ -168,3 +199,15 @@ function($world) { file_put_contents( $wp_config_path, $wp_config_code ); } ); + +$steps->Given( '/^a dependency on current wp-cli$/', + function ( $world ) { + $world->composer_require_current_wp_cli(); + } +); + +$steps->Given( '/^a PHP built-in web server$/', + function ( $world ) { + $world->start_php_server(); + } +); diff --git a/features/steps/then.php b/features/steps/then.php index b26448d..4455c5b 100644 --- a/features/steps/then.php +++ b/features/steps/then.php @@ -172,12 +172,12 @@ function ( $world, $path, $type, $action, $expected = null ) { switch ( $action ) { case 'exist': if ( ! $test( $path ) ) { - throw new Exception( $world->result ); + throw new Exception( "$path doesn't exist." ); } break; case 'not exist': if ( $test( $path ) ) { - throw new Exception( $world->result ); + throw new Exception( "$path exists." ); } break; default: @@ -209,3 +209,10 @@ function ( $world, $path, $type, $action, $expected = null ) { throw new Exception( 'Invalid expectation' ); } }); + +$steps->Then( '/^the HTTP status code should be (\d+)$/', + function ( $world, $return_code ) { + $response = \Requests::request( 'http://localhost:8080' ); + assertEquals( $return_code, $response->status_code ); + } +); diff --git a/src/Commands/Doctor.php b/src/Commands/Doctor.php index d5037e8..9d3311f 100644 --- a/src/Commands/Doctor.php +++ b/src/Commands/Doctor.php @@ -172,7 +172,7 @@ public function language( $args, $assoc_args ) { foreach( $locales_orphan as $locale ) { $this->cli->runcommand( - "core language uninstall $locale", + "language core uninstall $locale", array( 'return' => false, 'launch' => true, 'exit_error' => false ) ); } @@ -186,7 +186,7 @@ public function language( $args, $assoc_args ) { foreach( $locales_missing as $locale ) { $this->cli->runcommand( - "core language install $locale", + "language core install $locale", array( 'return' => false, 'launch' => true, 'exit_error' => false ) ); } @@ -197,7 +197,7 @@ public function language( $args, $assoc_args ) { ob_start(); - $this->cli->command( array( 'core', 'language', 'list' ), array( 'field' => 'language', 'update' => 'available', 'format' => 'json' ) ); + $this->cli->command( array( 'language', 'core', 'list' ), array( 'field' => 'language', 'update' => 'available', 'format' => 'json' ) ); $locales_outdated = ob_get_clean(); @@ -208,7 +208,7 @@ public function language( $args, $assoc_args ) { $this->cli->confirm( sprintf( "%d core language packs have updates available (%s).\nUpdate outdated language files?", count( $locales_outdated ), implode( ', ', $locales_outdated ) ), $assoc_args ); $this->cli->runcommand( - "core language update", + "language core update", array( 'return' => false, 'launch' => true, 'exit_error' => false ) ); }