Initial commit

This commit is contained in:
Daniel Bachhuber 2016-07-25 07:36:55 -07:00
commit 717ea43c89
19 changed files with 2031 additions and 0 deletions

21
.editorconfig Normal file
View file

@ -0,0 +1,21 @@
# This file is for unifying the coding style for different editors and IDEs
# editorconfig.org
# WordPress Coding Standards
# https://make.wordpress.org/core/handbook/coding-standards/
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = tab
[{.jshintrc,*.json,*.yml}]
indent_style = space
indent_size = 2
[{*.txt,wp-config-sample.php}]
end_of_line = crlf

5
.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
.DS_Store
composer.lock
wp-cli.local.yml
node_modules/
vendor/

29
.travis.yml Normal file
View file

@ -0,0 +1,29 @@
sudo: false
language: php
notifications:
email:
on_success: never
on_failure: change
branches:
only:
- master
php:
- 5.3
- 5.6
cache:
- composer
- $HOME/.composer/cache
env:
global:
- WP_CLI_BIN_DIR=/tmp/wp-cli-phar
before_script:
- bash bin/install-package-tests.sh
script: ./vendor/bin/behat

53
README.md Normal file
View file

@ -0,0 +1,53 @@
runcommand/profile
==================
Profile the performance of a request to WordPress.
[![Build Status](https://travis-ci.org/runcommand/profile.svg?branch=master)](https://travis-ci.org/runcommand/profile)
Quick links: [Using](#using) | [Installing](#installing) | [Contributing](#contributing)
## Using
~~~
wp profile
~~~
## Installing
Installing this package requires WP-CLI v0.23.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 runcommand/profile`.
## Contributing
We appreciate you taking the initiative to contribute to this project.
Contributing isnt 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.
### Reporting a bug
Think youve found a bug? Wed love for you to help us get it fixed.
Before you create a new issue, you should [search existing issues](https://github.com/runcommand/profile/issues?q=label%3Abug%20) to see if theres an existing resolution to it, or if its already been fixed in a newer version.
Once youve done a bit of searching and discovered there isnt an open or fixed issue for your bug, please [create a new issue](https://github.com/runcommand/profile/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.
### Creating a pull request
Want to contribute a new feature? Please first [open a new issue](https://github.com/runcommand/profile/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:
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/).
*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.*

40
bin/install-package-tests.sh Executable file
View file

@ -0,0 +1,40 @@
#!/usr/bin/env bash
set -ex
PACKAGE_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )"/../ && pwd )"
download() {
if [ `which curl` ]; then
curl -s "$1" > "$2";
elif [ `which wget` ]; then
wget -nv -O "$2" "$1"
fi
}
install_wp_cli() {
# the Behat test suite will pick up the executable found in $WP_CLI_BIN_DIR
mkdir -p $WP_CLI_BIN_DIR
download https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli-nightly.phar $WP_CLI_BIN_DIR/wp
chmod +x $WP_CLI_BIN_DIR/wp
}
download_behat() {
cd $PACKAGE_DIR
download https://getcomposer.org/installer installer
php installer
php composer.phar require --dev behat/behat='~2.5'
}
install_db() {
mysql -e 'CREATE DATABASE IF NOT EXISTS wp_cli_test;' -uroot
mysql -e 'GRANT ALL PRIVILEGES ON wp_cli_test.* TO "wp_cli_test"@"localhost" IDENTIFIED BY "password1"' -uroot
}
install_wp_cli
download_behat
install_db

6
command.php Normal file
View file

@ -0,0 +1,6 @@
<?php
if ( class_exists( 'WP_CLI' ) ) {
require_once dirname( __FILE__ ) . '/inc/class-profile-command.php';
WP_CLI::add_command( 'profile', 'Profile_Command' );
}

22
composer.json Normal file
View file

@ -0,0 +1,22 @@
{
"name": "runcommand/profile",
"description": "Profile the performance of a request to WordPress.",
"homepage": "https://runcommand.io/wp/profile/",
"license": "MIT",
"authors": [],
"minimum-stability": "dev",
"autoload": {
"files": [ "command.php" ]
},
"require": {
"wp-cli/wp-cli": "~0.23.0"
},
"require-dev": {
"behat/behat": "~2.5"
},
"extras": {
"commands": [
"profile"
]
}
}

View file

@ -0,0 +1,329 @@
<?php
use Behat\Behat\Context\ClosuredContextInterface,
Behat\Behat\Context\TranslatedContextInterface,
Behat\Behat\Context\BehatContext,
Behat\Behat\Event\SuiteEvent;
use \WP_CLI\Process;
use \WP_CLI\Utils;
// Inside a community package
if ( file_exists( __DIR__ . '/utils.php' ) ) {
require_once __DIR__ . '/utils.php';
require_once __DIR__ . '/Process.php';
$project_composer = dirname( dirname( dirname( __FILE__ ) ) ) . '/composer.json';
if ( file_exists( $project_composer ) ) {
$composer = json_decode( file_get_contents( $project_composer ) );
if ( ! empty( $composer->autoload->files ) ) {
$contents = 'require:' . PHP_EOL;
foreach( $composer->autoload->files as $file ) {
$contents .= ' - ' . dirname( dirname( dirname( __FILE__ ) ) ) . '/' . $file;
}
@mkdir( sys_get_temp_dir() . '/wp-cli-package-test/' );
$project_config = sys_get_temp_dir() . '/wp-cli-package-test/config.yml';
file_put_contents( $project_config, $contents );
putenv( 'WP_CLI_CONFIG_PATH=' . $project_config );
}
}
// Inside WP-CLI
} else {
require_once __DIR__ . '/../../php/utils.php';
require_once __DIR__ . '/../../php/WP_CLI/Process.php';
require_once __DIR__ . '/../../vendor/autoload.php';
}
/**
* Features context.
*/
class FeatureContext extends BehatContext implements ClosuredContextInterface {
private static $cache_dir, $suite_cache_dir;
private static $db_settings = array(
'dbname' => 'wp_cli_test',
'dbuser' => 'wp_cli_test',
'dbpass' => 'password1',
'dbhost' => '127.0.0.1',
);
private $running_procs = array();
public $variables = array();
/**
* 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
$bin_dir = getenv( 'WP_CLI_BIN_DIR' ) ?: realpath( __DIR__ . "/../../bin" );
$env = array(
'PATH' => $bin_dir . ':' . getenv( 'PATH' ),
'BEHAT_RUN' => 1,
'HOME' => '/tmp/wp-cli-home',
);
if ( $config_path = getenv( 'WP_CLI_CONFIG_PATH' ) ) {
$env['WP_CLI_CONFIG_PATH'] = $config_path;
}
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
private static function cache_wp_files() {
self::$cache_dir = sys_get_temp_dir() . '/wp-cli-test core-download-cache';
if ( is_readable( self::$cache_dir . '/wp-config-sample.php' ) )
return;
$cmd = Utils\esc_cmd( 'wp core download --force --path=%s', self::$cache_dir );
Process::create( $cmd, null, self::get_process_env_variables() )->run_check();
}
/**
* @BeforeSuite
*/
public static function prepare( SuiteEvent $event ) {
$result = Process::create( 'wp cli info', null, self::get_process_env_variables() )->run_check();
echo PHP_EOL;
echo $result->stdout;
echo PHP_EOL;
self::cache_wp_files();
}
/**
* @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();
}
}
/**
* @BeforeScenario
*/
public function beforeScenario( $event ) {
$this->variables['SRC_DIR'] = realpath( __DIR__ . '/../..' );
}
/**
* @AfterScenario
*/
public function afterScenario( $event ) {
if ( isset( $this->variables['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();
}
}
foreach ( $this->running_procs as $proc ) {
self::terminate_proc( $proc );
}
}
/**
* Terminate a process and any of its children.
*/
private static function terminate_proc( $proc ) {
$status = proc_get_status( $proc );
$master_pid = $status['pid'];
$output = `ps -o ppid,pid,command | grep $master_pid`;
foreach ( explode( PHP_EOL, $output ) as $line ) {
if ( preg_match( '/^\s*(\d+)\s+(\d+)/', $line, $matches ) ) {
$parent = $matches[1];
$child = $matches[2];
if ( $parent == $master_pid ) {
if ( ! posix_kill( (int) $child, 9 ) ) {
throw new RuntimeException( posix_strerror( posix_get_last_error() ) );
}
}
}
}
if ( ! posix_kill( (int) $master_pid, 9 ) ) {
throw new RuntimeException( posix_strerror( posix_get_last_error() ) );
}
}
public static function create_cache_dir() {
self::$suite_cache_dir = sys_get_temp_dir() . '/' . uniqid( "wp-cli-test-suite-cache-", TRUE );
mkdir( self::$suite_cache_dir );
return self::$suite_cache_dir;
}
/**
* Initializes context.
* Every scenario gets it's own context object.
*
* @param array $parameters context parameters (set them up through behat.yml)
*/
public function __construct( array $parameters ) {
$this->drop_db();
$this->set_cache_dir();
$this->variables['CORE_CONFIG_SETTINGS'] = Utils\assoc_args_to_str( self::$db_settings );
}
public function getStepDefinitionResources() {
return glob( __DIR__ . '/../steps/*.php' );
}
public function getHookDefinitionResources() {
return array();
}
public function replace_variables( $str ) {
return preg_replace_callback( '/\{([A-Z_]+)\}/', array( $this, '_replace_var' ), $str );
}
private function _replace_var( $matches ) {
$cmd = $matches[0];
foreach ( array_slice( $matches, 1 ) as $key ) {
$cmd = str_replace( '{' . $key . '}', $this->variables[ $key ], $cmd );
}
return $cmd;
}
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 );
mkdir( $this->variables['RUN_DIR'] );
}
}
public function build_phar( $version = 'same' ) {
$this->variables['PHAR_PATH'] = $this->variables['RUN_DIR'] . '/' . uniqid( "wp-cli-build-", TRUE ) . '.phar';
$this->proc( Utils\esc_cmd(
'php -dphar.readonly=0 %1$s %2$s --version=%3$s && chmod +x %2$s',
__DIR__ . '/../../utils/make-phar.php',
$this->variables['PHAR_PATH'],
$version
) )->run_check();
}
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();
$this->variables['CACHE_DIR'] = $path;
}
private static function run_sql( $sql ) {
Utils\run_mysql_command( 'mysql --no-defaults', array(
'execute' => $sql,
'host' => self::$db_settings['dbhost'],
'user' => self::$db_settings['dbuser'],
'pass' => self::$db_settings['dbpass'],
) );
}
public function create_db() {
$dbname = self::$db_settings['dbname'];
self::run_sql( "CREATE DATABASE IF NOT EXISTS $dbname" );
}
public function drop_db() {
$dbname = self::$db_settings['dbname'];
self::run_sql( "DROP DATABASE IF EXISTS $dbname" );
}
public function proc( $command, $assoc_args = array(), $path = '' ) {
if ( !empty( $assoc_args ) )
$command .= Utils\assoc_args_to_str( $assoc_args );
$env = self::get_process_env_variables();
if ( isset( $this->variables['SUITE_CACHE_DIR'] ) ) {
$env['WP_CLI_CACHE_DIR'] = $this->variables['SUITE_CACHE_DIR'];
}
if ( isset( $this->variables['RUN_DIR'] ) ) {
$cwd = "{$this->variables['RUN_DIR']}/{$path}";
} else {
$cwd = null;
}
return Process::create( $command, $cwd, $env );
}
/**
* Start a background process. Will automatically be closed when the tests finish.
*/
public function background_proc( $cmd ) {
$descriptors = array(
0 => STDIN,
1 => array( 'pipe', 'w' ),
2 => array( 'pipe', 'w' ),
);
$proc = proc_open( $cmd, $descriptors, $pipes, $this->variables['RUN_DIR'], self::get_process_env_variables() );
sleep(1);
$status = proc_get_status( $proc );
if ( !$status['running'] ) {
throw new RuntimeException( stream_get_contents( $pipes[2] ) );
} else {
$this->running_procs[] = $proc;
}
}
public function move_files( $src, $dest ) {
rename( $this->variables['RUN_DIR'] . "/$src", $this->variables['RUN_DIR'] . "/$dest" );
}
public function add_line_to_wp_config( &$wp_config_code, $line ) {
$token = "/* That's all, stop editing!";
$wp_config_code = str_replace( $token, "$line\n\n$token", $wp_config_code );
}
public function download_wp( $subdir = '' ) {
$dest_dir = $this->variables['RUN_DIR'] . "/$subdir";
if ( $subdir ) {
mkdir( $dest_dir );
}
$this->proc( Utils\esc_cmd( "cp -r %s/* %s", self::$cache_dir, $dest_dir ) )->run_check();
// 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 = '' ) {
$params = self::$db_settings;
$params['dbprefix'] = $subdir ?: 'wp_';
$params['skip-salts'] = true;
$this->proc( 'wp core config', $params, $subdir )->run_check();
}
public function install_wp( $subdir = '' ) {
$this->create_db();
$this->create_run_dir();
$this->download_wp( $subdir );
$this->create_config( $subdir );
$install_args = array(
'url' => 'http://example.com',
'title' => 'WP CLI Site',
'admin_user' => 'admin',
'admin_email' => 'admin@example.com',
'admin_password' => 'password1'
);
$this->proc( 'wp core install', $install_args, $subdir )->run_check();
}
}

View file

@ -0,0 +1,105 @@
<?php
namespace WP_CLI;
/**
* Run a system process, and learn what happened.
*/
class Process {
/**
* @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.
*/
public static function create( $command, $cwd = null, $env = array() ) {
$proc = new self;
$proc->command = $command;
$proc->cwd = $cwd;
$proc->env = $env;
return $proc;
}
private $command, $cwd, $env;
private function __construct() {}
/**
* Run the command.
*
* @return ProcessRun
*/
public function run() {
$cwd = $this->cwd;
$descriptors = array(
0 => STDIN,
1 => array( 'pipe', 'w' ),
2 => array( 'pipe', 'w' ),
);
$proc = proc_open( $this->command, $descriptors, $pipes, $cwd, $this->env );
$stdout = stream_get_contents( $pipes[1] );
fclose( $pipes[1] );
$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
) );
}
/**
* Run the command, but throw an Exception on error.
*
* @return ProcessRun
*/
public function run_check() {
$r = $this->run();
if ( $r->return_code || !empty( $r->STDERR ) ) {
throw new \RuntimeException( $r );
}
return $r;
}
}
/**
* Results of an executed command.
*/
class ProcessRun {
/**
* @var array $props Properties of executed command.
*/
public function __construct( $props ) {
foreach ( $props as $key => $value ) {
$this->$key = $value;
}
}
/**
* Return properties of executed command as a string.
*
* @return string
*/
public function __toString() {
$out = "$ $this->command\n";
$out .= "$this->stdout\n$this->stderr";
$out .= "cwd: $this->cwd\n";
$out .= "exit status: $this->return_code";
return $out;
}
}

View file

@ -0,0 +1,188 @@
<?php
// Utility functions used by Behat steps
function assertEquals( $expected, $actual ) {
if ( $expected != $actual ) {
throw new Exception( "Actual value: " . var_export( $actual, true ) );
}
}
function assertNumeric( $actual ) {
if ( !is_numeric( $actual ) ) {
throw new Exception( "Actual value: " . var_export( $actual, true ) );
}
}
function assertNotNumeric( $actual ) {
if ( is_numeric( $actual ) ) {
throw new Exception( "Actual value: " . var_export( $actual, true ) );
}
}
function checkString( $output, $expected, $action, $message = false ) {
switch ( $action ) {
case 'be':
$r = $expected === rtrim( $output, "\n" );
break;
case 'contain':
$r = false !== strpos( $output, $expected );
break;
case 'not contain':
$r = false === strpos( $output, $expected );
break;
default:
throw new Behat\Behat\Exception\PendingException();
}
if ( !$r ) {
if ( false === $message )
$message = $output;
throw new Exception( $message );
}
}
function compareTables( $expected_rows, $actual_rows, $output ) {
// the first row is the header and must be present
if ( $expected_rows[0] != $actual_rows[0] ) {
throw new \Exception( $output );
}
unset( $actual_rows[0] );
unset( $expected_rows[0] );
$missing_rows = array_diff( $expected_rows, $actual_rows );
if ( !empty( $missing_rows ) ) {
throw new \Exception( $output );
}
}
function compareContents( $expected, $actual ) {
if ( gettype( $expected ) != gettype( $actual ) ) {
return false;
}
if ( is_object( $expected ) ) {
foreach ( get_object_vars( $expected ) as $name => $value ) {
if ( ! compareContents( $value, $actual->$name ) )
return false;
}
} else if ( is_array( $expected ) ) {
foreach ( $expected as $key => $value ) {
if ( ! compareContents( $value, $actual[$key] ) )
return false;
}
} else {
return $expected === $actual;
}
return true;
}
/**
* Compare two strings containing JSON to ensure that @a $actualJson contains at
* least what the JSON string @a $expectedJson contains.
*
* @return whether or not @a $actualJson contains @a $expectedJson
* @retval true @a $actualJson contains @a $expectedJson
* @retval false @a $actualJson does not contain @a $expectedJson
*
* @param[in] $actualJson the JSON string to be tested
* @param[in] $expectedJson the expected JSON string
*
* Examples:
* expected: {'a':1,'array':[1,3,5]}
*
* 1 )
* actual: {'a':1,'b':2,'c':3,'array':[1,2,3,4,5]}
* return: true
*
* 2 )
* actual: {'b':2,'c':3,'array':[1,2,3,4,5]}
* return: false
* element 'a' is missing from the root object
*
* 3 )
* actual: {'a':0,'b':2,'c':3,'array':[1,2,3,4,5]}
* return: false
* the value of element 'a' is not 1
*
* 4 )
* actual: {'a':1,'b':2,'c':3,'array':[1,2,4,5]}
* return: false
* the contents of 'array' does not include 3
*/
function checkThatJsonStringContainsJsonString( $actualJson, $expectedJson ) {
$actualValue = json_decode( $actualJson );
$expectedValue = json_decode( $expectedJson );
if ( !$actualValue ) {
return false;
}
return compareContents( $expectedValue, $actualValue );
}
/**
* Compare two strings to confirm $actualCSV contains $expectedCSV
* Both strings are expected to have headers for their CSVs.
* $actualCSV must match all data rows in $expectedCSV
*
* @param string A CSV string
* @param array A nested array of values
* @return bool Whether $actualCSV contains $expectedCSV
*/
function checkThatCsvStringContainsValues( $actualCSV, $expectedCSV ) {
$actualCSV = array_map( 'str_getcsv', explode( PHP_EOL, $actualCSV ) );
if ( empty( $actualCSV ) )
return false;
// Each sample must have headers
$actualHeaders = array_values( array_shift( $actualCSV ) );
$expectedHeaders = array_values( array_shift( $expectedCSV ) );
// Each expectedCSV must exist somewhere in actualCSV in the proper column
$expectedResult = 0;
foreach ( $expectedCSV as $expected_row ) {
$expected_row = array_combine( $expectedHeaders, $expected_row );
foreach ( $actualCSV as $actual_row ) {
if ( count( $actualHeaders ) != count( $actual_row ) )
continue;
$actual_row = array_intersect_key( array_combine( $actualHeaders, $actual_row ), $expected_row );
if ( $actual_row == $expected_row )
$expectedResult++;
}
}
return $expectedResult >= count( $expectedCSV );
}
/**
* Compare two strings containing YAML to ensure that @a $actualYaml contains at
* least what the YAML string @a $expectedYaml contains.
*
* @return whether or not @a $actualYaml contains @a $expectedJson
* @retval true @a $actualYaml contains @a $expectedJson
* @retval false @a $actualYaml does not contain @a $expectedJson
*
* @param[in] $actualYaml the YAML string to be tested
* @param[in] $expectedYaml the expected YAML string
*/
function checkThatYamlStringContainsYamlString( $actualYaml, $expectedYaml ) {
$actualValue = spyc_load( $actualYaml );
$expectedValue = spyc_load( $expectedYaml );
if ( !$actualValue ) {
return false;
}
return compareContents( $expectedValue, $actualValue );
}

View file

@ -0,0 +1,757 @@
<?php
// Utilities that do NOT depend on WordPress code.
namespace WP_CLI\Utils;
use \Composer\Semver\Comparator;
use \Composer\Semver\Semver;
use \WP_CLI\Dispatcher;
use \WP_CLI\Iterators\Transform;
function inside_phar() {
return 0 === strpos( WP_CLI_ROOT, 'phar://' );
}
// Files that need to be read by external programs have to be extracted from the Phar archive.
function extract_from_phar( $path ) {
if ( ! inside_phar() ) {
return $path;
}
$fname = basename( $path );
$tmp_path = get_temp_dir() . "wp-cli-$fname";
copy( $path, $tmp_path );
register_shutdown_function( function() use ( $tmp_path ) {
@unlink( $tmp_path );
} );
return $tmp_path;
}
function load_dependencies() {
if ( inside_phar() ) {
require WP_CLI_ROOT . '/vendor/autoload.php';
return;
}
$has_autoload = false;
foreach ( get_vendor_paths() as $vendor_path ) {
if ( file_exists( $vendor_path . '/autoload.php' ) ) {
require $vendor_path . '/autoload.php';
$has_autoload = true;
break;
}
}
if ( !$has_autoload ) {
fputs( STDERR, "Internal error: Can't find Composer autoloader.\nTry running: composer install\n" );
exit(3);
}
}
function get_vendor_paths() {
$vendor_paths = array(
WP_CLI_ROOT . '/../../../vendor', // part of a larger project / installed via Composer (preferred)
WP_CLI_ROOT . '/vendor', // top-level project / installed as Git clone
);
$maybe_composer_json = WP_CLI_ROOT . '/../../../composer.json';
if ( file_exists( $maybe_composer_json ) && is_readable( $maybe_composer_json ) ) {
$composer = json_decode( file_get_contents( $maybe_composer_json ) );
if ( ! empty( $composer->config ) && ! empty( $composer->config->{'vendor-dir'} ) ) {
array_unshift( $vendor_paths, WP_CLI_ROOT . '/../../../' . $composer->config->{'vendor-dir'} );
}
}
return $vendor_paths;
}
// Using require() directly inside a class grants access to private methods to the loaded code
function load_file( $path ) {
require_once $path;
}
function load_command( $name ) {
$path = WP_CLI_ROOT . "/php/commands/$name.php";
if ( is_readable( $path ) ) {
include_once $path;
}
}
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.
*
* Example:
*
* $arr = array('Football', 'Socker');
*
* $it = iterator_map($arr, 'strtolower', function($val) {
* return str_replace('foo', 'bar', $val);
* });
*
* foreach ( $it as $val ) {
* var_dump($val);
* }
*
* @param array|object Either a plain array or another iterator
* @param callback The function to apply to an element
* @return object An iterator that applies the given callback(s)
*/
function iterator_map( $it, $fn ) {
if ( is_array( $it ) ) {
$it = new \ArrayIterator( $it );
}
if ( !method_exists( $it, 'add_transform' ) ) {
$it = new Transform( $it );
}
foreach ( array_slice( func_get_args(), 1 ) as $fn ) {
$it->add_transform( $fn );
}
return $it;
}
/**
* Search for file by walking up the directory tree until the first file is found or until $stop_check($dir) returns true
* @param string|array The files (or file) to search for
* @param string|null The directory to start searching from; defaults to CWD
* @param callable Function which is passed the current dir each time a directory level is traversed
* @return null|string Null if the file was not found
*/
function find_file_upward( $files, $dir = null, $stop_check = null ) {
$files = (array) $files;
if ( is_null( $dir ) ) {
$dir = getcwd();
}
while ( @is_readable( $dir ) ) {
// Stop walking up when the supplied callable returns true being passed the $dir
if ( is_callable( $stop_check ) && call_user_func( $stop_check, $dir ) ) {
return null;
}
foreach ( $files as $file ) {
$path = $dir . DIRECTORY_SEPARATOR . $file;
if ( file_exists( $path ) ) {
return $path;
}
}
$parent_dir = dirname( $dir );
if ( empty($parent_dir) || $parent_dir === $dir ) {
break;
}
$dir = $parent_dir;
}
return null;
}
function is_path_absolute( $path ) {
// Windows
if ( isset($path[1]) && ':' === $path[1] )
return true;
return $path[0] === '/';
}
/**
* Composes positional arguments into a command string.
*
* @param array
* @return string
*/
function args_to_str( $args ) {
return ' ' . implode( ' ', array_map( 'escapeshellarg', $args ) );
}
/**
* Composes associative arguments into a command string.
*
* @param array
* @return string
*/
function assoc_args_to_str( $assoc_args ) {
$str = '';
foreach ( $assoc_args as $key => $value ) {
if ( true === $value )
$str .= " --$key";
else
$str .= " --$key=" . escapeshellarg( $value );
}
return $str;
}
/**
* Given a template string and an arbitrary number of arguments,
* returns the final command, with the parameters escaped.
*/
function esc_cmd( $cmd ) {
if ( func_num_args() < 2 )
trigger_error( 'esc_cmd() requires at least two arguments.', E_USER_WARNING );
$args = func_get_args();
$cmd = array_shift( $args );
return vsprintf( $cmd, array_map( 'escapeshellarg', $args ) );
}
function locate_wp_config() {
static $path;
if ( null === $path ) {
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' ) )
$path = ABSPATH . '../wp-config.php';
else
$path = false;
if ( $path )
$path = realpath( $path );
}
return $path;
}
function wp_version_compare( $since, $operator ) {
return version_compare( str_replace( array( '-src' ), '', $GLOBALS['wp_version'] ), $since, $operator );
}
/**
* Render a collection of items as an ASCII table, JSON, CSV, YAML, list of ids, or count.
*
* Given a collection of items with a consistent data structure:
*
* ```
* $items = array(
* array(
* 'key' => 'foo',
* 'value' => 'bar',
* )
* );
* ```
*
* Render `$items` as an ASCII table:
*
* ```
* WP_CLI\Utils\format_items( 'table', $items, array( 'key', 'value' ) );
*
* # +-----+-------+
* # | key | value |
* # +-----+-------+
* # | foo | bar |
* # +-----+-------+
* ```
*
* Or render `$items` as YAML:
*
* ```
* WP_CLI\Utils\format_items( 'yaml', $items, array( 'key', 'value' ) );
*
* # ---
* # -
* # key: foo
* # value: bar
* ```
*
* @access public
* @category Output
*
* @param string $format Format to use: 'table', 'json', 'csv', 'yaml', 'ids', 'count'
* @param array $items An array of items to output.
* @param array|string $fields Named fields for each item of data. Can be array or comma-separated list.
* @return null
*/
function format_items( $format, $items, $fields ) {
$assoc_args = compact( 'format', 'fields' );
$formatter = new \WP_CLI\Formatter( $assoc_args );
$formatter->display_items( $items );
}
/**
* Write data as CSV to a given file.
*
* @access public
*
* @param resource $fd File descriptor
* @param array $rows Array of rows to output
* @param array $headers List of CSV columns (optional)
*/
function write_csv( $fd, $rows, $headers = array() ) {
if ( ! empty( $headers ) ) {
fputcsv( $fd, $headers );
}
foreach ( $rows as $row ) {
if ( ! empty( $headers ) ) {
$row = pick_fields( $row, $headers );
}
fputcsv( $fd, array_values( $row ) );
}
}
/**
* Pick fields from an associative array or object.
*
* @param array|object Associative array or object to pick fields from
* @param array List of fields to pick
* @return array
*/
function pick_fields( $item, $fields ) {
$item = (object) $item;
$values = array();
foreach ( $fields as $field ) {
$values[ $field ] = isset( $item->$field ) ? $item->$field : null;
}
return $values;
}
/**
* Launch system's $EDITOR for the user to edit some text.
*
* @access public
* @category Input
*
* @param string $content Some form of text to edit (e.g. post content)
* @return string|bool Edited text, if file is saved from editor; false, if no change to file.
*/
function launch_editor_for_input( $input, $filename = 'WP-CLI' ) {
$tmpdir = get_temp_dir();
do {
$tmpfile = basename( $filename );
$tmpfile = preg_replace( '|\.[^.]*$|', '', $tmpfile );
$tmpfile .= '-' . substr( md5( rand() ), 0, 6 );
$tmpfile = $tmpdir . $tmpfile . '.tmp';
$fp = @fopen( $tmpfile, 'x' );
if ( ! $fp && is_writable( $tmpdir ) && file_exists( $tmpfile ) ) {
$tmpfile = '';
continue;
}
if ( $fp ) {
fclose( $fp );
}
} while( ! $tmpfile );
if ( ! $tmpfile ) {
\WP_CLI::error( 'Error creating temporary file.' );
}
$output = '';
file_put_contents( $tmpfile, $input );
$editor = getenv( 'EDITOR' );
if ( !$editor ) {
if ( isset( $_SERVER['OS'] ) && false !== strpos( $_SERVER['OS'], 'indows' ) )
$editor = 'notepad';
else
$editor = 'vi';
}
$descriptorspec = array( STDIN, STDOUT, STDERR );
$process = proc_open( "$editor " . escapeshellarg( $tmpfile ), $descriptorspec, $pipes );
$r = proc_close( $process );
if ( $r ) {
exit( $r );
}
$output = file_get_contents( $tmpfile );
unlink( $tmpfile );
if ( $output === $input )
return false;
return $output;
}
/**
* @param string MySQL host string, as defined in wp-config.php
* @return array
*/
function mysql_host_to_cli_args( $raw_host ) {
$assoc_args = array();
$host_parts = explode( ':', $raw_host );
if ( count( $host_parts ) == 2 ) {
list( $assoc_args['host'], $extra ) = $host_parts;
$extra = trim( $extra );
if ( is_numeric( $extra ) ) {
$assoc_args['port'] = intval( $extra );
$assoc_args['protocol'] = 'tcp';
} else if ( $extra !== '' ) {
$assoc_args['socket'] = $extra;
}
} else {
$assoc_args['host'] = $raw_host;
}
return $assoc_args;
}
function run_mysql_command( $cmd, $assoc_args, $descriptors = null ) {
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'] ) );
}
$pass = $assoc_args['pass'];
unset( $assoc_args['pass'] );
$old_pass = getenv( 'MYSQL_PWD' );
putenv( 'MYSQL_PWD=' . $pass );
$final_cmd = $cmd . assoc_args_to_str( $assoc_args );
$proc = proc_open( $final_cmd, $descriptors, $pipes );
if ( !$proc )
exit(1);
$r = proc_close( $proc );
putenv( 'MYSQL_PWD=' . $old_pass );
if ( $r ) exit( $r );
}
/**
* Render PHP or other types of files using Mustache templates.
*
* IMPORTANT: Automatic HTML escaping is disabled!
*/
function mustache_render( $template_name, $data = array() ) {
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; }
) );
return $m->render( $template, $data );
}
/**
* Create a progress bar to display percent completion of a given operation.
*
* Progress bar is written to STDOUT, and disabled when command is piped. Progress
* advances with `$progress->tick()`, and completes with `$progress->finish()`.
* Process bar also indicates elapsed time and expected total time.
*
* ```
* # `wp user generate` ticks progress bar each time a new user is created.
* #
* # $ wp user generate --count=500
* # Generating users 22 % [=======> ] 0:05 / 0:23
*
* $progress = \WP_CLI\Utils\make_progress_bar( 'Generating users', $count );
* for ( $i = 0; $i < $count; $i++ ) {
* // uses wp_insert_user() to insert the user
* $progress->tick();
* }
* $progress->finish();
* ```
*
* @access public
* @category Output
*
* @param string $message Text to display before the progress bar.
* @param integer $count Total number of ticks to be performed.
* @return cli\progress\Bar|WP_CLI\NoOp
*/
function make_progress_bar( $message, $count ) {
if ( \cli\Shell::isPiped() )
return new \WP_CLI\NoOp;
return new \cli\progress\Bar( $message, $count );
}
function parse_url( $url ) {
$url_parts = \parse_url( $url );
if ( !isset( $url_parts['scheme'] ) ) {
$url_parts = parse_url( 'http://' . $url );
}
return $url_parts;
}
/**
* Check if we're running in a Windows environment (cmd.exe).
*/
function is_windows() {
return strtoupper(substr(PHP_OS, 0, 3)) === 'WIN';
}
/**
* Replace magic constants in some PHP source code.
*
* @param string $source The PHP code to manipulate.
* @param string $path The path to use instead of the magic constants
*/
function replace_path_consts( $source, $path ) {
$replacements = array(
'__FILE__' => "'$path'",
'__DIR__' => "'" . dirname( $path ) . "'"
);
$old = array_keys( $replacements );
$new = array_values( $replacements );
return str_replace( $old, $new, $source );
}
/**
* Make a HTTP request to a remote URL.
*
* Wraps the Requests HTTP library to ensure every request includes a cert.
*
* ```
* # `wp core download` verifies the hash for a downloaded WordPress archive
*
* $md5_response = Utils\http_request( 'GET', $download_url . '.md5' );
* if ( 20 != substr( $md5_response->status_code, 0, 2 ) ) {
* WP_CLI::error( "Couldn't access md5 hash for release (HTTP code {$response->status_code})" );
* }
* ```
*
* @access public
*
* @param string $method HTTP method (GET, POST, DELETE, etc.)
* @param string $url URL to make the HTTP request to.
* @param array $headers Add specific headers to the request.
* @param array $options
* @return object
*/
function http_request( $method, $url, $data = null, $headers = array(), $options = array() ) {
$cert_path = '/rmccue/requests/library/Requests/Transport/cacert.pem';
if ( inside_phar() ) {
// cURL can't read Phar archives
$options['verify'] = extract_from_phar(
WP_CLI_ROOT . '/vendor' . $cert_path );
} else {
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_log( "Cannot find SSL certificate." );
}
}
try {
$request = \Requests::request( $url, $headers, $data, $method, $options );
return $request;
} catch( \Requests_Exception $ex ) {
// Handle SSL certificate issues gracefully
\WP_CLI::warning( $ex->getMessage() );
$options['verify'] = false;
try {
return \Requests::request( $url, $headers, $data, $method, $options );
} catch( \Requests_Exception $ex ) {
\WP_CLI::error( $ex->getMessage() );
}
}
}
/**
* Increments a version string using the "x.y.z-pre" format
*
* Can increment the major, minor or patch number by one
* If $new_version == "same" the version string is not changed
* If $new_version is not a known keyword, it will be used as the new version string directly
*
* @param string $current_version
* @param string $new_version
* @return string
*/
function increment_version( $current_version, $new_version ) {
// split version assuming the format is x.y.z-pre
$current_version = explode( '-', $current_version, 2 );
$current_version[0] = explode( '.', $current_version[0] );
switch ( $new_version ) {
case 'same':
// do nothing
break;
case 'patch':
$current_version[0][2]++;
$current_version = array( $current_version[0] ); // drop possible pre-release info
break;
case 'minor':
$current_version[0][1]++;
$current_version[0][2] = 0;
$current_version = array( $current_version[0] ); // drop possible pre-release info
break;
case 'major':
$current_version[0][0]++;
$current_version[0][1] = 0;
$current_version[0][2] = 0;
$current_version = array( $current_version[0] ); // drop possible pre-release info
break;
default: // not a keyword
$current_version = array( array( $new_version ) );
break;
}
// reconstruct version string
$current_version[0] = implode( '.', $current_version[0] );
$current_version = implode( '-', $current_version );
return $current_version;
}
/**
* Compare two version strings to get the named semantic version.
*
* @access public
*
* @param string $new_version
* @param string $original_version
* @return string $name 'major', 'minor', 'patch'
*/
function get_named_sem_ver( $new_version, $original_version ) {
if ( ! Comparator::greaterThan( $new_version, $original_version ) ) {
return '';
}
$parts = explode( '-', $original_version );
list( $major, $minor, $patch ) = explode( '.', $parts[0] );
if ( Semver::satisfies( $new_version, "{$major}.{$minor}.x" ) ) {
return 'patch';
} else if ( Semver::satisfies( $new_version, "{$major}.x.x" ) ) {
return 'minor';
} else {
return 'major';
}
}
/**
* Return the flag value or, if it's not set, the $default value.
*
* Because flags can be negated (e.g. --no-quiet to negate --quiet), this
* function provides a safer alternative to using
* `isset( $assoc_args['quiet'] )` or similar.
*
* @access public
* @category Input
*
* @param array $assoc_args Arguments array.
* @param string $flag Flag to get the value.
* @param mixed $default Default value for the flag. Default: NULL
* @return mixed
*/
function get_flag_value( $assoc_args, $flag, $default = null ) {
return isset( $assoc_args[ $flag ] ) ? $assoc_args[ $flag ] : $default;
}
/**
* Get the system's temp directory. Warns user if it isn't writable.
*
* @access public
* @category System
*
* @return string
*/
function get_temp_dir() {
static $temp = '';
$trailingslashit = function( $path ) {
return rtrim( $path ) . '/';
};
if ( $temp )
return $trailingslashit( $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' );
} else {
$temp = '/tmp/';
}
if ( ! @is_writable( $temp ) ) {
WP_CLI::warning( "Temp directory isn't writable: {$temp}" );
}
return $trailingslashit( $temp );
}
/**
* Parse a SSH url for its host, port, and path.
*
* Similar to parse_url(), but adds support for defined SSH aliases.
*
* ```
* host OR host/path/to/wordpress OR host:port/path/to/wordpress
* ```
*
* @access public
*
* @return mixed
*/
function parse_ssh_url( $url, $component = -1 ) {
preg_match( '#^([^:/~]+)(:([\d]+))?((/|~)(.+))?$#', $url, $matches );
$bits = array();
foreach( array(
1 => 'host',
3 => 'port',
4 => 'path',
) as $i => $key ) {
if ( ! empty( $matches[ $i ] ) ) {
$bits[ $key ] = $matches[ $i ];
}
}
switch ( $component ) {
case PHP_URL_HOST:
return isset( $bits['host'] ) ? $bits['host'] : null;
case PHP_URL_PATH:
return isset( $bits['path'] ) ? $bits['path'] : null;
case PHP_URL_PORT:
return isset( $bits['port'] ) ? $bits['port'] : null;
default:
return $bits;
}
}

View file

@ -0,0 +1,6 @@
<?php
function wp_mail() {
// do nothing
}

View file

@ -0,0 +1,10 @@
Feature: Test that WP-CLI loads.
Scenario: WP-CLI loads for your tests
Given a WP install
When I run `wp eval 'echo "Hello world.";'`
Then STDOUT should contain:
"""
Hello world.
"""

157
features/steps/given.php Normal file
View file

@ -0,0 +1,157 @@
<?php
use Behat\Gherkin\Node\PyStringNode,
Behat\Gherkin\Node\TableNode,
WP_CLI\Process;
$steps->Given( '/^an empty directory$/',
function ( $world ) {
$world->create_run_dir();
}
);
$steps->Given( '/^an empty cache/',
function ( $world ) {
$world->variables['SUITE_CACHE_DIR'] = FeatureContext::create_cache_dir();
}
);
$steps->Given( '/^an? ([^\s]+) file:$/',
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();
file_put_contents( $full_path, $content );
}
);
$steps->Given( '/^WP files$/',
function ( $world ) {
$world->download_wp();
}
);
$steps->Given( '/^wp-config\.php$/',
function ( $world ) {
$world->create_config();
}
);
$steps->Given( '/^a database$/',
function ( $world ) {
$world->create_db();
}
);
$steps->Given( '/^a WP install$/',
function ( $world ) {
$world->install_wp();
}
);
$steps->Given( "/^a WP install in '([^\s]+)'$/",
function ( $world, $subdir ) {
$world->install_wp( $subdir );
}
);
$steps->Given( '/^a WP multisite (subdirectory|subdomain)?\s?install$/',
function ( $world, $type = 'subdirectory' ) {
$world->install_wp();
$subdomains = ! empty( $type ) && 'subdomain' === $type ? 1 : 0;
$world->proc( 'wp core install-network', array( 'title' => 'WP CLI Network', 'subdomains' => $subdomains ) )->run_check();
}
);
$steps->Given( '/^these installed and active plugins:$/',
function( $world, $stream ) {
$plugins = implode( ' ', array_map( 'trim', explode( PHP_EOL, (string)$stream ) ) );
$world->proc( "wp plugin install $plugins --activate" )->run_check();
}
);
$steps->Given( '/^a custom wp-content directory$/',
function ( $world ) {
$wp_config_path = $world->variables['RUN_DIR'] . "/wp-config.php";
$wp_config_code = file_get_contents( $wp_config_path );
$world->move_files( 'wp-content', 'my-content' );
$world->add_line_to_wp_config( $wp_config_code,
"define( 'WP_CONTENT_DIR', dirname(__FILE__) . '/my-content' );" );
$world->move_files( 'my-content/plugins', 'my-plugins' );
$world->add_line_to_wp_config( $wp_config_code,
"define( 'WP_PLUGIN_DIR', __DIR__ . '/my-plugins' );" );
file_put_contents( $wp_config_path, $wp_config_code );
}
);
$steps->Given( '/^download:$/',
function ( $world, TableNode $table ) {
foreach ( $table->getHash() as $row ) {
$path = $world->replace_variables( $row['path'] );
if ( file_exists( $path ) ) {
// assume it's the same file and skip re-download
continue;
}
Process::create( \WP_CLI\Utils\esc_cmd( 'curl -sSL %s > %s', $row['url'], $path ) )->run_check();
}
}
);
$steps->Given( '/^save (STDOUT|STDERR) ([\'].+[^\'])?as \{(\w+)\}$/',
function ( $world, $stream, $output_filter, $key ) {
$stream = strtolower( $stream );
if ( $output_filter ) {
$output_filter = '/' . trim( str_replace( '%s', '(.+[^\b])', $output_filter ), "' " ) . '/';
if ( false !== preg_match( $output_filter, $world->result->$stream, $matches ) )
$output = array_pop( $matches );
else
$output = '';
} else {
$output = $world->result->$stream;
}
$world->variables[ $key ] = trim( $output, "\n" );
}
);
$steps->Given( '/^a new Phar(?: with version "([^"]+)")$/',
function ( $world, $version ) {
$world->build_phar( $version );
}
);
$steps->Given( '/^save the (.+) file ([\'].+[^\'])?as \{(\w+)\}$/',
function ( $world, $filepath, $output_filter, $key ) {
$full_file = file_get_contents( $world->replace_variables( $filepath ) );
if ( $output_filter ) {
$output_filter = '/' . trim( str_replace( '%s', '(.+[^\b])', $output_filter ), "' " ) . '/';
if ( false !== preg_match( $output_filter, $full_file, $matches ) )
$output = array_pop( $matches );
else
$output = '';
} else {
$output = $full_file;
}
$world->variables[ $key ] = trim( $output, "\n" );
}
);
$steps->Given('/^a misconfigured WP_CONTENT_DIR constant directory$/',
function($world) {
$wp_config_path = $world->variables['RUN_DIR'] . "/wp-config.php";
$wp_config_code = file_get_contents( $wp_config_path );
$world->add_line_to_wp_config( $wp_config_code,
"define( 'WP_CONTENT_DIR', '' );" );
file_put_contents( $wp_config_path, $wp_config_code );
}
);

192
features/steps/then.php Normal file
View file

@ -0,0 +1,192 @@
<?php
use Behat\Gherkin\Node\PyStringNode,
Behat\Gherkin\Node\TableNode;
$steps->Then( '/^the return code should be (\d+)$/',
function ( $world, $return_code ) {
if ( $return_code != $world->result->return_code ) {
throw new RuntimeException( $world->result );
}
}
);
$steps->Then( '/^(STDOUT|STDERR) should (be|contain|not contain):$/',
function ( $world, $stream, $action, PyStringNode $expected ) {
$stream = strtolower( $stream );
$expected = $world->replace_variables( (string) $expected );
checkString( $world->result->$stream, $expected, $action, $world->result );
}
);
$steps->Then( '/^(STDOUT|STDERR) should be a number$/',
function ( $world, $stream ) {
$stream = strtolower( $stream );
assertNumeric( trim( $world->result->$stream, "\n" ) );
}
);
$steps->Then( '/^(STDOUT|STDERR) should not be a number$/',
function ( $world, $stream ) {
$stream = strtolower( $stream );
assertNotNumeric( trim( $world->result->$stream, "\n" ) );
}
);
$steps->Then( '/^STDOUT should be a table containing rows:$/',
function ( $world, TableNode $expected ) {
$output = $world->result->stdout;
$actual_rows = explode( "\n", rtrim( $output, "\n" ) );
$expected_rows = array();
foreach ( $expected->getRows() as $row ) {
$expected_rows[] = $world->replace_variables( implode( "\t", $row ) );
}
compareTables( $expected_rows, $actual_rows, $output );
}
);
$steps->Then( '/^STDOUT should end with a table containing rows:$/',
function ( $world, TableNode $expected ) {
$output = $world->result->stdout;
$actual_rows = explode( "\n", rtrim( $output, "\n" ) );
$expected_rows = array();
foreach ( $expected->getRows() as $row ) {
$expected_rows[] = $world->replace_variables( implode( "\t", $row ) );
}
$start = array_search( $expected_rows[0], $actual_rows );
if ( false === $start )
throw new \Exception( $world->result );
compareTables( $expected_rows, array_slice( $actual_rows, $start ), $output );
}
);
$steps->Then( '/^STDOUT should be JSON containing:$/',
function ( $world, PyStringNode $expected ) {
$output = $world->result->stdout;
$expected = $world->replace_variables( (string) $expected );
if ( !checkThatJsonStringContainsJsonString( $output, $expected ) ) {
throw new \Exception( $world->result );
}
});
$steps->Then( '/^STDOUT should be a JSON array containing:$/',
function ( $world, PyStringNode $expected ) {
$output = $world->result->stdout;
$expected = $world->replace_variables( (string) $expected );
$actualValues = json_decode( $output );
$expectedValues = json_decode( $expected );
$missing = array_diff( $expectedValues, $actualValues );
if ( !empty( $missing ) ) {
throw new \Exception( $world->result );
}
});
$steps->Then( '/^STDOUT should be CSV containing:$/',
function ( $world, TableNode $expected ) {
$output = $world->result->stdout;
$expected_rows = $expected->getRows();
foreach ( $expected as &$row ) {
foreach ( $row as &$value ) {
$value = $world->replace_variables( $value );
}
}
if ( ! checkThatCsvStringContainsValues( $output, $expected_rows ) )
throw new \Exception( $world->result );
}
);
$steps->Then( '/^STDOUT should be YAML containing:$/',
function ( $world, PyStringNode $expected ) {
$output = $world->result->stdout;
$expected = $world->replace_variables( (string) $expected );
if ( !checkThatYamlStringContainsYamlString( $output, $expected ) ) {
throw new \Exception( $world->result );
}
});
$steps->Then( '/^(STDOUT|STDERR) should be empty$/',
function ( $world, $stream ) {
$stream = strtolower( $stream );
if ( !empty( $world->result->$stream ) ) {
throw new \Exception( $world->result );
}
}
);
$steps->Then( '/^(STDOUT|STDERR) should not be empty$/',
function ( $world, $stream ) {
$stream = strtolower( $stream );
if ( '' === rtrim( $world->result->$stream, "\n" ) ) {
throw new Exception( $world->result );
}
}
);
$steps->Then( '/^the (.+) (file|directory) should (exist|not exist|be:|contain:|not contain:)$/',
function ( $world, $path, $type, $action, $expected = null ) {
$path = $world->replace_variables( $path );
// If it's a relative path, make it relative to the current test dir
if ( '/' !== $path[0] )
$path = $world->variables['RUN_DIR'] . "/$path";
if ( 'file' == $type ) {
$test = 'file_exists';
} else if ( 'directory' == $type ) {
$test = 'is_dir';
}
switch ( $action ) {
case 'exist':
if ( ! $test( $path ) ) {
throw new Exception( $world->result );
}
break;
case 'not exist':
if ( $test( $path ) ) {
throw new Exception( $world->result );
}
break;
default:
if ( ! $test( $path ) ) {
throw new Exception( "$path doesn't exist." );
}
$action = substr( $action, 0, -1 );
$expected = $world->replace_variables( (string) $expected );
if ( 'file' == $type ) {
$contents = file_get_contents( $path );
} else if ( 'directory' == $type ) {
$files = glob( rtrim( $path, '/' ) . '/*' );
foreach( $files as &$file ) {
$file = str_replace( $path . '/', '', $file );
}
$contents = implode( PHP_EOL, $files );
}
checkString( $contents, $expected, $action );
}
}
);

46
features/steps/when.php Normal file
View file

@ -0,0 +1,46 @@
<?php
use Behat\Gherkin\Node\PyStringNode,
Behat\Gherkin\Node\TableNode,
WP_CLI\Process;
function invoke_proc( $proc, $mode ) {
$map = array(
'run' => 'run_check',
'try' => 'run'
);
$method = $map[ $mode ];
return $proc->$method();
}
$steps->When( '/^I launch in the background `([^`]+)`$/',
function ( $world, $cmd ) {
$world->background_proc( $cmd );
}
);
$steps->When( '/^I (run|try) `([^`]+)`$/',
function ( $world, $mode, $cmd ) {
$cmd = $world->replace_variables( $cmd );
$world->result = invoke_proc( $world->proc( $cmd ), $mode );
}
);
$steps->When( "/^I (run|try) `([^`]+)` from '([^\s]+)'$/",
function ( $world, $mode, $cmd, $subdir ) {
$cmd = $world->replace_variables( $cmd );
$world->result = invoke_proc( $world->proc( $cmd, array(), $subdir ), $mode );
}
);
$steps->When( '/^I (run|try) the previous command again$/',
function ( $world, $mode ) {
if ( !isset( $world->result ) )
throw new \Exception( 'No previous command.' );
$proc = Process::create( $world->result->command, $world->result->cwd, $world->result->env );
$world->result = invoke_proc( $proc, $mode );
}
);

View file

@ -0,0 +1,17 @@
<?php
/**
* Profile the performance of a request to WordPress.
*/
class Profile_Command {
/**
* Profile the performance of a request to WordPress.
*
* @when before_wp_load
*/
public function __invoke() {
}
}

46
utils/behat-tags.php Normal file
View file

@ -0,0 +1,46 @@
<?php
/**
* Generate a list of tags to skip during the test run.
*
* Require a minimum version of WordPress:
*
* @require-wp-4.0
* Scenario: Core translation CRUD
*
* Then use in bash script:
*
* BEHAT_TAGS=$(php behat-tags.php)
* vendor/bin/behat --format progress $BEHAT_TAGS
*/
function version_tags( $prefix, $current, $operator = '<' ) {
if ( ! $current )
return;
exec( "grep '@{$prefix}-[0-9\.]*' -h -o features/*.feature | uniq", $existing_tags );
$skip_tags = array();
foreach ( $existing_tags as $tag ) {
$compare = str_replace( "@{$prefix}-", '', $tag );
if ( version_compare( $current, $compare, $operator ) ) {
$skip_tags[] = $tag;
}
}
return $skip_tags;
}
$skip_tags = array_merge(
version_tags( 'require-wp', getenv( 'WP_VERSION' ), '<' ),
version_tags( 'require-php', PHP_VERSION, '<' ),
version_tags( 'less-than-php', PHP_VERSION, '>' )
);
# Skip Github API tests by default because of rate limiting. See https://github.com/wp-cli/wp-cli/issues/1612
$skip_tags[] = '@github-api';
if ( !empty( $skip_tags ) ) {
echo '--tags=~' . implode( '&&~', $skip_tags );
}

2
wp-cli.yml Normal file
View file

@ -0,0 +1,2 @@
require:
- command.php