diff --git a/.travis.yml b/.travis.yml index 0835aa68c..923370a29 100644 --- a/.travis.yml +++ b/.travis.yml @@ -39,7 +39,13 @@ matrix: env: WP_VERSION=latest before_install: - - phpenv config-rm xdebug.ini + - | + # Remove Xdebug for a huge performance increase: + if [ -f ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/xdebug.ini ]; then + phpenv config-rm xdebug.ini + else + echo "xdebug.ini does not exist" + fi install: - composer require wp-cli/wp-cli:dev-master diff --git a/composer.json b/composer.json index 9371e9e80..125cd8554 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "wp-cli/super-admin-command", - "description": "Manage super admins on WordPress multisite.", + "description": "Lists, adds, or removes super admin users on a multisite install.", "type": "wp-cli-package", "homepage": "https://github.com/wp-cli/super-admin-command", "support": { diff --git a/features/bootstrap/FeatureContext.php b/features/bootstrap/FeatureContext.php index 33620fa61..aaf39713b 100644 --- a/features/bootstrap/FeatureContext.php +++ b/features/bootstrap/FeatureContext.php @@ -54,6 +54,11 @@ class FeatureContext extends BehatContext implements ClosuredContextInterface { */ private static $cache_dir; + /** + * The directory that holds the install cache, and which is copied to RUN_DIR during a "Given a WP install" step. Recreated on each suite run. + */ + private static $install_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). @@ -92,6 +97,19 @@ class FeatureContext extends BehatContext implements ClosuredContextInterface { */ private static $temp_dir_infix; + /** + * Settings and variables for WP_CLI_TEST_LOG_RUN_TIMES run time logging. + */ + private static $log_run_times; // Whether to log run times - WP_CLI_TEST_LOG_RUN_TIMES env var. Set on `@BeforeScenario'. + private static $suite_start_time; // When the suite started, set on `@BeforeScenario'. + private static $output_to; // Where to output log - stdout|error_log. Set on `@BeforeSuite`. + private static $num_top_processes; // Number of processes/methods to output by longest run times. Set on `@BeforeSuite`. + private static $num_top_scenarios; // Number of scenarios to output by longest run times. Set on `@BeforeSuite`. + + private static $scenario_run_times = array(); // Scenario run times (top `self::$num_top_scenarios` only). + private static $scenario_count = 0; // Scenario count, incremented on `@AfterScenario`. + private static $proc_method_run_times = array(); // Array of run time info for proc methods, keyed by method name and arg, each a 2-element array containing run time and run count. + /** * Get the environment variables required for launched `wp` processes */ @@ -102,7 +120,7 @@ private static function get_process_env_variables() { $env = array( 'PATH' => $bin_dir . ':' . $vendor_dir . ':' . getenv( 'PATH' ), 'BEHAT_RUN' => 1, - 'HOME' => '/tmp/wp-cli-home', + 'HOME' => sys_get_temp_dir() . '/wp-cli-home', ); if ( $config_path = getenv( 'WP_CLI_CONFIG_PATH' ) ) { $env['WP_CLI_CONFIG_PATH'] = $config_path; @@ -116,6 +134,9 @@ private static function get_process_env_variables() { if ( $travis_build_dir = getenv( 'TRAVIS_BUILD_DIR' ) ) { $env['TRAVIS_BUILD_DIR'] = $travis_build_dir; } + if ( $github_token = getenv( 'GITHUB_TOKEN' ) ) { + $env['GITHUB_TOKEN'] = $github_token; + } return $env; } @@ -124,14 +145,15 @@ private static function get_process_env_variables() { * 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'; + $wp_version_suffix = ( $wp_version = getenv( 'WP_VERSION' ) ) ? "-$wp_version" : ''; + self::$cache_dir = sys_get_temp_dir() . '/wp-cli-test-core-download-cache' . $wp_version_suffix; if ( is_readable( self::$cache_dir . '/wp-config-sample.php' ) ) return; $cmd = Utils\esc_cmd( 'wp core download --force --path=%s', self::$cache_dir ); - if ( getenv( 'WP_VERSION' ) ) { - $cmd .= Utils\esc_cmd( ' --version=%s', getenv( 'WP_VERSION' ) ); + if ( $wp_version ) { + $cmd .= Utils\esc_cmd( ' --version=%s', $wp_version ); } Process::create( $cmd, null, self::get_process_env_variables() )->run_check(); } @@ -140,6 +162,11 @@ private static function cache_wp_files() { * @BeforeSuite */ public static function prepare( SuiteEvent $event ) { + // Test performance statistics - useful for detecting slow tests. + if ( self::$log_run_times = getenv( 'WP_CLI_TEST_LOG_RUN_TIMES' ) ) { + self::log_run_times_before_suite( $event ); + } + $result = Process::create( 'wp cli info', null, self::get_process_env_variables() )->run_check(); echo PHP_EOL; echo $result->stdout; @@ -148,6 +175,13 @@ public static function prepare( SuiteEvent $event ) { $result = Process::create( Utils\esc_cmd( 'wp core version --path=%s', self::$cache_dir ) , null, self::get_process_env_variables() )->run_check(); echo 'WordPress ' . $result->stdout; echo PHP_EOL; + + // Remove install cache if any (not setting the static var). + $wp_version_suffix = ( $wp_version = getenv( 'WP_VERSION' ) ) ? "-$wp_version" : ''; + $install_cache_dir = sys_get_temp_dir() . '/wp-cli-test-core-install-cache' . $wp_version_suffix; + if ( file_exists( $install_cache_dir ) ) { + self::remove_dir( $install_cache_dir ); + } } /** @@ -158,12 +192,20 @@ public static function afterSuite( SuiteEvent $event ) { self::remove_dir( self::$composer_local_repository ); self::$composer_local_repository = null; } + + if ( self::$log_run_times ) { + self::log_run_times_after_suite( $event ); + } } /** * @BeforeScenario */ public function beforeScenario( $event ) { + if ( self::$log_run_times ) { + self::log_run_times_before_scenario( $event ); + } + $this->variables['SRC_DIR'] = realpath( __DIR__ . '/../..' ); // Used in the names of the RUN_DIR and SUITE_CACHE_DIR directories. @@ -202,6 +244,10 @@ public function afterScenario( $event ) { $status = proc_get_status( $proc ); self::terminate_proc( $status['pid'] ); } + + if ( self::$log_run_times ) { + self::log_run_times_after_scenario( $event ); + } } /** @@ -338,6 +384,8 @@ private static function get_event_file( $event, &$line ) { $scenario_feature = $event->getScenario(); } elseif ( method_exists( $event, 'getFeature' ) ) { $scenario_feature = $event->getFeature(); + } elseif ( method_exists( $event, 'getOutline' ) ) { + $scenario_feature = $event->getOutline(); } else { return null; } @@ -410,23 +458,37 @@ private function set_cache_dir() { $this->variables['CACHE_DIR'] = $path; } - private static function run_sql( $sql ) { - Utils\run_mysql_command( '/usr/bin/env mysql --no-defaults', array( - 'execute' => $sql, + /** + * Run a MySQL command with `$db_settings`. + * + * @param string $sql_cmd Command to run. + * @param array $assoc_args Optional. Associative array of options. Default empty. + * @param bool $add_database Optional. Whether to add dbname to the $sql_cmd. Default false. + */ + private static function run_sql( $sql_cmd, $assoc_args = array(), $add_database = false ) { + $default_assoc_args = array( 'host' => self::$db_settings['dbhost'], 'user' => self::$db_settings['dbuser'], 'pass' => self::$db_settings['dbpass'], - ) ); + ); + if ( $add_database ) { + $sql_cmd .= ' ' . escapeshellarg( self::$db_settings['dbname'] ); + } + $start_time = microtime( true ); + Utils\run_mysql_command( $sql_cmd, array_merge( $assoc_args, $default_assoc_args ) ); + if ( self::$log_run_times ) { + self::log_proc_method_run_time( 'run_sql ' . $sql_cmd, $start_time ); + } } public function create_db() { $dbname = self::$db_settings['dbname']; - self::run_sql( "CREATE DATABASE IF NOT EXISTS $dbname" ); + self::run_sql( 'mysql --no-defaults', array( 'execute' => "CREATE DATABASE IF NOT EXISTS $dbname" ) ); } public function drop_db() { $dbname = self::$db_settings['dbname']; - self::run_sql( "DROP DATABASE IF EXISTS $dbname" ); + self::run_sql( 'mysql --no-defaults', array( 'execute' => "DROP DATABASE IF EXISTS $dbname" ) ); } public function proc( $command, $assoc_args = array(), $path = '' ) { @@ -520,10 +582,29 @@ public function create_config( $subdir = '', $extra_php = false ) { $params['extra-php'] = $extra_php; } - $this->proc( 'wp core config', $params, $subdir )->run_check(); + $config_cache_path = ''; + if ( self::$install_cache_dir ) { + $config_cache_path = self::$install_cache_dir . '/config_' . md5( implode( ':', $params ) . ':subdir=' . $subdir ); + $run_dir = '' !== $subdir ? ( $this->variables['RUN_DIR'] . "/$subdir" ) : $this->variables['RUN_DIR']; + } + + if ( $config_cache_path && file_exists( $config_cache_path ) ) { + copy( $config_cache_path, $run_dir . '/wp-config.php' ); + } else { + $this->proc( 'wp config create', $params, $subdir )->run_check(); + if ( $config_cache_path && file_exists( $run_dir . '/wp-config.php' ) ) { + copy( $run_dir . '/wp-config.php', $config_cache_path ); + } + } } public function install_wp( $subdir = '' ) { + $wp_version_suffix = ( $wp_version = getenv( 'WP_VERSION' ) ) ? "-$wp_version" : ''; + self::$install_cache_dir = sys_get_temp_dir() . '/wp-cli-test-core-install-cache' . $wp_version_suffix; + if ( ! file_exists( self::$install_cache_dir ) ) { + mkdir( self::$install_cache_dir ); + } + $subdir = $this->replace_variables( $subdir ); $this->create_db(); @@ -539,20 +620,37 @@ public function install_wp( $subdir = '' ) { 'admin_password' => 'password1' ); - $this->proc( 'wp core install', $install_args, $subdir )->run_check(); + $install_cache_path = ''; + if ( self::$install_cache_dir ) { + $install_cache_path = self::$install_cache_dir . '/install_' . md5( implode( ':', $install_args ) . ':subdir=' . $subdir ); + $run_dir = '' !== $subdir ? ( $this->variables['RUN_DIR'] . "/$subdir" ) : $this->variables['RUN_DIR']; + } + + if ( $install_cache_path && file_exists( $install_cache_path ) ) { + self::copy_dir( $install_cache_path, $run_dir ); + self::run_sql( 'mysql --no-defaults', array( 'execute' => "source {$install_cache_path}.sql" ), true /*add_database*/ ); + } else { + $this->proc( 'wp core install', $install_args, $subdir )->run_check(); + if ( $install_cache_path ) { + mkdir( $install_cache_path ); + self::dir_diff_copy( $run_dir, self::$cache_dir, $install_cache_path ); + self::run_sql( 'mysqldump --no-defaults', array( 'result-file' => "{$install_cache_path}.sql" ), true /*add_database*/ ); + } + } } - public function install_wp_with_composer() { + 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->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(); + $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/autoload.php';"; + $config_extra_php = "require_once dirname(__DIR__) . '/" . $vendor_directory . "/autoload.php';"; $this->create_config( 'wordpress', $config_extra_php ); $install_args = array( @@ -574,20 +672,18 @@ public function composer_add_wp_cli_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(); + 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->proc( 'composer require wp-cli/wp-cli:dev-master --optimize-autoloader --no-interaction' )->run_check(); + $this->composer_command( 'require wp-cli/wp-cli:dev-master --optimize-autoloader --no-interaction' ); } public function get_php_binary() { @@ -603,15 +699,209 @@ public function get_php_binary() { return 'php'; } - public function start_php_server() { + public function start_php_server( $subdir = '' ) { + $dir = $this->variables['RUN_DIR'] . '/'; + if ( $subdir ) { + $dir .= trim( $subdir, '/' ) . '/'; + } $cmd = Utils\esc_cmd( '%s -S %s -t %s -c %s %s', $this->get_php_binary(), 'localhost:8080', - $this->variables['RUN_DIR'] . '/wordpress/', + $dir, 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(); + } + + /** + * Initialize run time logging. + */ + private static function log_run_times_before_suite( $event ) { + self::$suite_start_time = microtime( true ); + + Process::$log_run_times = true; + + $travis = getenv( 'TRAVIS' ); + + // Default output settings. + self::$output_to = 'stdout'; + self::$num_top_processes = $travis ? 10 : 40; + self::$num_top_scenarios = $travis ? 10 : 20; + + // Allow setting of above with "WP_CLI_TEST_LOG_RUN_TIMES=[,][,]" formatted env var. + if ( preg_match( '/^(stdout|error_log)?(,[0-9]+)?(,[0-9]+)?$/i', self::$log_run_times, $matches ) ) { + if ( isset( $matches[1] ) ) { + self::$output_to = strtolower( $matches[1] ); + } + if ( isset( $matches[2] ) ) { + self::$num_top_processes = max( (int) substr( $matches[2], 1 ), 1 ); + } + if ( isset( $matches[3] ) ) { + self::$num_top_scenarios = max( (int) substr( $matches[3], 1 ), 1 ); + } + } + } + + /** + * Record the start time of the scenario into the `$scenario_run_times` array. + */ + private static function log_run_times_before_scenario( $event ) { + if ( $scenario_key = self::get_scenario_key( $event ) ) { + self::$scenario_run_times[ $scenario_key ] = -microtime( true ); + } + } + + /** + * Save the run time of the scenario into the `$scenario_run_times` array. Only the top `self::$num_top_scenarios` are kept. + */ + private static function log_run_times_after_scenario( $event ) { + if ( $scenario_key = self::get_scenario_key( $event ) ) { + self::$scenario_run_times[ $scenario_key ] += microtime( true ); + self::$scenario_count++; + if ( count( self::$scenario_run_times ) > self::$num_top_scenarios ) { + arsort( self::$scenario_run_times ); + array_pop( self::$scenario_run_times ); + } + } + } + + /** + * Copy files in updated directory that are not in source directory to copy directory. ("Incremental backup".) + * Note: does not deal with changed files (ie does not compare file contents for changes), for speed reasons. + * + * @param string $upd_dir The directory to search looking for files/directories not in `$src_dir`. + * @param string $src_dir The directory to be compared to `$upd_dir`. + * @param string $cop_dir Where to copy any files/directories in `$upd_dir` but not in `$src_dir` to. + */ + private static function dir_diff_copy( $upd_dir, $src_dir, $cop_dir ) { + if ( false === ( $files = scandir( $upd_dir ) ) ) { + $error = error_get_last(); + throw new \RuntimeException( sprintf( "Failed to open updated directory '%s': %s. " . __FILE__ . ':' . __LINE__, $upd_dir, $error['message'] ) ); + } + foreach ( array_diff( $files, array( '.', '..' ) ) as $file ) { + $upd_file = $upd_dir . '/' . $file; + $src_file = $src_dir . '/' . $file; + $cop_file = $cop_dir . '/' . $file; + if ( ! file_exists( $src_file ) ) { + if ( is_dir( $upd_file ) ) { + if ( ! file_exists( $cop_file ) && ! mkdir( $cop_file, 0777, true /*recursive*/ ) ) { + $error = error_get_last(); + throw new \RuntimeException( sprintf( "Failed to create copy directory '%s': %s. " . __FILE__ . ':' . __LINE__, $cop_file, $error['message'] ) ); + } + self::copy_dir( $upd_file, $cop_file ); + } else { + if ( ! copy( $upd_file, $cop_file ) ) { + $error = error_get_last(); + throw new \RuntimeException( sprintf( "Failed to copy '%s' to '%s': %s. " . __FILE__ . ':' . __LINE__, $upd_file, $cop_file, $error['message'] ) ); + } + } + } elseif ( is_dir( $upd_file ) ) { + self::dir_diff_copy( $upd_file, $src_file, $cop_file ); + } + } + } + + /** + * Get the scenario key used for `$scenario_run_times` array. + * Format " :", eg "core-command core-update.feature:221". + */ + private static function get_scenario_key( $event ) { + $scenario_key = ''; + if ( $file = self::get_event_file( $event, $line ) ) { + $scenario_grandparent = Utils\basename( dirname( dirname( $file ) ) ); + $scenario_key = $scenario_grandparent . ' ' . Utils\basename( $file ) . ':' . $line; + } + return $scenario_key; + } + + /** + * Print out stats on the run times of processes and scenarios. + */ + private static function log_run_times_after_suite( $event ) { + + $suite = ''; + if ( self::$scenario_run_times ) { + // Grandparent directory is first part of key. + $keys = array_keys( self::$scenario_run_times ); + $suite = substr( $keys[0], 0, strpos( $keys[0], ' ' ) ); + } + + $run_from = Utils\basename( dirname( dirname( __DIR__ ) ) ); + + // Format same as Behat, if have minutes. + $fmt = function ( $time ) { + $mins = floor( $time / 60 ); + return round( $time, 3 ) . ( $mins ? ( ' (' . $mins . 'm' . round( $time - ( $mins * 60 ), 3 ) . 's)' ) : '' ); + }; + + $time = microtime( true ) - self::$suite_start_time; + + $log = PHP_EOL . str_repeat( '(', 80 ) . PHP_EOL; + + // Process and proc method run times. + $run_times = array_merge( Process::$run_times, self::$proc_method_run_times ); + + list( $ptime, $calls ) = array_reduce( $run_times, function ( $carry, $item ) { + return array( $carry[0] + $item[0], $carry[1] + $item[1] ); + }, array( 0, 0 ) ); + + $overhead = $time - $ptime; + $pct = round( ( $overhead / $time ) * 100 ); + $unique = count( $run_times ); + + $log .= sprintf( + PHP_EOL . "Total process run time %s (tests %s, overhead %.3f %d%%), calls %d (%d unique) for '%s' run from '%s'" . PHP_EOL, + $fmt( $ptime ), $fmt( $time ), $overhead, $pct, $calls, $unique, $suite, $run_from + ); + + uasort( $run_times, function ( $a, $b ) { + return $a[0] === $b[0] ? 0 : ( $a[0] < $b[0] ? 1 : -1 ); // Reverse sort. + } ); + + $tops = array_slice( $run_times, 0, self::$num_top_processes, true ); + + $log .= PHP_EOL . "Top " . self::$num_top_processes . " process run times for '$suite'"; + $log .= PHP_EOL . implode( PHP_EOL, array_map( function ( $k, $v, $i ) { + return sprintf( ' %3d. %7.3f %3d %s', $i + 1, round( $v[0], 3 ), $v[1], $k ); + }, array_keys( $tops ), $tops, array_keys( array_keys( $tops ) ) ) ) . PHP_EOL; + + // Scenario run times. + arsort( self::$scenario_run_times ); + + $tops = array_slice( self::$scenario_run_times, 0, self::$num_top_scenarios, true ); + + $log .= PHP_EOL . "Top " . self::$num_top_scenarios . " (of " . self::$scenario_count . ") scenario run times for '$suite'"; + $log .= PHP_EOL . implode( PHP_EOL, array_map( function ( $k, $v, $i ) { + return sprintf( ' %3d. %7.3f %s', $i + 1, round( $v, 3 ), substr( $k, strpos( $k, ' ' ) + 1 ) ); + }, array_keys( $tops ), $tops, array_keys( array_keys( $tops ) ) ) ) . PHP_EOL; + + $log .= PHP_EOL . str_repeat( ')', 80 ); + + if ( 'error_log' === self::$output_to ) { + error_log( $log ); + } else { + echo PHP_EOL . $log; + } + } + + /** + * Log the run time of a proc method (one that doesn't use Process but does (use a function that does) a `proc_open()`). + */ + private static function log_proc_method_run_time( $key, $start_time ) { + $run_time = microtime( true ) - $start_time; + if ( ! isset( self::$proc_method_run_times[ $key ] ) ) { + self::$proc_method_run_times[ $key ] = array( 0, 0 ); + } + self::$proc_method_run_times[ $key ][0] += $run_time; + self::$proc_method_run_times[ $key ][1]++; + } + } diff --git a/features/bootstrap/Process.php b/features/bootstrap/Process.php index 858e1947e..5016580b7 100644 --- a/features/bootstrap/Process.php +++ b/features/bootstrap/Process.php @@ -21,6 +21,25 @@ class Process { */ private $env; + /** + * @var array Descriptor spec for `proc_open()`. + */ + private static $descriptors = array( + 0 => STDIN, + 1 => array( 'pipe', 'w' ), + 2 => array( 'pipe', 'w' ), + ); + + /** + * @var bool Whether to log run time info or not. + */ + public static $log_run_times = false; + + /** + * @var array Array of process run time info, keyed by process command, each a 2-element array containing run time and run count. + */ + public static $run_times = array(); + /** * @param string $command Command to execute. * @param string $cwd Directory to execute the command in. @@ -46,15 +65,9 @@ private function __construct() {} * @return ProcessRun */ public function run() { - $cwd = $this->cwd; - - $descriptors = array( - 0 => STDIN, - 1 => array( 'pipe', 'w' ), - 2 => array( 'pipe', 'w' ), - ); + $start_time = microtime( true ); - $proc = proc_open( $this->command, $descriptors, $pipes, $cwd, $this->env ); + $proc = proc_open( $this->command, self::$descriptors, $pipes, $this->cwd, $this->env ); $stdout = stream_get_contents( $pipes[1] ); fclose( $pipes[1] ); @@ -62,14 +75,29 @@ public function run() { $stderr = stream_get_contents( $pipes[2] ); fclose( $pipes[2] ); - return new ProcessRun( array( - 'stdout' => $stdout, - 'stderr' => $stderr, - 'return_code' => proc_close( $proc ), - 'command' => $this->command, - 'cwd' => $cwd, - 'env' => $this->env, - ) ); + $return_code = proc_close( $proc ); + + $run_time = microtime( true ) - $start_time; + + if ( self::$log_run_times ) { + if ( ! isset( self::$run_times[ $this->command ] ) ) { + self::$run_times[ $this->command ] = array( 0, 0 ); + } + self::$run_times[ $this->command ][0] += $run_time; + self::$run_times[ $this->command ][1]++; + } + + return new ProcessRun( + array( + 'stdout' => $stdout, + 'stderr' => $stderr, + 'return_code' => $return_code, + 'command' => $this->command, + 'cwd' => $this->cwd, + 'env' => $this->env, + 'run_time' => $run_time, + ) + ); } /** @@ -81,7 +109,7 @@ public function run_check() { $r = $this->run(); // $r->STDERR is incorrect, but kept incorrect for backwards-compat - if ( $r->return_code || !empty( $r->STDERR ) ) { + if ( $r->return_code || ! empty( $r->STDERR ) ) { throw new \RuntimeException( $r ); } diff --git a/features/bootstrap/ProcessRun.php b/features/bootstrap/ProcessRun.php index aedc5f65d..96b4c80b6 100644 --- a/features/bootstrap/ProcessRun.php +++ b/features/bootstrap/ProcessRun.php @@ -36,6 +36,11 @@ class ProcessRun { */ public $return_code; + /** + * @var float The run time of the process. + */ + public $run_time; + /** * @var array $props Properties of executed command. */ @@ -54,6 +59,7 @@ public function __toString() { $out = "$ $this->command\n"; $out .= "$this->stdout\n$this->stderr"; $out .= "cwd: $this->cwd\n"; + $out .= "run time: $this->run_time\n"; $out .= "exit status: $this->return_code"; return $out; diff --git a/features/bootstrap/utils.php b/features/bootstrap/utils.php index 46796053e..32caac15f 100644 --- a/features/bootstrap/utils.php +++ b/features/bootstrap/utils.php @@ -28,9 +28,11 @@ function extract_from_phar( $path ) { copy( $path, $tmp_path ); - register_shutdown_function( function() use ( $tmp_path ) { - @unlink( $tmp_path ); - } ); + register_shutdown_function( + function() use ( $tmp_path ) { + @unlink( $tmp_path ); + } + ); return $tmp_path; } @@ -55,9 +57,9 @@ function load_dependencies() { } } - if ( !$has_autoload ) { + if ( ! $has_autoload ) { fputs( STDERR, "Internal error: Can't find Composer autoloader.\nTry running: composer install\n" ); - exit(3); + exit( 3 ); } } @@ -113,7 +115,7 @@ function iterator_map( $it, $fn ) { $it = new \ArrayIterator( $it ); } - if ( !method_exists( $it, 'add_transform' ) ) { + if ( ! method_exists( $it, 'add_transform' ) ) { $it = new Transform( $it ); } @@ -150,7 +152,7 @@ function find_file_upward( $files, $dir = null, $stop_check = null ) { } $parent_dir = dirname( $dir ); - if ( empty($parent_dir) || $parent_dir === $dir ) { + if ( empty( $parent_dir ) || $parent_dir === $dir ) { break; } $dir = $parent_dir; @@ -160,8 +162,9 @@ function find_file_upward( $files, $dir = null, $stop_check = null ) { function is_path_absolute( $path ) { // Windows - if ( isset($path[1]) && ':' === $path[1] ) + if ( isset( $path[1] ) && ':' === $path[1] ) { return true; + } return $path[0] === '/'; } @@ -188,9 +191,13 @@ function assoc_args_to_str( $assoc_args ) { foreach ( $assoc_args as $key => $value ) { if ( true === $value ) { $str .= " --$key"; - } elseif( is_array( $value ) ) { - foreach( $value as $_ => $v ) { - $str .= assoc_args_to_str( array( $key => $v ) ); + } elseif ( is_array( $value ) ) { + foreach ( $value as $_ => $v ) { + $str .= assoc_args_to_str( + array( + $key => $v, + ) + ); } } else { $str .= " --$key=" . escapeshellarg( $value ); @@ -205,8 +212,9 @@ function assoc_args_to_str( $assoc_args ) { * returns the final command, with the parameters escaped. */ function esc_cmd( $cmd ) { - if ( func_num_args() < 2 ) + if ( func_num_args() < 2 ) { trigger_error( 'esc_cmd() requires at least two arguments.', E_USER_WARNING ); + } $args = func_get_args(); @@ -219,15 +227,17 @@ function locate_wp_config() { static $path; if ( null === $path ) { - if ( file_exists( ABSPATH . 'wp-config.php' ) ) + if ( file_exists( ABSPATH . 'wp-config.php' ) ) { $path = ABSPATH . 'wp-config.php'; - elseif ( file_exists( ABSPATH . '../wp-config.php' ) && ! file_exists( ABSPATH . '/../wp-settings.php' ) ) + } elseif ( file_exists( ABSPATH . '../wp-config.php' ) && ! file_exists( ABSPATH . '/../wp-settings.php' ) ) { $path = ABSPATH . '../wp-config.php'; - else + } else { $path = false; + } - if ( $path ) + if ( $path ) { $path = realpath( $path ); + } } return $path; @@ -358,7 +368,7 @@ function launch_editor_for_input( $input, $filename = 'WP-CLI' ) { if ( $fp ) { fclose( $fp ); } - } while( ! $tmpfile ); + } while ( ! $tmpfile ); if ( ! $tmpfile ) { \WP_CLI::error( 'Error creating temporary file.' ); @@ -368,11 +378,12 @@ function launch_editor_for_input( $input, $filename = 'WP-CLI' ) { file_put_contents( $tmpfile, $input ); $editor = getenv( 'EDITOR' ); - if ( !$editor ) { - if ( isset( $_SERVER['OS'] ) && false !== strpos( $_SERVER['OS'], 'indows' ) ) + if ( ! $editor ) { + if ( isset( $_SERVER['OS'] ) && false !== strpos( $_SERVER['OS'], 'indows' ) ) { $editor = 'notepad'; - else + } else { $editor = 'vi'; + } } $descriptorspec = array( STDIN, STDOUT, STDERR ); @@ -386,8 +397,9 @@ function launch_editor_for_input( $input, $filename = 'WP-CLI' ) { unlink( $tmpfile ); - if ( $output === $input ) + if ( $output === $input ) { return false; + } return $output; } @@ -406,7 +418,7 @@ function mysql_host_to_cli_args( $raw_host ) { if ( is_numeric( $extra ) ) { $assoc_args['port'] = intval( $extra ); $assoc_args['protocol'] = 'tcp'; - } else if ( $extra !== '' ) { + } elseif ( $extra !== '' ) { $assoc_args['socket'] = $extra; } } else { @@ -419,8 +431,9 @@ 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 ) + if ( ! $descriptors ) { $descriptors = array( STDIN, STDOUT, STDERR ); + } if ( isset( $assoc_args['host'] ) ) { $assoc_args = array_merge( $assoc_args, mysql_host_to_cli_args( $assoc_args['host'] ) ); @@ -435,14 +448,17 @@ function run_mysql_command( $cmd, $assoc_args, $descriptors = null ) { $final_cmd = force_env_on_nix_systems( $cmd ) . assoc_args_to_str( $assoc_args ); $proc = proc_open( $final_cmd, $descriptors, $pipes ); - if ( !$proc ) - exit(1); + if ( ! $proc ) { + exit( 1 ); + } $r = proc_close( $proc ); putenv( 'MYSQL_PWD=' . $old_pass ); - if ( $r ) exit( $r ); + if ( $r ) { + exit( $r ); + } } /** @@ -451,14 +467,18 @@ function run_mysql_command( $cmd, $assoc_args, $descriptors = null ) { * IMPORTANT: Automatic HTML escaping is disabled! */ function mustache_render( $template_name, $data = array() ) { - if ( ! file_exists( $template_name ) ) + if ( ! file_exists( $template_name ) ) { $template_name = WP_CLI_ROOT . "/templates/$template_name"; + } $template = file_get_contents( $template_name ); - $m = new \Mustache_Engine( array( - 'escape' => function ( $val ) { return $val; }, - ) ); + $m = new \Mustache_Engine( + array( + 'escape' => function ( $val ) { + return $val; }, + ) + ); return $m->render( $template, $data ); } @@ -492,8 +512,9 @@ function mustache_render( $template_name, $data = array() ) { * @return cli\progress\Bar|WP_CLI\NoOp */ function make_progress_bar( $message, $count ) { - if ( \cli\Shell::isPiped() ) + if ( \cli\Shell::isPiped() ) { return new \WP_CLI\NoOp; + } return new \cli\progress\Bar( $message, $count ); } @@ -501,7 +522,7 @@ function make_progress_bar( $message, $count ) { function parse_url( $url ) { $url_parts = \parse_url( $url ); - if ( !isset( $url_parts['scheme'] ) ) { + if ( ! isset( $url_parts['scheme'] ) ) { $url_parts = parse_url( 'http://' . $url ); } @@ -514,7 +535,7 @@ function parse_url( $url ) { * @return bool */ function is_windows() { - return false !== ( $test_is_windows = getenv( 'WP_CLI_TEST_IS_WINDOWS' ) ) ? (bool) $test_is_windows : 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'; } /** @@ -563,30 +584,35 @@ 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_VENDOR_DIR . $cert_path ); + WP_CLI_VENDOR_DIR . $cert_path + ); } else { - foreach( get_vendor_paths() as $vendor_path ) { + foreach ( get_vendor_paths() as $vendor_path ) { if ( file_exists( $vendor_path . $cert_path ) ) { $options['verify'] = $vendor_path . $cert_path; break; } } - if ( empty( $options['verify'] ) ){ - WP_CLI::error( "Cannot find SSL certificate." ); + if ( empty( $options['verify'] ) ) { + WP_CLI::error( 'Cannot find SSL certificate.' ); } } try { $request = \Requests::request( $url, $headers, $data, $method, $options ); return $request; - } catch( \Requests_Exception $ex ) { + } catch ( \Requests_Exception $ex ) { + // CURLE_SSL_CACERT_BADFILE only defined for PHP >= 7. + if ( 'curlerror' !== $ex->getType() || ! in_array( curl_errno( $ex->getData() ), array( CURLE_SSL_CONNECT_ERROR, CURLE_SSL_CERTPROBLEM, 77 /*CURLE_SSL_CACERT_BADFILE*/ ), true ) ) { + \WP_CLI::error( sprintf( "Failed to get url '%s': %s.", $url, $ex->getMessage() ) ); + } // Handle SSL certificate issues gracefully - \WP_CLI::warning( $ex->getMessage() ); + \WP_CLI::warning( sprintf( "Re-trying without verify after failing to get verified url '%s' %s.", $url, $ex->getMessage() ) ); $options['verify'] = false; try { return \Requests::request( $url, $headers, $data, $method, $options ); - } catch( \Requests_Exception $ex ) { - \WP_CLI::error( $ex->getMessage() ); + } catch ( \Requests_Exception $ex ) { + \WP_CLI::error( sprintf( "Failed to get non-verified url '%s' %s.", $url, $ex->getMessage() ) ); } } } @@ -672,7 +698,7 @@ function get_named_sem_ver( $new_version, $original_version ) { if ( ! is_null( $minor ) && Semver::satisfies( $new_version, "{$major}.{$minor}.x" ) ) { return 'patch'; - } else if ( Semver::satisfies( $new_version, "{$major}.x.x" ) ) { + } elseif ( Semver::satisfies( $new_version, "{$major}.x.x" ) ) { return 'minor'; } else { return 'major'; @@ -774,9 +800,9 @@ function get_temp_dir() { * @return mixed */ function parse_ssh_url( $url, $component = -1 ) { - preg_match( '#^((docker|docker\-compose|ssh):)?(([^@:]+)@)?([^:/~]+)(:([\d]*))?((/|~)(.+))?$#', $url, $matches ); + preg_match( '#^((docker|docker\-compose|ssh|vagrant):)?(([^@:]+)@)?([^:/~]+)(:([\d]*))?((/|~)(.+))?$#', $url, $matches ); $bits = array(); - foreach( array( + foreach ( array( 2 => 'scheme', 4 => 'user', 5 => 'host', @@ -817,11 +843,7 @@ function parse_ssh_url( $url, $component = -1 ) { */ function report_batch_operation_results( $noun, $verb, $total, $successes, $failures ) { $plural_noun = $noun . 's'; - if ( in_array( $verb, array( 'reset' ), true ) ) { - $past_tense_verb = $verb; - } else { - $past_tense_verb = 'e' === substr( $verb, -1 ) ? $verb . 'd' : $verb . 'ed'; - } + $past_tense_verb = past_tense_verb( $verb ); $past_tense_verb_upper = ucfirst( $past_tense_verb ); if ( $failures ) { if ( $successes ) { @@ -849,17 +871,19 @@ function report_batch_operation_results( $noun, $verb, $total, $successes, $fail * @return array */ function parse_str_to_argv( $arguments ) { - preg_match_all ('/(?<=^|\s)([\'"]?)(.+?)(? 'reset', + ); + if ( isset( $irregular[ $verb ] ) ) { + return $irregular[ $verb ]; + } + $last = substr( $verb, -1 ); + if ( 'e' === $last ) { + $verb = substr( $verb, 0, -1 ); + } elseif ( 'y' === $last && ! preg_match( '/[aeiou]y$/', $verb ) ) { + $verb = substr( $verb, 0, -1 ) . 'i'; + } elseif ( preg_match( '/^[^aeiou]*[aeiou][^aeiouhwxy]$/', $verb ) ) { + // Rule of thumb that most (all?) one-voweled regular verbs ending in vowel + consonant (excluding "h", "w", "x", "y") double their final consonant - misses many cases (eg "submit"). + $verb .= $last; + } + return $verb . 'ed'; +} diff --git a/features/steps/given.php b/features/steps/given.php index e7ae7bf6b..f2eae113f 100644 --- a/features/steps/given.php +++ b/features/steps/given.php @@ -87,6 +87,12 @@ function ( $world ) { } ); +$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(); @@ -205,3 +211,9 @@ function ( $world ) { $world->start_php_server(); } ); + +$steps->Given( "/^a PHP built-in web server to serve '([^\s]+)'$/", + function ( $world, $subdir ) { + $world->start_php_server( $subdir ); + } +); diff --git a/features/steps/then.php b/features/steps/then.php index 887555485..aa1f3f67f 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: