From 900b01fa7f44e1a106b84200451622569090ab78 Mon Sep 17 00:00:00 2001 From: Daniel Bachhuber Date: Wed, 2 Aug 2017 10:21:06 -0700 Subject: [PATCH] Update package with latest scaffolded components --- .github/ISSUE_TEMPLATE | 11 + .github/PULL_REQUEST_TEMPLATE | 14 ++ .gitignore | 1 - .travis.yml | 6 +- CONTRIBUTING.md | 8 + README.md | 21 +- bin/test.sh | 13 + features/bootstrap/FeatureContext.php | 330 +++++++++++++++++++++++--- features/bootstrap/Process.php | 21 +- features/bootstrap/ProcessRun.php | 29 +++ features/bootstrap/support.php | 4 +- features/bootstrap/utils.php | 311 ++++++++++++++++++++---- features/steps/given.php | 49 +++- features/steps/then.php | 10 +- utils/behat-tags.php | 12 +- 15 files changed, 736 insertions(+), 104 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE create mode 100644 .github/PULL_REQUEST_TEMPLATE create mode 100644 CONTRIBUTING.md create mode 100644 bin/test.sh diff --git a/.github/ISSUE_TEMPLATE b/.github/ISSUE_TEMPLATE new file mode 100644 index 0000000..6c06a14 --- /dev/null +++ b/.github/ISSUE_TEMPLATE @@ -0,0 +1,11 @@ + diff --git a/.github/PULL_REQUEST_TEMPLATE b/.github/PULL_REQUEST_TEMPLATE new file mode 100644 index 0000000..12b9800 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE @@ -0,0 +1,14 @@ + diff --git a/.gitignore b/.gitignore index f3b9082..54f24c8 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,3 @@ node_modules/ vendor/ *.zip *.tar.gz -composer.lock diff --git a/.travis.yml b/.travis.yml index b4a967d..c579887 100644 --- a/.travis.yml +++ b/.travis.yml @@ -29,9 +29,11 @@ matrix: env: WP_VERSION=latest - php: 5.6 env: WP_VERSION=latest + - php: 5.6 + env: WP_VERSION=3.7.11 - php: 5.6 env: WP_VERSION=trunk - - php: 5.4 + - php: 5.3 env: WP_VERSION=latest before_install: @@ -46,4 +48,4 @@ before_script: - composer validate script: - - behat --format progress --strict + - bash bin/test.sh diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..fa86aa5 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,8 @@ +Contributing +============ + +We appreciate you taking the initiative to contribute to this project. + +Contributing isn’t limited to just code. We encourage you to contribute in the way that best fits your abilities, by writing tutorials, giving a demo at your local meetup, helping other users with their support questions, or revising our documentation. + +For a more thorough introduction, [check out WP-CLI's guide to contributing](https://make.wordpress.org/cli/handbook/contributing/). This package follows those policy and guidelines. diff --git a/README.md b/README.md index a9238f9..e539e14 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Launch PHP's built-in web server for this specific WordPress installation. [![Build Status](https://travis-ci.org/wp-cli/server-command.svg?branch=master)](https://travis-ci.org/wp-cli/server-command) -Quick links: [Using](#using) | [Installing](#installing) | [Contributing](#contributing) +Quick links: [Using](#using) | [Installing](#installing) | [Contributing](#contributing) | [Support](#support) ## Using @@ -74,30 +74,25 @@ We appreciate you taking the initiative to contribute to this project. Contributing isn’t limited to just code. We encourage you to contribute in the way that best fits your abilities, by writing tutorials, giving a demo at your local meetup, helping other users with their support questions, or revising our documentation. +For a more thorough introduction, [check out WP-CLI's guide to contributing](https://make.wordpress.org/cli/handbook/contributing/). This package follows those policy and guidelines. + ### Reporting a bug Think you’ve found a bug? We’d love for you to help us get it fixed. Before you create a new issue, you should [search existing issues](https://github.com/wp-cli/server-command/issues?q=label%3Abug%20) to see if there’s an existing resolution to it, or if it’s already been fixed in a newer version. -Once you’ve done a bit of searching and discovered there isn’t an open or fixed issue for your bug, please [create a new issue](https://github.com/wp-cli/server-command/issues/new) with the following: - -1. What you were doing (e.g. "When I run `wp post list`"). -2. What you saw (e.g. "I see a fatal about a class being undefined."). -3. What you expected to see (e.g. "I expected to see the list of posts.") - -Include as much detail as you can, and clear steps to reproduce if possible. +Once you’ve done a bit of searching and discovered there isn’t an open or fixed issue for your bug, please [create a new issue](https://github.com/wp-cli/server-command/issues/new). Include as much detail as you can, and clear steps to reproduce if possible. For more guidance, [review our bug report documentation](https://make.wordpress.org/cli/handbook/bug-reports/). ### Creating a pull request Want to contribute a new feature? Please first [open a new issue](https://github.com/wp-cli/server-command/issues/new) to discuss whether the feature is a good fit for the project. -Once you've decided to commit the time to seeing your pull request through, please follow our guidelines for creating a pull request to make sure it's a pleasant experience: +Once you've decided to commit the time to seeing your pull request through, [please follow our guidelines for creating a pull request](https://make.wordpress.org/cli/handbook/pull-requests/) to make sure it's a pleasant experience. See "[Setting up](https://make.wordpress.org/cli/handbook/pull-requests/#setting-up)" for details specific to working on this package locally. + +## Support -1. Create a feature branch for each contribution. -2. Submit your pull request early for feedback. -3. Include functional tests with your changes. [Read the WP-CLI documentation](https://wp-cli.org/docs/pull-requests/#functional-tests) for an introduction. -4. Follow the [WordPress Coding Standards](http://make.wordpress.org/core/handbook/coding-standards/). +Github issues aren't for general support questions, but there are other venues you can try: http://wp-cli.org/#support *This README.md is generated dynamically from the project's codebase using `wp scaffold package-readme` ([doc](https://github.com/wp-cli/scaffold-package-command#wp-scaffold-package-readme)). To suggest changes, please submit a pull request against the corresponding part of the codebase.* diff --git a/bin/test.sh b/bin/test.sh new file mode 100644 index 0000000..bd3ae6e --- /dev/null +++ b/bin/test.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +set -ex + +# Run the unit tests, if they exist +if [ -f "phpunit.xml" ] || [ -f "phpunit.xml.dist" ] +then + phpunit +fi + +# Run the functional tests +BEHAT_TAGS=$(php utils/behat-tags.php) +behat --format progress $BEHAT_TAGS --strict diff --git a/features/bootstrap/FeatureContext.php b/features/bootstrap/FeatureContext.php index 13bb008..33620fa 100644 --- a/features/bootstrap/FeatureContext.php +++ b/features/bootstrap/FeatureContext.php @@ -32,7 +32,11 @@ require_once __DIR__ . '/../../php/utils.php'; require_once __DIR__ . '/../../php/WP_CLI/Process.php'; require_once __DIR__ . '/../../php/WP_CLI/ProcessRun.php'; - require_once __DIR__ . '/../../vendor/autoload.php'; + if ( file_exists( __DIR__ . '/../../vendor/autoload.php' ) ) { + require_once __DIR__ . '/../../vendor/autoload.php'; + } else if ( file_exists( __DIR__ . '/../../../../autoload.php' ) ) { + require_once __DIR__ . '/../../../../autoload.php'; + } } /** @@ -40,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', @@ -49,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 @@ -69,13 +107,24 @@ private static function get_process_env_variables() { if ( $config_path = getenv( 'WP_CLI_CONFIG_PATH' ) ) { $env['WP_CLI_CONFIG_PATH'] = $config_path; } + 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; @@ -105,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; } } @@ -115,36 +165,49 @@ 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 ) { - $this->proc( Utils\esc_cmd( 'rm -r %s', $this->variables['RUN_DIR'] ) )->run(); + 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 ) { - self::terminate_proc( $proc ); + $status = proc_get_status( $proc ); + self::terminate_proc( $status['pid'] ); } } /** * Terminate a process and any of its children. */ - private static function terminate_proc( $proc ) { - $status = proc_get_status( $proc ); - - $master_pid = $status['pid']; + private static function terminate_proc( $master_pid ) { $output = `ps -o ppid,pid,command | grep $master_pid`; @@ -154,34 +217,51 @@ private static function terminate_proc( $proc ) { $child = $matches[2]; if ( $parent == $master_pid ) { - if ( ! posix_kill( (int) $child, 9 ) ) { - throw new RuntimeException( posix_strerror( posix_get_last_error() ) ); - } + self::terminate_proc( $child ); } } } if ( ! posix_kill( (int) $master_pid, 9 ) ) { - throw new RuntimeException( posix_strerror( posix_get_last_error() ) ); + $errno = posix_get_last_error(); + // Ignore "No such process" error as that's what we want. + if ( 3 /*ESRCH*/ !== $errno ) { + throw new RuntimeException( posix_strerror( $errno ) ); + } } } + /** + * 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 ); @@ -195,10 +275,20 @@ 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 ) { - return preg_replace_callback( '/\{([A-Z_]+)\}/', array( $this, '_replace_var' ), $str ); + $ret = preg_replace_callback( '/\{([A-Z_]+)\}/', array( $this, '_replace_var' ), $str ); + if ( false !== strpos( $str, '{WP_VERSION-' ) ) { + $ret = $this->_replace_wp_versions( $ret ); + } + return $ret; } + /** + * Replace variables callback. + */ private function _replace_var( $matches ) { $cmd = $matches[0]; @@ -209,9 +299,58 @@ private function _replace_var( $matches ) { return $cmd; } + /** + * Substitute "{WP_VERSION-version-latest}" variables. + */ + private function _replace_wp_versions( $str ) { + static $wp_versions = null; + if ( null === $wp_versions ) { + $wp_versions = array(); + + $response = Requests::get( 'https://api.wordpress.org/core/version-check/1.7/', null, array( 'timeout' => 30 ) ); + if ( 200 === $response->status_code && ( $body = json_decode( $response->body ) ) && is_object( $body ) && isset( $body->offers ) && is_array( $body->offers ) ) { + // Latest version alias. + $wp_versions["{WP_VERSION-latest}"] = count( $body->offers ) ? $body->offers[0]->version : ''; + foreach ( $body->offers as $offer ) { + $sub_ver = preg_replace( '/(^[0-9]+\.[0-9]+)\.[0-9]+$/', '$1', $offer->version ); + $sub_ver_key = "{WP_VERSION-{$sub_ver}-latest}"; + + $main_ver = preg_replace( '/(^[0-9]+)\.[0-9]+$/', '$1', $sub_ver ); + $main_ver_key = "{WP_VERSION-{$main_ver}-latest}"; + + if ( ! isset( $wp_versions[ $main_ver_key ] ) ) { + $wp_versions[ $main_ver_key ] = $offer->version; + } + if ( ! isset( $wp_versions[ $sub_ver_key ] ) ) { + $wp_versions[ $sub_ver_key ] = $offer->version; + } + } + } + } + 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'] ); } } @@ -219,12 +358,12 @@ public function create_run_dir() { public function build_phar( $version = 'same' ) { $this->variables['PHAR_PATH'] = $this->variables['RUN_DIR'] . '/' . uniqid( "wp-cli-build-", TRUE ) . '.phar'; - // Test running against WP-CLI proper - $make_phar_path = __DIR__ . '/../../utils/make-phar.php'; + // Test running against a package installed as a WP-CLI dependency + // WP-CLI installed as a project dependency + $make_phar_path = __DIR__ . '/../../../../../utils/make-phar.php'; if ( ! file_exists( $make_phar_path ) ) { - // Test running against a package installed as a WP-CLI dependency - // WP-CLI installed as a project dependency - $make_phar_path = __DIR__ . '/../../../../../utils/make-phar.php'; + // Test running against WP-CLI proper + $make_phar_path = __DIR__ . '/../../utils/make-phar.php'; if ( ! file_exists( $make_phar_path ) ) { // WP-CLI as a dependency of this project $make_phar_path = __DIR__ . '/../../vendor/wp-cli/wp-cli/utils/make-phar.php'; @@ -239,14 +378,40 @@ public function build_phar( $version = 'same' ) { ) )->run_check(); } + public function download_phar( $version = 'same' ) { + if ( 'same' === $version ) { + $version = WP_CLI_VERSION; + } + + $download_url = sprintf( + 'https://github.com/wp-cli/wp-cli/releases/download/v%1$s/wp-cli-%1$s.phar', + $version + ); + + $this->variables['PHAR_PATH'] = $this->variables['RUN_DIR'] . '/' + . uniqid( 'wp-cli-download-', true ) + . '.phar'; + + Process::create( Utils\esc_cmd( + 'curl -sSfL %1$s > %2$s && chmod +x %2$s', + $download_url, + $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; } private static function run_sql( $sql ) { - Utils\run_mysql_command( 'mysql --no-defaults', array( + Utils\run_mysql_command( '/usr/bin/env mysql --no-defaults', array( 'execute' => $sql, 'host' => self::$db_settings['dbhost'], 'user' => self::$db_settings['dbuser'], @@ -309,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!"; @@ -322,27 +501,34 @@ 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' ); } - 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(); } public function install_wp( $subdir = '' ) { + $subdir = $this->replace_variables( $subdir ); + $this->create_db(); $this->create_run_dir(); $this->download_wp( $subdir ); - $this->create_config( $subdir ); $install_args = array( @@ -355,5 +541,77 @@ public function install_wp( $subdir = '' ) { $this->proc( 'wp core install', $install_args, $subdir )->run_check(); } -} + public function install_wp_with_composer() { + $this->create_run_dir(); + $this->create_db(); + + $yml_path = $this->variables['RUN_DIR'] . "/wp-cli.yml"; + file_put_contents( $yml_path, 'path: wordpress' ); + + $this->proc( 'composer init --name="wp-cli/composer-test" --type="project" --no-interaction' )->run_check(); + $this->proc( 'composer require johnpbloch/wordpress --optimize-autoloader --no-interaction' )->run_check(); + + $config_extra_php = "require_once dirname(__DIR__) . '/vendor/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__ . '/../../' ); + + $dest = self::$composer_local_repository . '/'; + + self::copy_dir( $src, $dest ); + self::remove_dir( $dest . '.git' ); + self::remove_dir( $dest . 'vendor' ); + + $this->proc( "composer config repositories.wp-cli '{\"type\": \"path\", \"url\": \"$dest\", \"options\": {\"symlink\": false}}'" )->run_check(); + } + $this->variables['COMPOSER_LOCAL_REPOSITORY'] = self::$composer_local_repository; + } + + public function composer_require_current_wp_cli() { + $this->composer_add_wp_cli_local_repository(); + $this->proc( 'composer require wp-cli/wp-cli:dev-master --optimize-autoloader --no-interaction' )->run_check(); + } + + 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 ); + } + +} 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/support.php b/features/bootstrap/support.php index 75ee5fb..a37a064 100644 --- a/features/bootstrap/support.php +++ b/features/bootstrap/support.php @@ -182,8 +182,8 @@ function checkThatCsvStringContainsValues( $actualCSV, $expectedCSV ) { * @param[in] $expectedYaml the expected YAML string */ function checkThatYamlStringContainsYamlString( $actualYaml, $expectedYaml ) { - $actualValue = spyc_load( $actualYaml ); - $expectedValue = spyc_load( $expectedYaml ); + $actualValue = Mustangostang\Spyc::YAMLLoad( $actualYaml ); + $expectedValue = Mustangostang\Spyc::YAMLLoad( $expectedYaml ); if ( !$actualValue ) { return false; diff --git a/features/bootstrap/utils.php b/features/bootstrap/utils.php index a4867c5..4679605 100644 --- a/features/bootstrap/utils.php +++ b/features/bootstrap/utils.php @@ -10,8 +10,10 @@ use \WP_CLI\Dispatcher; use \WP_CLI\Iterators\Transform; +const PHAR_STREAM_PREFIX = 'phar://'; + function inside_phar() { - return 0 === strpos( WP_CLI_ROOT, 'phar://' ); + return 0 === strpos( WP_CLI_ROOT, PHAR_STREAM_PREFIX ); } // Files that need to be read by external programs have to be extracted from the Phar archive. @@ -87,19 +89,6 @@ function load_command( $name ) { } } -function load_all_commands() { - $cmd_dir = WP_CLI_ROOT . '/php/commands'; - - $iterator = new \DirectoryIterator( $cmd_dir ); - - foreach ( $iterator as $filename ) { - if ( '.php' != substr( $filename, -4 ) ) - continue; - - include_once "$cmd_dir/$filename"; - } -} - /** * Like array_map(), except it returns a new iterator, instead of a modified array. * @@ -197,10 +186,15 @@ function assoc_args_to_str( $assoc_args ) { $str = ''; foreach ( $assoc_args as $key => $value ) { - if ( true === $value ) + if ( true === $value ) { $str .= " --$key"; - else + } elseif( is_array( $value ) ) { + foreach( $value as $_ => $v ) { + $str .= assoc_args_to_str( array( $key => $v ) ); + } + } else { $str .= " --$key=" . escapeshellarg( $value ); + } } return $str; @@ -347,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 { @@ -421,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 ); @@ -434,7 +432,7 @@ function run_mysql_command( $cmd, $assoc_args, $descriptors = null ) { $old_pass = getenv( 'MYSQL_PWD' ); putenv( 'MYSQL_PWD=' . $pass ); - $final_cmd = $cmd . assoc_args_to_str( $assoc_args ); + $final_cmd = force_env_on_nix_systems( $cmd ) . assoc_args_to_str( $assoc_args ); $proc = proc_open( $final_cmd, $descriptors, $pipes ); if ( !$proc ) @@ -459,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 ); @@ -512,9 +510,11 @@ function parse_url( $url ) { /** * Check if we're running in a Windows environment (cmd.exe). + * + * @return bool */ function is_windows() { - return strtoupper(substr(PHP_OS, 0, 3)) === 'WIN'; + return false !== ( $test_is_windows = getenv( 'WP_CLI_TEST_IS_WINDOWS' ) ) ? (bool) $test_is_windows : strtoupper(substr(PHP_OS, 0, 3)) === 'WIN'; } /** @@ -526,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 ); @@ -563,7 +563,7 @@ function http_request( $method, $url, $data = null, $headers = array(), $options if ( inside_phar() ) { // cURL can't read Phar archives $options['verify'] = extract_from_phar( - WP_CLI_ROOT . '/vendor' . $cert_path ); + WP_CLI_VENDOR_DIR . $cert_path ); } else { foreach( get_vendor_paths() as $vendor_path ) { if ( file_exists( $vendor_path . $cert_path ) ) { @@ -572,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." ); } } @@ -610,20 +610,20 @@ function increment_version( $current_version, $new_version ) { switch ( $new_version ) { case 'same': // do nothing - break; + break; case 'patch': $current_version[0][2]++; $current_version = array( $current_version[0] ); // drop possible pre-release info - break; + break; case 'minor': $current_version[0][1]++; $current_version[0][2] = 0; $current_version = array( $current_version[0] ); // drop possible pre-release info - break; + break; case 'major': $current_version[0][0]++; @@ -631,11 +631,11 @@ function increment_version( $current_version, $new_version ) { $current_version[0][2] = 0; $current_version = array( $current_version[0] ); // drop possible pre-release info - break; + break; default: // not a keyword $current_version = array( array( $new_version ) ); - break; + break; } // reconstruct version string @@ -698,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. * @@ -709,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/'; } @@ -728,7 +757,7 @@ function get_temp_dir() { \WP_CLI::warning( "Temp directory isn't writable: {$temp}" ); } - return $trailingslashit( $temp ); + return $temp; } /** @@ -745,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: @@ -840,3 +875,195 @@ function parse_str_to_argv( $arguments ) { function basename( $path, $suffix = '' ) { return urldecode( \basename( str_replace( array( '%2F', '%5C' ), '/', urlencode( $path ) ), $suffix ) ); } + +/** + * Checks whether the output of the current script is a TTY or a pipe / redirect + * + * Returns true if STDOUT output is being redirected to a pipe or a file; false is + * output is being sent directly to the terminal. + * + * If an env variable SHELL_PIPE exists, returned result depends it's + * value. Strings like 1, 0, yes, no, that validate to booleans are accepted. + * + * To enable ASCII formatting even when shell is piped, use the + * ENV variable SHELL_PIPE=0 + * + * @access public + * + * @return bool + */ +function isPiped() { + $shellPipe = getenv('SHELL_PIPE'); + + if ($shellPipe !== false) { + return filter_var($shellPipe, FILTER_VALIDATE_BOOLEAN); + } else { + return (function_exists('posix_isatty') && !posix_isatty(STDOUT)); + } +} + +/** + * Expand within paths to their matching paths. + * + * Has no effect on paths which do not use glob patterns. + * + * @param string|array $paths Single path as a string, or an array of paths. + * @param int $flags Flags to pass to glob. + * + * @return array Expanded paths. + */ +function expand_globs( $paths, $flags = GLOB_BRACE ) { + $expanded = array(); + + foreach ( (array) $paths as $path ) { + $matching = array( $path ); + + if ( preg_match( '/[' . preg_quote( '*?[]{}!', '/' ) . ']/', $path ) ) { + $matching = glob( $path, $flags ) ?: array(); + } + + $expanded = array_merge( $expanded, $matching ); + } + + return array_unique( $expanded ); +} + +/** + * Get the closest suggestion for a mis-typed target term amongst a list of + * options. + * + * Uses the Levenshtein algorithm to calculate the relative "distance" between + * terms. + * + * If the "distance" to the closest term is higher than the threshold, an empty + * string is returned. + * + * @param string $target Target term to get a suggestion for. + * @param array $options Array with possible options. + * @param int $threshold Threshold above which to return an empty string. + * + * @return string + */ +function get_suggestion( $target, array $options, $threshold = 2 ) { + if ( empty( $options ) ) { + return ''; + } + foreach ( $options as $option ) { + $distance = levenshtein( $option, $target ); + $levenshtein[ $option ] = $distance; + } + + // Sort known command strings by distance to user entry. + asort( $levenshtein ); + + // Fetch the closest command string. + reset( $levenshtein ); + $suggestion = key( $levenshtein ); + + // Only return a suggestion if below a given threshold. + return $levenshtein[ $suggestion ] <= $threshold && $suggestion !== $target + ? (string) $suggestion + : ''; +} + +/** + * Get a Phar-safe version of a path. + * + * For paths inside a Phar, this strips the outer filesystem's location to + * reduce the path to what it needs to be within the Phar archive. + * + * Use the __FILE__ or __DIR__ constants as a starting point. + * + * @param string $path An absolute path that might be within a Phar. + * + * @return string A Phar-safe version of the path. + */ +function phar_safe_path( $path ) { + + if ( ! inside_phar() ) { + return $path; + } + + return str_replace( + PHAR_STREAM_PREFIX . WP_CLI_PHAR_PATH . '/', + PHAR_STREAM_PREFIX, + $path + ); +} + +/** + * Check whether a given Command object is part of the bundled set of + * commands. + * + * This function accepts both a fully qualified class name as a string as + * well as an object that extends `WP_CLI\Dispatcher\CompositeCommand`. + * + * @param \WP_CLI\Dispatcher\CompositeCommand|string $command + * + * @return bool + */ +function is_bundled_command( $command ) { + static $classes; + + if ( null === $classes ) { + $classes = array(); + $class_map = WP_CLI_VENDOR_DIR . '/composer/autoload_commands_classmap.php'; + if ( file_exists( WP_CLI_VENDOR_DIR . '/composer/') ) { + $classes = include $class_map; + } + } + + if ( is_object( $command ) ) { + $command = get_class( $command ); + } + + return is_string( $command ) + ? array_key_exists( $command, $classes ) + : false; +} + +/** + * Maybe prefix command string with "/usr/bin/env". + * Removes (if there) if Windows, adds (if not there) if not. + * + * @param string $command + * + * @return string + */ +function force_env_on_nix_systems( $command ) { + $env_prefix = '/usr/bin/env '; + $env_prefix_len = strlen( $env_prefix ); + if ( is_windows() ) { + if ( 0 === strncmp( $command, $env_prefix, $env_prefix_len ) ) { + $command = substr( $command, $env_prefix_len ); + } + } else { + if ( 0 !== strncmp( $command, $env_prefix, $env_prefix_len ) ) { + $command = $env_prefix . $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 a1b6a1b..e7ae7bf 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,12 @@ function ( $world, $subdir ) { } ); +$steps->Given( '/^a WP install with Composer$/', + function ( $world ) { + $world->install_wp_with_composer(); + } +); + $steps->Given( '/^a WP multisite (subdirectory|subdomain)?\s?install$/', function ( $world, $type = 'subdirectory' ) { $world->install_wp(); @@ -127,12 +152,18 @@ function ( $world, $stream, $output_filter, $key ) { } ); -$steps->Given( '/^a new Phar(?: with version "([^"]+)")$/', - function ( $world, $version ) { +$steps->Given( '/^a new Phar with (?:the same version|version "([^"]+)")$/', + function ( $world, $version = 'same' ) { $world->build_phar( $version ); } ); +$steps->Given( '/^a downloaded Phar with (?:the same version|version "([^"]+)")$/', + function ( $world, $version = 'same' ) { + $world->download_phar( $version ); + } +); + $steps->Given( '/^save the (.+) file ([\'].+[^\'])?as \{(\w+)\}$/', function ( $world, $filepath, $output_filter, $key ) { $full_file = file_get_contents( $world->replace_variables( $filepath ) ); @@ -162,3 +193,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 a5974f1..8875554 100644 --- a/features/steps/then.php +++ b/features/steps/then.php @@ -145,8 +145,9 @@ function ( $world, $stream ) { } ); -$steps->Then( '/^(STDOUT|STDERR) should be a version string (<|<=|>|>=|==|=|!=|<>) ([+\w\.-]+)$/', +$steps->Then( '/^(STDOUT|STDERR) should be a version string (<|<=|>|>=|==|=|!=|<>) ([+\w.{}-]+)$/', function ( $world, $stream, $operator, $goal_ver ) { + $goal_ver = $world->replace_variables( $goal_ver ); $stream = strtolower( $stream ); if ( false === version_compare( trim( $world->result->$stream, "\n" ), $goal_ver, $operator ) ) { throw new Exception( $world->result ); @@ -208,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/utils/behat-tags.php b/utils/behat-tags.php index df836b8..ee51fc9 100644 --- a/utils/behat-tags.php +++ b/utils/behat-tags.php @@ -31,8 +31,15 @@ function version_tags( $prefix, $current, $operator = '<' ) { return $skip_tags; } +$wp_version_reqs = array(); +// Only apply @require-wp tags when WP_VERSION isn't 'latest' or 'nightly' +// 'latest' and 'nightly' are expected to work with all features +if ( ! in_array( getenv( 'WP_VERSION' ), array( 'latest', 'nightly', 'trunk' ), true ) ) { + $wp_version_reqs = version_tags( 'require-wp', getenv( 'WP_VERSION' ), '<' ); +} + $skip_tags = array_merge( - version_tags( 'require-wp', getenv( 'WP_VERSION' ), '<' ), + $wp_version_reqs, version_tags( 'require-php', PHP_VERSION, '<' ), version_tags( 'less-than-php', PHP_VERSION, '>' ) ); @@ -40,6 +47,9 @@ function version_tags( $prefix, $current, $operator = '<' ) { # Skip Github API tests by default because of rate limiting. See https://github.com/wp-cli/wp-cli/issues/1612 $skip_tags[] = '@github-api'; +# Skip tests known to be broken. +$skip_tags[] = '@broken'; + # Require PHP extension, eg 'imagick'. function extension_tags() { $extension_tags = array();