├── .distignore ├── .editorconfig ├── .gitignore ├── .travis.yml ├── README.md ├── bin └── install-package-tests.sh ├── command.php ├── composer.json ├── features ├── bootstrap │ ├── FeatureContext.php │ ├── Process.php │ ├── support.php │ └── utils.php ├── extra │ └── no-mail.php ├── load-wp-cli.feature └── steps │ ├── given.php │ ├── then.php │ └── when.php ├── phpcs.xml.dist └── utils └── behat-tags.php /.distignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .git 3 | .gitignore 4 | .editorconfig 5 | .travis.yml 6 | circle.yml 7 | bin/ 8 | features/ 9 | utils/ 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is for unifying the coding style for different editors and IDEs 2 | # editorconfig.org 3 | 4 | # WordPress Coding Standards 5 | # https://make.wordpress.org/core/handbook/coding-standards/ 6 | 7 | root = true 8 | 9 | [*] 10 | charset = utf-8 11 | end_of_line = lf 12 | insert_final_newline = true 13 | trim_trailing_whitespace = true 14 | indent_style = tab 15 | 16 | [{.jshintrc,*.json,*.yml,*.feature}] 17 | indent_style = space 18 | indent_size = 2 19 | 20 | [{*.txt,wp-config-sample.php}] 21 | end_of_line = crlf 22 | 23 | [composer.json] 24 | indent_style = space 25 | indent_size = 4 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | wp-cli.local.yml 3 | composer.lock 4 | node_modules/ 5 | vendor/ 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: php 4 | 5 | notifications: 6 | email: 7 | on_success: never 8 | on_failure: change 9 | 10 | branches: 11 | only: 12 | - master 13 | 14 | php: 15 | - 5.5 16 | - 7.0 17 | 18 | cache: 19 | - composer 20 | - $HOME/.composer/cache 21 | 22 | env: 23 | global: 24 | - WP_CLI_BIN_DIR=/tmp/wp-cli-phar 25 | 26 | before_script: 27 | - composer validate --strict 28 | - bash bin/install-package-tests.sh 29 | 30 | script: ./vendor/bin/behat --strict 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | anhskohbo/wp-cli-themecheck 2 | =========================== 3 | 4 | 5 | 6 | [![Build Status](https://travis-ci.org/anhskohbo/wp-cli-themecheck.svg?branch=master)](https://travis-ci.org/anhskohbo/wp-cli-themecheck) 7 | 8 | Quick links: [Using](#using) | [Installing](#installing) | [Contributing](#contributing) 9 | 10 | ## Using 11 | 12 | ``` 13 | wp themecheck 14 | ``` 15 | 16 | ![peek 2016-10-10 15-02](https://cloud.githubusercontent.com/assets/1529454/19229848/cd793e9a-8efa-11e6-8abe-a2b55a4243b4.gif) 17 | 18 | ## Installing 19 | 20 | Installing this package requires WP-CLI v0.23.0 or greater. Update to the latest stable release with `wp cli update`. 21 | 22 | Once you've done so, you can install this package with `wp package install anhskohbo/wp-cli-themecheck`. 23 | 24 | ## Contributing 25 | 26 | We appreciate you taking the initiative to contribute to this project. 27 | 28 | 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. 29 | 30 | ### Reporting a bug 31 | 32 | Think you’ve found a bug? We’d love for you to help us get it fixed. 33 | 34 | Before you create a new issue, you should [search existing issues](https://github.com/anhskohbo/wp-cli-themecheck/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. 35 | 36 | 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/anhskohbo/wp-cli-themecheck/issues/new) with the following: 37 | 38 | 1. What you were doing (e.g. "When I run `wp post list`"). 39 | 2. What you saw (e.g. "I see a fatal about a class being undefined."). 40 | 3. What you expected to see (e.g. "I expected to see the list of posts.") 41 | 42 | Include as much detail as you can, and clear steps to reproduce if possible. 43 | 44 | ### Creating a pull request 45 | 46 | Want to contribute a new feature? Please first [open a new issue](https://github.com/anhskohbo/wp-cli-themecheck/issues/new) to discuss whether the feature is a good fit for the project. 47 | 48 | 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: 49 | 50 | 1. Create a feature branch for each contribution. 51 | 2. Submit your pull request early for feedback. 52 | 3. Include functional tests with your changes. [Read the WP-CLI documentation](https://wp-cli.org/docs/pull-requests/#functional-tests) for an introduction. 53 | 4. Follow the [WordPress Coding Standards](http://make.wordpress.org/core/handbook/coding-standards/). 54 | -------------------------------------------------------------------------------- /bin/install-package-tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -ex 4 | 5 | PACKAGE_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )"/../ && pwd )" 6 | 7 | download() { 8 | if [ `which curl` ]; then 9 | curl -s "$1" > "$2"; 10 | elif [ `which wget` ]; then 11 | wget -nv -O "$2" "$1" 12 | fi 13 | } 14 | 15 | install_wp_cli() { 16 | 17 | # the Behat test suite will pick up the executable found in $WP_CLI_BIN_DIR 18 | mkdir -p $WP_CLI_BIN_DIR 19 | download https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli-nightly.phar $WP_CLI_BIN_DIR/wp 20 | chmod +x $WP_CLI_BIN_DIR/wp 21 | 22 | } 23 | 24 | download_behat() { 25 | 26 | cd $PACKAGE_DIR 27 | download https://getcomposer.org/installer installer 28 | php installer 29 | php composer.phar require --dev behat/behat='~2.5' 30 | 31 | } 32 | 33 | install_db() { 34 | mysql -e 'CREATE DATABASE IF NOT EXISTS wp_cli_test;' -uroot 35 | mysql -e 'GRANT ALL PRIVILEGES ON wp_cli_test.* TO "wp_cli_test"@"localhost" IDENTIFIED BY "password1"' -uroot 36 | } 37 | 38 | install_wp_cli 39 | download_behat 40 | install_db 41 | -------------------------------------------------------------------------------- /command.php: -------------------------------------------------------------------------------- 1 | 6 | * @license MIT 7 | * @link https://github.com/anhskohbo/wp-cli-themecheck 8 | */ 9 | 10 | if ( ! class_exists( 'WP_CLI' ) ) { 11 | return; 12 | } 13 | 14 | use WP_CLI\Utils as Utils; 15 | use Symfony\Component\Finder\Finder; 16 | 17 | if ( ! class_exists( 'WP_CLI_Themecheck_Command' ) ) : 18 | 19 | /** 20 | * Themecheck_Command class. 21 | */ 22 | class WP_CLI_Themecheck_Command extends WP_CLI_Command { 23 | 24 | /** 25 | * Run themecheck in CLI. 26 | * 27 | * ## OPTIONS 28 | * 29 | * [--theme=] 30 | * : Theme name to check. 31 | * 32 | * [--skip-info] 33 | * : Suppress INFO. 34 | * 35 | * [--skip-recommended] 36 | * : Suppress RECOMMENDED. 37 | * 38 | * [--interactive] 39 | * : Prompt user for input (default). 40 | * 41 | * ## EXAMPLES 42 | * 43 | * $ wp themecheck --theme="twentysixteen" 44 | * 45 | * @when after_wp_load 46 | */ 47 | public function __invoke( $args, $assoc_args ) { 48 | 49 | $interactive = (bool) Utils\get_flag_value( $assoc_args, 'interactive', true ); 50 | $this->before_themecheck( $interactive ); 51 | 52 | require_once WP_PLUGIN_DIR . '/theme-check/checkbase.php'; 53 | require_once WP_PLUGIN_DIR . '/theme-check/main.php'; 54 | 55 | if ( Utils\get_flag_value( $assoc_args, 'theme' ) ) { 56 | $themename = Utils\get_flag_value( $assoc_args, 'theme' ); 57 | } elseif ( ! $interactive ) { 58 | $themename = get_stylesheet(); 59 | } else { 60 | $themename = $this->choices_theme(); 61 | } 62 | 63 | // Find theme to check. 64 | $theme = wp_get_theme( $themename ); 65 | if ( $theme->exists() ) { 66 | $themepath = trailingslashit( $theme->get_template_directory() ); 67 | } elseif ( is_dir( $guest_path ) ) { 68 | $themepath = trailingslashit( $guest_path ); 69 | } else { 70 | WP_CLI::error( 'Unable find theme named "' . $themename . '"' ); 71 | } 72 | 73 | // Run themecheck. 74 | WP_CLI::line( "\n" ); 75 | WP_CLI::line( sprintf( '| Checking %s...', $theme->get( 'Name' ) ) ); 76 | WP_CLI::line( "\n" ); 77 | 78 | $is_success = $this->themecheck( $themename, $themepath, $theme ); 79 | 80 | // Display themecheck logs. 81 | foreach ( $this->stack_errors as $log_level => $errors ) { 82 | if ( Utils\get_flag_value( $assoc_args, 'skip-info' ) && 'INFO' === $log_level ) { 83 | continue; 84 | } 85 | 86 | if ( Utils\get_flag_value( $assoc_args, 'skip-recommended' ) && 'RECOMMENDED' === $log_level ) { 87 | continue; 88 | } 89 | 90 | foreach ( $errors as $error ) { 91 | WP_CLI::line( $error . "\n" ); 92 | usleep( 50000 ); 93 | } 94 | } 95 | 96 | $required_count = ! empty( $this->stack_errors['REQUIRED'] ) ? count( $this->stack_errors['REQUIRED'] ) : 0; 97 | $warning_count = ! empty( $this->stack_errors['WARNING'] ) ? count( $this->stack_errors['WARNING'] ) : 0; 98 | $total_errors = $required_count + $warning_count; 99 | 100 | if ( $is_success ) { 101 | WP_CLI::success( sprintf( 'Congratulations! %s passed the tests!', $theme->get( 'Name' ) ) ); 102 | } else { 103 | WP_CLI::error( sprintf( '%d error(s) found for %s!', $total_errors, $theme->get( 'Name' ) ), $total_errors ); 104 | } 105 | } 106 | 107 | /** 108 | * Run themecheck. 109 | * 110 | * @param string $theme Theme name. 111 | * @param string $path Them absolute path. 112 | * @param WP_Theme $theme_object Theme object. 113 | */ 114 | private function themecheck( $theme, $path, $theme_object ) { 115 | global $themechecks, $data, $themename; 116 | 117 | $themename = $theme; 118 | $datafiles = array( 'php' => array(), 'css' => array(), 'other' => array() ); 119 | 120 | // Find all files. 121 | $finder = new Finder(); 122 | $finder->ignoreDotFiles( false )->ignoreVCS( false )->exclude( array( 'node_modules', 'tests' ) )->in( $path ); 123 | 124 | foreach ( $finder as $node ) { 125 | $filename = $node->getRealPath(); 126 | 127 | switch ( $node->getExtension() ) { 128 | case 'php': 129 | $datafiles['php'][ $filename ] = tc_strip_comments( file_get_contents( $filename ) ); 130 | break; 131 | 132 | case 'css': 133 | $datafiles['css'][ $filename ] = file_get_contents( $filename ); 134 | break; 135 | 136 | default: 137 | $datafiles['other'][ $filename ] = $node->isDir() ? '' : file_get_contents( $filename ); 138 | break; 139 | } 140 | } 141 | 142 | // Run Themecheck. 143 | $data = array( 144 | 'Name' => $theme_object->get( 'Name' ), 145 | 'URI' => $theme_object->display( 'ThemeURI', true, false ), 146 | 'Description' => $theme_object->display( 'Description', true, false ), 147 | 'Author' => $theme_object->display( 'Author', true, false ), 148 | 'AuthorURI' => $theme_object->display( 'AuthorURI', true, false ), 149 | 'Version' => $theme_object->get( 'Version' ), 150 | 'Template' => $theme_object->get( 'Template' ), 151 | 'Status' => $theme_object->get( 'Status' ), 152 | 'Tags' => $theme_object->get( 'Tags' ), 153 | 'Title' => $theme_object->get( 'Name' ), 154 | 'AuthorName' => $theme_object->display( 'Author', false, false ), 155 | 'License' => $theme_object->display( 'License', false, false ), 156 | 'License URI' => $theme_object->display( 'License URI', false, false ), 157 | 'Template Version' => $theme_object->display( 'Template Version', false, false ), 158 | ); 159 | 160 | $success = run_themechecks( $datafiles['php'], $datafiles['css'], $datafiles['other'], array( 161 | 'theme' => $theme_object, 162 | 'slug' => $themename, 163 | ) ); 164 | 165 | // Build logs report. 166 | $log_pattern = '/((REQUIRED|WARNING|RECOMMENDED|INFO)<\/span>\s?:)/i'; 167 | $stack_errors = array( 'REQUIRED' => array(), 'WARNING' => array(), 'RECOMMENDED' => array(), 'INFO' => array() ); 168 | 169 | foreach ( $themechecks as $check ) { 170 | if ( ! $check instanceof Themecheck ) { 171 | continue; 172 | } 173 | 174 | $errors = (array) $check->getError(); 175 | if ( empty( $errors ) ) { 176 | continue; 177 | } 178 | 179 | foreach ( $errors as $error ) { 180 | $log_level = ''; 181 | 182 | if ( preg_match( $log_pattern, $error, $matches ) ) { 183 | $error = preg_replace( $log_pattern, '', $error ); 184 | $log_level = strtoupper( $matches[2] ); 185 | } 186 | 187 | $error = $this->format_themecheck_result( $error, $log_level ); 188 | if ( ! in_array( $error, $stack_errors[ $log_level ] ) ) { 189 | $stack_errors[ $log_level ][] = $error; 190 | } 191 | } 192 | } 193 | 194 | $this->stack_errors = $stack_errors; 195 | 196 | return $success; 197 | } 198 | 199 | /** 200 | * Show a list to choices theme. 201 | * 202 | * @return string 203 | */ 204 | private function choices_theme() { 205 | $themes = array(); 206 | 207 | foreach ( wp_get_themes() as $id => $theme ) { 208 | $themes[ $id ] = $theme->get( 'Name' ); 209 | } 210 | 211 | return cli\menu( $themes, get_stylesheet(), 'Choose a theme' ); 212 | } 213 | 214 | /** 215 | * Format themecheck result. 216 | * 217 | * @param string $string HTML report result. 218 | * @param string $log_level Log level. 219 | * @return string 220 | */ 221 | private function format_themecheck_result( $string, $log_level ) { 222 | switch ( $log_level ) { 223 | case 'REQUIRED': 224 | $string = WP_CLI::colorize( '%R× ' . $log_level . ':%n' ) . $string; 225 | break; 226 | 227 | case 'WARNING': 228 | $string = WP_CLI::colorize( '%R* ' . $log_level . ':%n' ) . $string; 229 | break; 230 | 231 | case 'RECOMMENDED': 232 | $string = WP_CLI::colorize( '%G* ' . $log_level . ':%n' ) . $string; 233 | break; 234 | 235 | case 'INFO': 236 | $string = WP_CLI::colorize( '%C* ' . $log_level . ':%n' ) . $string; 237 | break; 238 | 239 | default: 240 | $string = $log_level ? ( '* ' . $log_level . $string ) : '* ' . $string; 241 | break; 242 | } 243 | 244 | $string = str_replace( array( '', '', "", '' ), '', $string ); 245 | $string = str_replace( array( '
', '
', '
' ), "\n ", $string ); 246 | 247 | $string = str_replace( array( '', '' ), WP_CLI::colorize( '%9"' ), $string ); 248 | $string = str_replace( array( '', '' ), WP_CLI::colorize( '"%n' ), $string ); 249 | 250 | $string = str_replace( array( "
", '
' ), WP_CLI::colorize( "\n  %5" ), $string );
251 | 			$string = str_replace( '
', WP_CLI::colorize( '%n' ), $string ); 252 | 253 | $string = preg_replace( '/(([^<]*)<\/a>)/i', "\033[1m\$3\033[0m (\$2)", $string ); 254 | $string = str_replace( 'See See:', 'See:', $string ); // Correcly wrong spell. 255 | 256 | return htmlspecialchars_decode( $string ); 257 | } 258 | 259 | /** 260 | * Make sure the Theme Check plugin is installed. 261 | * 262 | * @param bool $interactive Prompt user before exiting. 263 | */ 264 | private function before_themecheck( $interactive ) { 265 | if ( class_exists( 'ThemeCheckMain' ) ) { 266 | return true; 267 | } 268 | 269 | require_once ABSPATH . 'wp-admin/includes/plugin.php'; 270 | 271 | if ( array_key_exists( 'theme-check/theme-check.php', get_plugins() ) ) { 272 | $command = array( array( 'plugin', 'activate', 'theme-check' ), array() ); 273 | $error_msg = "The Theme Check plugin must be activated.\n\n\tRun: wp plugin activate theme-check\n"; 274 | } else { 275 | $command = array( array( 'plugin', 'install', 'theme-check' ), array( 'activate' => true ) ); 276 | $error_msg = "The Theme Check plugin must be installed and activated.\n\n\tRun: wp plugin install theme-check --activate\n"; 277 | } 278 | 279 | WP_CLI::error( $error_msg, ! $interactive ); 280 | $choose = cli\choose( 'Do you want run above command right now' ); 281 | 282 | if ( 'y' !== $choose ) { 283 | exit; 284 | } 285 | 286 | WP_CLI::line(); 287 | WP_CLI::run_command( $command[0], $command[1] ); 288 | WP_CLI::line(); 289 | } 290 | } 291 | 292 | WP_CLI::add_command( 'themecheck', 'WP_CLI_Themecheck_Command' ); 293 | endif; 294 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "anhskohbo/wp-cli-themecheck", 3 | "description": "Run Themecheck in WP_CLI", 4 | "type": "wp-cli-package", 5 | "homepage": "https://github.com/anhskohbo/wp-cli-themecheck", 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Nguyen Van Anh", 10 | "email": "anhskohbo@gmail.com" 11 | } 12 | ], 13 | "autoload": { 14 | "files": [ "command.php" ] 15 | }, 16 | "require": { 17 | "symfony/finder": "~2.0|~3.0" 18 | }, 19 | "require-dev": { 20 | "behat/behat": "~2.5" 21 | }, 22 | "scripts": { 23 | "run-behat": "./vendor/bin/behat --strict --ansi" 24 | }, 25 | "minimum-stability": "dev" 26 | } 27 | -------------------------------------------------------------------------------- /features/bootstrap/FeatureContext.php: -------------------------------------------------------------------------------- 1 | autoload->files ) ) { 19 | $contents = 'require:' . PHP_EOL; 20 | foreach( $composer->autoload->files as $file ) { 21 | $contents .= ' - ' . dirname( dirname( dirname( __FILE__ ) ) ) . '/' . $file; 22 | } 23 | @mkdir( sys_get_temp_dir() . '/wp-cli-package-test/' ); 24 | $project_config = sys_get_temp_dir() . '/wp-cli-package-test/config.yml'; 25 | file_put_contents( $project_config, $contents ); 26 | putenv( 'WP_CLI_CONFIG_PATH=' . $project_config ); 27 | } 28 | } 29 | // Inside WP-CLI 30 | } else { 31 | require_once __DIR__ . '/../../php/utils.php'; 32 | require_once __DIR__ . '/../../php/WP_CLI/Process.php'; 33 | require_once __DIR__ . '/../../vendor/autoload.php'; 34 | } 35 | 36 | /** 37 | * Features context. 38 | */ 39 | class FeatureContext extends BehatContext implements ClosuredContextInterface { 40 | 41 | private static $cache_dir, $suite_cache_dir; 42 | 43 | private static $db_settings = array( 44 | 'dbname' => 'wp_cli_test', 45 | 'dbuser' => 'wp_cli_test', 46 | 'dbpass' => 'password1', 47 | 'dbhost' => '127.0.0.1', 48 | ); 49 | 50 | private $running_procs = array(); 51 | 52 | public $variables = array(); 53 | 54 | /** 55 | * Get the environment variables required for launched `wp` processes 56 | * @beforeSuite 57 | */ 58 | private static function get_process_env_variables() { 59 | // Ensure we're using the expected `wp` binary 60 | $bin_dir = getenv( 'WP_CLI_BIN_DIR' ) ?: realpath( __DIR__ . "/../../bin" ); 61 | $env = array( 62 | 'PATH' => $bin_dir . ':' . getenv( 'PATH' ), 63 | 'BEHAT_RUN' => 1, 64 | 'HOME' => '/tmp/wp-cli-home', 65 | ); 66 | if ( $config_path = getenv( 'WP_CLI_CONFIG_PATH' ) ) { 67 | $env['WP_CLI_CONFIG_PATH'] = $config_path; 68 | } 69 | return $env; 70 | } 71 | 72 | // We cache the results of `wp core download` to improve test performance 73 | // Ideally, we'd cache at the HTTP layer for more reliable tests 74 | private static function cache_wp_files() { 75 | self::$cache_dir = sys_get_temp_dir() . '/wp-cli-test core-download-cache'; 76 | 77 | if ( is_readable( self::$cache_dir . '/wp-config-sample.php' ) ) 78 | return; 79 | 80 | $cmd = Utils\esc_cmd( 'wp core download --force --path=%s', self::$cache_dir ); 81 | Process::create( $cmd, null, self::get_process_env_variables() )->run_check(); 82 | } 83 | 84 | /** 85 | * @BeforeSuite 86 | */ 87 | public static function prepare( SuiteEvent $event ) { 88 | $result = Process::create( 'wp cli info', null, self::get_process_env_variables() )->run_check(); 89 | echo PHP_EOL; 90 | echo $result->stdout; 91 | echo PHP_EOL; 92 | self::cache_wp_files(); 93 | } 94 | 95 | /** 96 | * @AfterSuite 97 | */ 98 | public static function afterSuite( SuiteEvent $event ) { 99 | if ( self::$suite_cache_dir ) { 100 | Process::create( Utils\esc_cmd( 'rm -r %s', self::$suite_cache_dir ), null, self::get_process_env_variables() )->run(); 101 | } 102 | } 103 | 104 | /** 105 | * @BeforeScenario 106 | */ 107 | public function beforeScenario( $event ) { 108 | $this->variables['SRC_DIR'] = realpath( __DIR__ . '/../..' ); 109 | } 110 | 111 | /** 112 | * @AfterScenario 113 | */ 114 | public function afterScenario( $event ) { 115 | if ( isset( $this->variables['RUN_DIR'] ) ) { 116 | // remove altered WP install, unless there's an error 117 | if ( $event->getResult() < 4 ) { 118 | $this->proc( Utils\esc_cmd( 'rm -r %s', $this->variables['RUN_DIR'] ) )->run(); 119 | } 120 | } 121 | 122 | foreach ( $this->running_procs as $proc ) { 123 | self::terminate_proc( $proc ); 124 | } 125 | } 126 | 127 | /** 128 | * Terminate a process and any of its children. 129 | */ 130 | private static function terminate_proc( $proc ) { 131 | $status = proc_get_status( $proc ); 132 | 133 | $master_pid = $status['pid']; 134 | 135 | $output = `ps -o ppid,pid,command | grep $master_pid`; 136 | 137 | foreach ( explode( PHP_EOL, $output ) as $line ) { 138 | if ( preg_match( '/^\s*(\d+)\s+(\d+)/', $line, $matches ) ) { 139 | $parent = $matches[1]; 140 | $child = $matches[2]; 141 | 142 | if ( $parent == $master_pid ) { 143 | if ( ! posix_kill( (int) $child, 9 ) ) { 144 | throw new RuntimeException( posix_strerror( posix_get_last_error() ) ); 145 | } 146 | } 147 | } 148 | } 149 | 150 | if ( ! posix_kill( (int) $master_pid, 9 ) ) { 151 | throw new RuntimeException( posix_strerror( posix_get_last_error() ) ); 152 | } 153 | } 154 | 155 | public static function create_cache_dir() { 156 | self::$suite_cache_dir = sys_get_temp_dir() . '/' . uniqid( "wp-cli-test-suite-cache-", TRUE ); 157 | mkdir( self::$suite_cache_dir ); 158 | return self::$suite_cache_dir; 159 | } 160 | 161 | /** 162 | * Initializes context. 163 | * Every scenario gets it's own context object. 164 | * 165 | * @param array $parameters context parameters (set them up through behat.yml) 166 | */ 167 | public function __construct( array $parameters ) { 168 | $this->drop_db(); 169 | $this->set_cache_dir(); 170 | $this->variables['CORE_CONFIG_SETTINGS'] = Utils\assoc_args_to_str( self::$db_settings ); 171 | } 172 | 173 | public function getStepDefinitionResources() { 174 | return glob( __DIR__ . '/../steps/*.php' ); 175 | } 176 | 177 | public function getHookDefinitionResources() { 178 | return array(); 179 | } 180 | 181 | public function replace_variables( $str ) { 182 | return preg_replace_callback( '/\{([A-Z_]+)\}/', array( $this, '_replace_var' ), $str ); 183 | } 184 | 185 | private function _replace_var( $matches ) { 186 | $cmd = $matches[0]; 187 | 188 | foreach ( array_slice( $matches, 1 ) as $key ) { 189 | $cmd = str_replace( '{' . $key . '}', $this->variables[ $key ], $cmd ); 190 | } 191 | 192 | return $cmd; 193 | } 194 | 195 | public function create_run_dir() { 196 | if ( !isset( $this->variables['RUN_DIR'] ) ) { 197 | $this->variables['RUN_DIR'] = sys_get_temp_dir() . '/' . uniqid( "wp-cli-test-run-", TRUE ); 198 | mkdir( $this->variables['RUN_DIR'] ); 199 | } 200 | } 201 | 202 | public function build_phar( $version = 'same' ) { 203 | $this->variables['PHAR_PATH'] = $this->variables['RUN_DIR'] . '/' . uniqid( "wp-cli-build-", TRUE ) . '.phar'; 204 | 205 | $this->proc( Utils\esc_cmd( 206 | 'php -dphar.readonly=0 %1$s %2$s --version=%3$s && chmod +x %2$s', 207 | __DIR__ . '/../../utils/make-phar.php', 208 | $this->variables['PHAR_PATH'], 209 | $version 210 | ) )->run_check(); 211 | } 212 | 213 | private function set_cache_dir() { 214 | $path = sys_get_temp_dir() . '/wp-cli-test-cache'; 215 | $this->proc( Utils\esc_cmd( 'mkdir -p %s', $path ) )->run_check(); 216 | $this->variables['CACHE_DIR'] = $path; 217 | } 218 | 219 | private static function run_sql( $sql ) { 220 | Utils\run_mysql_command( 'mysql --no-defaults', array( 221 | 'execute' => $sql, 222 | 'host' => self::$db_settings['dbhost'], 223 | 'user' => self::$db_settings['dbuser'], 224 | 'pass' => self::$db_settings['dbpass'], 225 | ) ); 226 | } 227 | 228 | public function create_db() { 229 | $dbname = self::$db_settings['dbname']; 230 | self::run_sql( "CREATE DATABASE IF NOT EXISTS $dbname" ); 231 | } 232 | 233 | public function drop_db() { 234 | $dbname = self::$db_settings['dbname']; 235 | self::run_sql( "DROP DATABASE IF EXISTS $dbname" ); 236 | } 237 | 238 | public function proc( $command, $assoc_args = array(), $path = '' ) { 239 | if ( !empty( $assoc_args ) ) 240 | $command .= Utils\assoc_args_to_str( $assoc_args ); 241 | 242 | $env = self::get_process_env_variables(); 243 | if ( isset( $this->variables['SUITE_CACHE_DIR'] ) ) { 244 | $env['WP_CLI_CACHE_DIR'] = $this->variables['SUITE_CACHE_DIR']; 245 | } 246 | 247 | if ( isset( $this->variables['RUN_DIR'] ) ) { 248 | $cwd = "{$this->variables['RUN_DIR']}/{$path}"; 249 | } else { 250 | $cwd = null; 251 | } 252 | 253 | return Process::create( $command, $cwd, $env ); 254 | } 255 | 256 | /** 257 | * Start a background process. Will automatically be closed when the tests finish. 258 | */ 259 | public function background_proc( $cmd ) { 260 | $descriptors = array( 261 | 0 => STDIN, 262 | 1 => array( 'pipe', 'w' ), 263 | 2 => array( 'pipe', 'w' ), 264 | ); 265 | 266 | $proc = proc_open( $cmd, $descriptors, $pipes, $this->variables['RUN_DIR'], self::get_process_env_variables() ); 267 | 268 | sleep(1); 269 | 270 | $status = proc_get_status( $proc ); 271 | 272 | if ( !$status['running'] ) { 273 | throw new RuntimeException( stream_get_contents( $pipes[2] ) ); 274 | } else { 275 | $this->running_procs[] = $proc; 276 | } 277 | } 278 | 279 | public function move_files( $src, $dest ) { 280 | rename( $this->variables['RUN_DIR'] . "/$src", $this->variables['RUN_DIR'] . "/$dest" ); 281 | } 282 | 283 | public function add_line_to_wp_config( &$wp_config_code, $line ) { 284 | $token = "/* That's all, stop editing!"; 285 | 286 | $wp_config_code = str_replace( $token, "$line\n\n$token", $wp_config_code ); 287 | } 288 | 289 | public function download_wp( $subdir = '' ) { 290 | $dest_dir = $this->variables['RUN_DIR'] . "/$subdir"; 291 | 292 | if ( $subdir ) { 293 | mkdir( $dest_dir ); 294 | } 295 | 296 | $this->proc( Utils\esc_cmd( "cp -r %s/* %s", self::$cache_dir, $dest_dir ) )->run_check(); 297 | 298 | // disable emailing 299 | mkdir( $dest_dir . '/wp-content/mu-plugins' ); 300 | copy( __DIR__ . '/../extra/no-mail.php', $dest_dir . '/wp-content/mu-plugins/no-mail.php' ); 301 | } 302 | 303 | public function create_config( $subdir = '' ) { 304 | $params = self::$db_settings; 305 | $params['dbprefix'] = $subdir ?: 'wp_'; 306 | 307 | $params['skip-salts'] = true; 308 | $this->proc( 'wp core config', $params, $subdir )->run_check(); 309 | } 310 | 311 | public function install_wp( $subdir = '' ) { 312 | $this->create_db(); 313 | $this->create_run_dir(); 314 | $this->download_wp( $subdir ); 315 | 316 | $this->create_config( $subdir ); 317 | 318 | $install_args = array( 319 | 'url' => 'http://example.com', 320 | 'title' => 'WP CLI Site', 321 | 'admin_user' => 'admin', 322 | 'admin_email' => 'admin@example.com', 323 | 'admin_password' => 'password1' 324 | ); 325 | 326 | $this->proc( 'wp core install', $install_args, $subdir )->run_check(); 327 | } 328 | } 329 | 330 | -------------------------------------------------------------------------------- /features/bootstrap/Process.php: -------------------------------------------------------------------------------- 1 | command = $command; 19 | $proc->cwd = $cwd; 20 | $proc->env = $env; 21 | 22 | return $proc; 23 | } 24 | 25 | private $command, $cwd, $env; 26 | 27 | private function __construct() {} 28 | 29 | /** 30 | * Run the command. 31 | * 32 | * @return ProcessRun 33 | */ 34 | public function run() { 35 | $cwd = $this->cwd; 36 | 37 | $descriptors = array( 38 | 0 => STDIN, 39 | 1 => array( 'pipe', 'w' ), 40 | 2 => array( 'pipe', 'w' ), 41 | ); 42 | 43 | $proc = proc_open( $this->command, $descriptors, $pipes, $cwd, $this->env ); 44 | 45 | $stdout = stream_get_contents( $pipes[1] ); 46 | fclose( $pipes[1] ); 47 | 48 | $stderr = stream_get_contents( $pipes[2] ); 49 | fclose( $pipes[2] ); 50 | 51 | return new ProcessRun( array( 52 | 'stdout' => $stdout, 53 | 'stderr' => $stderr, 54 | 'return_code' => proc_close( $proc ), 55 | 'command' => $this->command, 56 | 'cwd' => $cwd, 57 | 'env' => $this->env 58 | ) ); 59 | } 60 | 61 | /** 62 | * Run the command, but throw an Exception on error. 63 | * 64 | * @return ProcessRun 65 | */ 66 | public function run_check() { 67 | $r = $this->run(); 68 | 69 | if ( $r->return_code || !empty( $r->STDERR ) ) { 70 | throw new \RuntimeException( $r ); 71 | } 72 | 73 | return $r; 74 | } 75 | } 76 | 77 | /** 78 | * Results of an executed command. 79 | */ 80 | class ProcessRun { 81 | 82 | /** 83 | * @var array $props Properties of executed command. 84 | */ 85 | public function __construct( $props ) { 86 | foreach ( $props as $key => $value ) { 87 | $this->$key = $value; 88 | } 89 | } 90 | 91 | /** 92 | * Return properties of executed command as a string. 93 | * 94 | * @return string 95 | */ 96 | public function __toString() { 97 | $out = "$ $this->command\n"; 98 | $out .= "$this->stdout\n$this->stderr"; 99 | $out .= "cwd: $this->cwd\n"; 100 | $out .= "exit status: $this->return_code"; 101 | 102 | return $out; 103 | } 104 | 105 | } 106 | -------------------------------------------------------------------------------- /features/bootstrap/support.php: -------------------------------------------------------------------------------- 1 | $value ) { 71 | if ( ! compareContents( $value, $actual->$name ) ) 72 | return false; 73 | } 74 | } else if ( is_array( $expected ) ) { 75 | foreach ( $expected as $key => $value ) { 76 | if ( ! compareContents( $value, $actual[$key] ) ) 77 | return false; 78 | } 79 | } else { 80 | return $expected === $actual; 81 | } 82 | 83 | return true; 84 | } 85 | 86 | /** 87 | * Compare two strings containing JSON to ensure that @a $actualJson contains at 88 | * least what the JSON string @a $expectedJson contains. 89 | * 90 | * @return whether or not @a $actualJson contains @a $expectedJson 91 | * @retval true @a $actualJson contains @a $expectedJson 92 | * @retval false @a $actualJson does not contain @a $expectedJson 93 | * 94 | * @param[in] $actualJson the JSON string to be tested 95 | * @param[in] $expectedJson the expected JSON string 96 | * 97 | * Examples: 98 | * expected: {'a':1,'array':[1,3,5]} 99 | * 100 | * 1 ) 101 | * actual: {'a':1,'b':2,'c':3,'array':[1,2,3,4,5]} 102 | * return: true 103 | * 104 | * 2 ) 105 | * actual: {'b':2,'c':3,'array':[1,2,3,4,5]} 106 | * return: false 107 | * element 'a' is missing from the root object 108 | * 109 | * 3 ) 110 | * actual: {'a':0,'b':2,'c':3,'array':[1,2,3,4,5]} 111 | * return: false 112 | * the value of element 'a' is not 1 113 | * 114 | * 4 ) 115 | * actual: {'a':1,'b':2,'c':3,'array':[1,2,4,5]} 116 | * return: false 117 | * the contents of 'array' does not include 3 118 | */ 119 | function checkThatJsonStringContainsJsonString( $actualJson, $expectedJson ) { 120 | $actualValue = json_decode( $actualJson ); 121 | $expectedValue = json_decode( $expectedJson ); 122 | 123 | if ( !$actualValue ) { 124 | return false; 125 | } 126 | 127 | return compareContents( $expectedValue, $actualValue ); 128 | } 129 | 130 | /** 131 | * Compare two strings to confirm $actualCSV contains $expectedCSV 132 | * Both strings are expected to have headers for their CSVs. 133 | * $actualCSV must match all data rows in $expectedCSV 134 | * 135 | * @param string A CSV string 136 | * @param array A nested array of values 137 | * @return bool Whether $actualCSV contains $expectedCSV 138 | */ 139 | function checkThatCsvStringContainsValues( $actualCSV, $expectedCSV ) { 140 | $actualCSV = array_map( 'str_getcsv', explode( PHP_EOL, $actualCSV ) ); 141 | 142 | if ( empty( $actualCSV ) ) 143 | return false; 144 | 145 | // Each sample must have headers 146 | $actualHeaders = array_values( array_shift( $actualCSV ) ); 147 | $expectedHeaders = array_values( array_shift( $expectedCSV ) ); 148 | 149 | // Each expectedCSV must exist somewhere in actualCSV in the proper column 150 | $expectedResult = 0; 151 | foreach ( $expectedCSV as $expected_row ) { 152 | $expected_row = array_combine( $expectedHeaders, $expected_row ); 153 | foreach ( $actualCSV as $actual_row ) { 154 | 155 | if ( count( $actualHeaders ) != count( $actual_row ) ) 156 | continue; 157 | 158 | $actual_row = array_intersect_key( array_combine( $actualHeaders, $actual_row ), $expected_row ); 159 | if ( $actual_row == $expected_row ) 160 | $expectedResult++; 161 | } 162 | } 163 | 164 | return $expectedResult >= count( $expectedCSV ); 165 | } 166 | 167 | /** 168 | * Compare two strings containing YAML to ensure that @a $actualYaml contains at 169 | * least what the YAML string @a $expectedYaml contains. 170 | * 171 | * @return whether or not @a $actualYaml contains @a $expectedJson 172 | * @retval true @a $actualYaml contains @a $expectedJson 173 | * @retval false @a $actualYaml does not contain @a $expectedJson 174 | * 175 | * @param[in] $actualYaml the YAML string to be tested 176 | * @param[in] $expectedYaml the expected YAML string 177 | */ 178 | function checkThatYamlStringContainsYamlString( $actualYaml, $expectedYaml ) { 179 | $actualValue = spyc_load( $actualYaml ); 180 | $expectedValue = spyc_load( $expectedYaml ); 181 | 182 | if ( !$actualValue ) { 183 | return false; 184 | } 185 | 186 | return compareContents( $expectedValue, $actualValue ); 187 | } 188 | 189 | -------------------------------------------------------------------------------- /features/bootstrap/utils.php: -------------------------------------------------------------------------------- 1 | config ) && ! empty( $composer->config->{'vendor-dir'} ) ) { 66 | array_unshift( $vendor_paths, WP_CLI_ROOT . '/../../../' . $composer->config->{'vendor-dir'} ); 67 | } 68 | } 69 | return $vendor_paths; 70 | } 71 | 72 | // Using require() directly inside a class grants access to private methods to the loaded code 73 | function load_file( $path ) { 74 | require_once $path; 75 | } 76 | 77 | function load_command( $name ) { 78 | $path = WP_CLI_ROOT . "/php/commands/$name.php"; 79 | 80 | if ( is_readable( $path ) ) { 81 | include_once $path; 82 | } 83 | } 84 | 85 | function load_all_commands() { 86 | $cmd_dir = WP_CLI_ROOT . '/php/commands'; 87 | 88 | $iterator = new \DirectoryIterator( $cmd_dir ); 89 | 90 | foreach ( $iterator as $filename ) { 91 | if ( '.php' != substr( $filename, -4 ) ) 92 | continue; 93 | 94 | include_once "$cmd_dir/$filename"; 95 | } 96 | } 97 | 98 | /** 99 | * Like array_map(), except it returns a new iterator, instead of a modified array. 100 | * 101 | * Example: 102 | * 103 | * $arr = array('Football', 'Socker'); 104 | * 105 | * $it = iterator_map($arr, 'strtolower', function($val) { 106 | * return str_replace('foo', 'bar', $val); 107 | * }); 108 | * 109 | * foreach ( $it as $val ) { 110 | * var_dump($val); 111 | * } 112 | * 113 | * @param array|object Either a plain array or another iterator 114 | * @param callback The function to apply to an element 115 | * @return object An iterator that applies the given callback(s) 116 | */ 117 | function iterator_map( $it, $fn ) { 118 | if ( is_array( $it ) ) { 119 | $it = new \ArrayIterator( $it ); 120 | } 121 | 122 | if ( !method_exists( $it, 'add_transform' ) ) { 123 | $it = new Transform( $it ); 124 | } 125 | 126 | foreach ( array_slice( func_get_args(), 1 ) as $fn ) { 127 | $it->add_transform( $fn ); 128 | } 129 | 130 | return $it; 131 | } 132 | 133 | /** 134 | * Search for file by walking up the directory tree until the first file is found or until $stop_check($dir) returns true 135 | * @param string|array The files (or file) to search for 136 | * @param string|null The directory to start searching from; defaults to CWD 137 | * @param callable Function which is passed the current dir each time a directory level is traversed 138 | * @return null|string Null if the file was not found 139 | */ 140 | function find_file_upward( $files, $dir = null, $stop_check = null ) { 141 | $files = (array) $files; 142 | if ( is_null( $dir ) ) { 143 | $dir = getcwd(); 144 | } 145 | while ( @is_readable( $dir ) ) { 146 | // Stop walking up when the supplied callable returns true being passed the $dir 147 | if ( is_callable( $stop_check ) && call_user_func( $stop_check, $dir ) ) { 148 | return null; 149 | } 150 | 151 | foreach ( $files as $file ) { 152 | $path = $dir . DIRECTORY_SEPARATOR . $file; 153 | if ( file_exists( $path ) ) { 154 | return $path; 155 | } 156 | } 157 | 158 | $parent_dir = dirname( $dir ); 159 | if ( empty($parent_dir) || $parent_dir === $dir ) { 160 | break; 161 | } 162 | $dir = $parent_dir; 163 | } 164 | return null; 165 | } 166 | 167 | function is_path_absolute( $path ) { 168 | // Windows 169 | if ( isset($path[1]) && ':' === $path[1] ) 170 | return true; 171 | 172 | return $path[0] === '/'; 173 | } 174 | 175 | /** 176 | * Composes positional arguments into a command string. 177 | * 178 | * @param array 179 | * @return string 180 | */ 181 | function args_to_str( $args ) { 182 | return ' ' . implode( ' ', array_map( 'escapeshellarg', $args ) ); 183 | } 184 | 185 | /** 186 | * Composes associative arguments into a command string. 187 | * 188 | * @param array 189 | * @return string 190 | */ 191 | function assoc_args_to_str( $assoc_args ) { 192 | $str = ''; 193 | 194 | foreach ( $assoc_args as $key => $value ) { 195 | if ( true === $value ) 196 | $str .= " --$key"; 197 | else 198 | $str .= " --$key=" . escapeshellarg( $value ); 199 | } 200 | 201 | return $str; 202 | } 203 | 204 | /** 205 | * Given a template string and an arbitrary number of arguments, 206 | * returns the final command, with the parameters escaped. 207 | */ 208 | function esc_cmd( $cmd ) { 209 | if ( func_num_args() < 2 ) 210 | trigger_error( 'esc_cmd() requires at least two arguments.', E_USER_WARNING ); 211 | 212 | $args = func_get_args(); 213 | 214 | $cmd = array_shift( $args ); 215 | 216 | return vsprintf( $cmd, array_map( 'escapeshellarg', $args ) ); 217 | } 218 | 219 | function locate_wp_config() { 220 | static $path; 221 | 222 | if ( null === $path ) { 223 | if ( file_exists( ABSPATH . 'wp-config.php' ) ) 224 | $path = ABSPATH . 'wp-config.php'; 225 | elseif ( file_exists( ABSPATH . '../wp-config.php' ) && ! file_exists( ABSPATH . '/../wp-settings.php' ) ) 226 | $path = ABSPATH . '../wp-config.php'; 227 | else 228 | $path = false; 229 | 230 | if ( $path ) 231 | $path = realpath( $path ); 232 | } 233 | 234 | return $path; 235 | } 236 | 237 | function wp_version_compare( $since, $operator ) { 238 | return version_compare( str_replace( array( '-src' ), '', $GLOBALS['wp_version'] ), $since, $operator ); 239 | } 240 | 241 | /** 242 | * Render a collection of items as an ASCII table, JSON, CSV, YAML, list of ids, or count. 243 | * 244 | * Given a collection of items with a consistent data structure: 245 | * 246 | * ``` 247 | * $items = array( 248 | * array( 249 | * 'key' => 'foo', 250 | * 'value' => 'bar', 251 | * ) 252 | * ); 253 | * ``` 254 | * 255 | * Render `$items` as an ASCII table: 256 | * 257 | * ``` 258 | * WP_CLI\Utils\format_items( 'table', $items, array( 'key', 'value' ) ); 259 | * 260 | * # +-----+-------+ 261 | * # | key | value | 262 | * # +-----+-------+ 263 | * # | foo | bar | 264 | * # +-----+-------+ 265 | * ``` 266 | * 267 | * Or render `$items` as YAML: 268 | * 269 | * ``` 270 | * WP_CLI\Utils\format_items( 'yaml', $items, array( 'key', 'value' ) ); 271 | * 272 | * # --- 273 | * # - 274 | * # key: foo 275 | * # value: bar 276 | * ``` 277 | * 278 | * @access public 279 | * @category Output 280 | * 281 | * @param string $format Format to use: 'table', 'json', 'csv', 'yaml', 'ids', 'count' 282 | * @param array $items An array of items to output. 283 | * @param array|string $fields Named fields for each item of data. Can be array or comma-separated list. 284 | * @return null 285 | */ 286 | function format_items( $format, $items, $fields ) { 287 | $assoc_args = compact( 'format', 'fields' ); 288 | $formatter = new \WP_CLI\Formatter( $assoc_args ); 289 | $formatter->display_items( $items ); 290 | } 291 | 292 | /** 293 | * Write data as CSV to a given file. 294 | * 295 | * @access public 296 | * 297 | * @param resource $fd File descriptor 298 | * @param array $rows Array of rows to output 299 | * @param array $headers List of CSV columns (optional) 300 | */ 301 | function write_csv( $fd, $rows, $headers = array() ) { 302 | if ( ! empty( $headers ) ) { 303 | fputcsv( $fd, $headers ); 304 | } 305 | 306 | foreach ( $rows as $row ) { 307 | if ( ! empty( $headers ) ) { 308 | $row = pick_fields( $row, $headers ); 309 | } 310 | 311 | fputcsv( $fd, array_values( $row ) ); 312 | } 313 | } 314 | 315 | /** 316 | * Pick fields from an associative array or object. 317 | * 318 | * @param array|object Associative array or object to pick fields from 319 | * @param array List of fields to pick 320 | * @return array 321 | */ 322 | function pick_fields( $item, $fields ) { 323 | $item = (object) $item; 324 | 325 | $values = array(); 326 | 327 | foreach ( $fields as $field ) { 328 | $values[ $field ] = isset( $item->$field ) ? $item->$field : null; 329 | } 330 | 331 | return $values; 332 | } 333 | 334 | /** 335 | * Launch system's $EDITOR for the user to edit some text. 336 | * 337 | * @access public 338 | * @category Input 339 | * 340 | * @param string $content Some form of text to edit (e.g. post content) 341 | * @return string|bool Edited text, if file is saved from editor; false, if no change to file. 342 | */ 343 | function launch_editor_for_input( $input, $filename = 'WP-CLI' ) { 344 | 345 | $tmpdir = get_temp_dir(); 346 | 347 | do { 348 | $tmpfile = basename( $filename ); 349 | $tmpfile = preg_replace( '|\.[^.]*$|', '', $tmpfile ); 350 | $tmpfile .= '-' . substr( md5( rand() ), 0, 6 ); 351 | $tmpfile = $tmpdir . $tmpfile . '.tmp'; 352 | $fp = @fopen( $tmpfile, 'x' ); 353 | if ( ! $fp && is_writable( $tmpdir ) && file_exists( $tmpfile ) ) { 354 | $tmpfile = ''; 355 | continue; 356 | } 357 | if ( $fp ) { 358 | fclose( $fp ); 359 | } 360 | } while( ! $tmpfile ); 361 | 362 | if ( ! $tmpfile ) { 363 | \WP_CLI::error( 'Error creating temporary file.' ); 364 | } 365 | 366 | $output = ''; 367 | file_put_contents( $tmpfile, $input ); 368 | 369 | $editor = getenv( 'EDITOR' ); 370 | if ( !$editor ) { 371 | if ( isset( $_SERVER['OS'] ) && false !== strpos( $_SERVER['OS'], 'indows' ) ) 372 | $editor = 'notepad'; 373 | else 374 | $editor = 'vi'; 375 | } 376 | 377 | $descriptorspec = array( STDIN, STDOUT, STDERR ); 378 | $process = proc_open( "$editor " . escapeshellarg( $tmpfile ), $descriptorspec, $pipes ); 379 | $r = proc_close( $process ); 380 | if ( $r ) { 381 | exit( $r ); 382 | } 383 | 384 | $output = file_get_contents( $tmpfile ); 385 | 386 | unlink( $tmpfile ); 387 | 388 | if ( $output === $input ) 389 | return false; 390 | 391 | return $output; 392 | } 393 | 394 | /** 395 | * @param string MySQL host string, as defined in wp-config.php 396 | * @return array 397 | */ 398 | function mysql_host_to_cli_args( $raw_host ) { 399 | $assoc_args = array(); 400 | 401 | $host_parts = explode( ':', $raw_host ); 402 | if ( count( $host_parts ) == 2 ) { 403 | list( $assoc_args['host'], $extra ) = $host_parts; 404 | $extra = trim( $extra ); 405 | if ( is_numeric( $extra ) ) { 406 | $assoc_args['port'] = intval( $extra ); 407 | $assoc_args['protocol'] = 'tcp'; 408 | } else if ( $extra !== '' ) { 409 | $assoc_args['socket'] = $extra; 410 | } 411 | } else { 412 | $assoc_args['host'] = $raw_host; 413 | } 414 | 415 | return $assoc_args; 416 | } 417 | 418 | function run_mysql_command( $cmd, $assoc_args, $descriptors = null ) { 419 | if ( !$descriptors ) 420 | $descriptors = array( STDIN, STDOUT, STDERR ); 421 | 422 | if ( isset( $assoc_args['host'] ) ) { 423 | $assoc_args = array_merge( $assoc_args, mysql_host_to_cli_args( $assoc_args['host'] ) ); 424 | } 425 | 426 | $pass = $assoc_args['pass']; 427 | unset( $assoc_args['pass'] ); 428 | 429 | $old_pass = getenv( 'MYSQL_PWD' ); 430 | putenv( 'MYSQL_PWD=' . $pass ); 431 | 432 | $final_cmd = $cmd . assoc_args_to_str( $assoc_args ); 433 | 434 | $proc = proc_open( $final_cmd, $descriptors, $pipes ); 435 | if ( !$proc ) 436 | exit(1); 437 | 438 | $r = proc_close( $proc ); 439 | 440 | putenv( 'MYSQL_PWD=' . $old_pass ); 441 | 442 | if ( $r ) exit( $r ); 443 | } 444 | 445 | /** 446 | * Render PHP or other types of files using Mustache templates. 447 | * 448 | * IMPORTANT: Automatic HTML escaping is disabled! 449 | */ 450 | function mustache_render( $template_name, $data = array() ) { 451 | if ( ! file_exists( $template_name ) ) 452 | $template_name = WP_CLI_ROOT . "/templates/$template_name"; 453 | 454 | $template = file_get_contents( $template_name ); 455 | 456 | $m = new \Mustache_Engine( array( 457 | 'escape' => function ( $val ) { return $val; } 458 | ) ); 459 | 460 | return $m->render( $template, $data ); 461 | } 462 | 463 | /** 464 | * Create a progress bar to display percent completion of a given operation. 465 | * 466 | * Progress bar is written to STDOUT, and disabled when command is piped. Progress 467 | * advances with `$progress->tick()`, and completes with `$progress->finish()`. 468 | * Process bar also indicates elapsed time and expected total time. 469 | * 470 | * ``` 471 | * # `wp user generate` ticks progress bar each time a new user is created. 472 | * # 473 | * # $ wp user generate --count=500 474 | * # Generating users 22 % [=======> ] 0:05 / 0:23 475 | * 476 | * $progress = \WP_CLI\Utils\make_progress_bar( 'Generating users', $count ); 477 | * for ( $i = 0; $i < $count; $i++ ) { 478 | * // uses wp_insert_user() to insert the user 479 | * $progress->tick(); 480 | * } 481 | * $progress->finish(); 482 | * ``` 483 | * 484 | * @access public 485 | * @category Output 486 | * 487 | * @param string $message Text to display before the progress bar. 488 | * @param integer $count Total number of ticks to be performed. 489 | * @return cli\progress\Bar|WP_CLI\NoOp 490 | */ 491 | function make_progress_bar( $message, $count ) { 492 | if ( \cli\Shell::isPiped() ) 493 | return new \WP_CLI\NoOp; 494 | 495 | return new \cli\progress\Bar( $message, $count ); 496 | } 497 | 498 | function parse_url( $url ) { 499 | $url_parts = \parse_url( $url ); 500 | 501 | if ( !isset( $url_parts['scheme'] ) ) { 502 | $url_parts = parse_url( 'http://' . $url ); 503 | } 504 | 505 | return $url_parts; 506 | } 507 | 508 | /** 509 | * Check if we're running in a Windows environment (cmd.exe). 510 | */ 511 | function is_windows() { 512 | return strtoupper(substr(PHP_OS, 0, 3)) === 'WIN'; 513 | } 514 | 515 | /** 516 | * Replace magic constants in some PHP source code. 517 | * 518 | * @param string $source The PHP code to manipulate. 519 | * @param string $path The path to use instead of the magic constants 520 | */ 521 | function replace_path_consts( $source, $path ) { 522 | $replacements = array( 523 | '__FILE__' => "'$path'", 524 | '__DIR__' => "'" . dirname( $path ) . "'" 525 | ); 526 | 527 | $old = array_keys( $replacements ); 528 | $new = array_values( $replacements ); 529 | 530 | return str_replace( $old, $new, $source ); 531 | } 532 | 533 | /** 534 | * Make a HTTP request to a remote URL. 535 | * 536 | * Wraps the Requests HTTP library to ensure every request includes a cert. 537 | * 538 | * ``` 539 | * # `wp core download` verifies the hash for a downloaded WordPress archive 540 | * 541 | * $md5_response = Utils\http_request( 'GET', $download_url . '.md5' ); 542 | * if ( 20 != substr( $md5_response->status_code, 0, 2 ) ) { 543 | * WP_CLI::error( "Couldn't access md5 hash for release (HTTP code {$response->status_code})" ); 544 | * } 545 | * ``` 546 | * 547 | * @access public 548 | * 549 | * @param string $method HTTP method (GET, POST, DELETE, etc.) 550 | * @param string $url URL to make the HTTP request to. 551 | * @param array $headers Add specific headers to the request. 552 | * @param array $options 553 | * @return object 554 | */ 555 | function http_request( $method, $url, $data = null, $headers = array(), $options = array() ) { 556 | 557 | $cert_path = '/rmccue/requests/library/Requests/Transport/cacert.pem'; 558 | if ( inside_phar() ) { 559 | // cURL can't read Phar archives 560 | $options['verify'] = extract_from_phar( 561 | WP_CLI_ROOT . '/vendor' . $cert_path ); 562 | } else { 563 | foreach( get_vendor_paths() as $vendor_path ) { 564 | if ( file_exists( $vendor_path . $cert_path ) ) { 565 | $options['verify'] = $vendor_path . $cert_path; 566 | break; 567 | } 568 | } 569 | if ( empty( $options['verify'] ) ){ 570 | WP_CLI::error_log( "Cannot find SSL certificate." ); 571 | } 572 | } 573 | 574 | try { 575 | $request = \Requests::request( $url, $headers, $data, $method, $options ); 576 | return $request; 577 | } catch( \Requests_Exception $ex ) { 578 | // Handle SSL certificate issues gracefully 579 | \WP_CLI::warning( $ex->getMessage() ); 580 | $options['verify'] = false; 581 | try { 582 | return \Requests::request( $url, $headers, $data, $method, $options ); 583 | } catch( \Requests_Exception $ex ) { 584 | \WP_CLI::error( $ex->getMessage() ); 585 | } 586 | } 587 | } 588 | 589 | /** 590 | * Increments a version string using the "x.y.z-pre" format 591 | * 592 | * Can increment the major, minor or patch number by one 593 | * If $new_version == "same" the version string is not changed 594 | * If $new_version is not a known keyword, it will be used as the new version string directly 595 | * 596 | * @param string $current_version 597 | * @param string $new_version 598 | * @return string 599 | */ 600 | function increment_version( $current_version, $new_version ) { 601 | // split version assuming the format is x.y.z-pre 602 | $current_version = explode( '-', $current_version, 2 ); 603 | $current_version[0] = explode( '.', $current_version[0] ); 604 | 605 | switch ( $new_version ) { 606 | case 'same': 607 | // do nothing 608 | break; 609 | 610 | case 'patch': 611 | $current_version[0][2]++; 612 | 613 | $current_version = array( $current_version[0] ); // drop possible pre-release info 614 | break; 615 | 616 | case 'minor': 617 | $current_version[0][1]++; 618 | $current_version[0][2] = 0; 619 | 620 | $current_version = array( $current_version[0] ); // drop possible pre-release info 621 | break; 622 | 623 | case 'major': 624 | $current_version[0][0]++; 625 | $current_version[0][1] = 0; 626 | $current_version[0][2] = 0; 627 | 628 | $current_version = array( $current_version[0] ); // drop possible pre-release info 629 | break; 630 | 631 | default: // not a keyword 632 | $current_version = array( array( $new_version ) ); 633 | break; 634 | } 635 | 636 | // reconstruct version string 637 | $current_version[0] = implode( '.', $current_version[0] ); 638 | $current_version = implode( '-', $current_version ); 639 | 640 | return $current_version; 641 | } 642 | 643 | /** 644 | * Compare two version strings to get the named semantic version. 645 | * 646 | * @access public 647 | * 648 | * @param string $new_version 649 | * @param string $original_version 650 | * @return string $name 'major', 'minor', 'patch' 651 | */ 652 | function get_named_sem_ver( $new_version, $original_version ) { 653 | 654 | if ( ! Comparator::greaterThan( $new_version, $original_version ) ) { 655 | return ''; 656 | } 657 | 658 | $parts = explode( '-', $original_version ); 659 | list( $major, $minor, $patch ) = explode( '.', $parts[0] ); 660 | 661 | if ( Semver::satisfies( $new_version, "{$major}.{$minor}.x" ) ) { 662 | return 'patch'; 663 | } else if ( Semver::satisfies( $new_version, "{$major}.x.x" ) ) { 664 | return 'minor'; 665 | } else { 666 | return 'major'; 667 | } 668 | } 669 | 670 | /** 671 | * Return the flag value or, if it's not set, the $default value. 672 | * 673 | * Because flags can be negated (e.g. --no-quiet to negate --quiet), this 674 | * function provides a safer alternative to using 675 | * `isset( $assoc_args['quiet'] )` or similar. 676 | * 677 | * @access public 678 | * @category Input 679 | * 680 | * @param array $assoc_args Arguments array. 681 | * @param string $flag Flag to get the value. 682 | * @param mixed $default Default value for the flag. Default: NULL 683 | * @return mixed 684 | */ 685 | function get_flag_value( $assoc_args, $flag, $default = null ) { 686 | return isset( $assoc_args[ $flag ] ) ? $assoc_args[ $flag ] : $default; 687 | } 688 | 689 | /** 690 | * Get the system's temp directory. Warns user if it isn't writable. 691 | * 692 | * @access public 693 | * @category System 694 | * 695 | * @return string 696 | */ 697 | function get_temp_dir() { 698 | static $temp = ''; 699 | 700 | $trailingslashit = function( $path ) { 701 | return rtrim( $path ) . '/'; 702 | }; 703 | 704 | if ( $temp ) 705 | return $trailingslashit( $temp ); 706 | 707 | if ( function_exists( 'sys_get_temp_dir' ) ) { 708 | $temp = sys_get_temp_dir(); 709 | } else if ( ini_get( 'upload_tmp_dir' ) ) { 710 | $temp = ini_get( 'upload_tmp_dir' ); 711 | } else { 712 | $temp = '/tmp/'; 713 | } 714 | 715 | if ( ! @is_writable( $temp ) ) { 716 | WP_CLI::warning( "Temp directory isn't writable: {$temp}" ); 717 | } 718 | 719 | return $trailingslashit( $temp ); 720 | } 721 | 722 | /** 723 | * Parse a SSH url for its host, port, and path. 724 | * 725 | * Similar to parse_url(), but adds support for defined SSH aliases. 726 | * 727 | * ``` 728 | * host OR host/path/to/wordpress OR host:port/path/to/wordpress 729 | * ``` 730 | * 731 | * @access public 732 | * 733 | * @return mixed 734 | */ 735 | function parse_ssh_url( $url, $component = -1 ) { 736 | preg_match( '#^([^:/~]+)(:([\d]+))?((/|~)(.+))?$#', $url, $matches ); 737 | $bits = array(); 738 | foreach( array( 739 | 1 => 'host', 740 | 3 => 'port', 741 | 4 => 'path', 742 | ) as $i => $key ) { 743 | if ( ! empty( $matches[ $i ] ) ) { 744 | $bits[ $key ] = $matches[ $i ]; 745 | } 746 | } 747 | switch ( $component ) { 748 | case PHP_URL_HOST: 749 | return isset( $bits['host'] ) ? $bits['host'] : null; 750 | case PHP_URL_PATH: 751 | return isset( $bits['path'] ) ? $bits['path'] : null; 752 | case PHP_URL_PORT: 753 | return isset( $bits['port'] ) ? $bits['port'] : null; 754 | default: 755 | return $bits; 756 | } 757 | } 758 | -------------------------------------------------------------------------------- /features/extra/no-mail.php: -------------------------------------------------------------------------------- 1 | Given( '/^an empty directory$/', 8 | function ( $world ) { 9 | $world->create_run_dir(); 10 | } 11 | ); 12 | 13 | $steps->Given( '/^an empty cache/', 14 | function ( $world ) { 15 | $world->variables['SUITE_CACHE_DIR'] = FeatureContext::create_cache_dir(); 16 | } 17 | ); 18 | 19 | $steps->Given( '/^an? ([^\s]+) file:$/', 20 | function ( $world, $path, PyStringNode $content ) { 21 | $content = (string) $content . "\n"; 22 | $full_path = $world->variables['RUN_DIR'] . "/$path"; 23 | Process::create( \WP_CLI\utils\esc_cmd( 'mkdir -p %s', dirname( $full_path ) ) )->run_check(); 24 | file_put_contents( $full_path, $content ); 25 | } 26 | ); 27 | 28 | $steps->Given( '/^WP files$/', 29 | function ( $world ) { 30 | $world->download_wp(); 31 | } 32 | ); 33 | 34 | $steps->Given( '/^wp-config\.php$/', 35 | function ( $world ) { 36 | $world->create_config(); 37 | } 38 | ); 39 | 40 | $steps->Given( '/^a database$/', 41 | function ( $world ) { 42 | $world->create_db(); 43 | } 44 | ); 45 | 46 | $steps->Given( '/^a WP install$/', 47 | function ( $world ) { 48 | $world->install_wp(); 49 | } 50 | ); 51 | 52 | $steps->Given( "/^a WP install in '([^\s]+)'$/", 53 | function ( $world, $subdir ) { 54 | $world->install_wp( $subdir ); 55 | } 56 | ); 57 | 58 | $steps->Given( '/^a WP multisite (subdirectory|subdomain)?\s?install$/', 59 | function ( $world, $type = 'subdirectory' ) { 60 | $world->install_wp(); 61 | $subdomains = ! empty( $type ) && 'subdomain' === $type ? 1 : 0; 62 | $world->proc( 'wp core install-network', array( 'title' => 'WP CLI Network', 'subdomains' => $subdomains ) )->run_check(); 63 | } 64 | ); 65 | 66 | $steps->Given( '/^these installed and active plugins:$/', 67 | function( $world, $stream ) { 68 | $plugins = implode( ' ', array_map( 'trim', explode( PHP_EOL, (string)$stream ) ) ); 69 | $world->proc( "wp plugin install $plugins --activate" )->run_check(); 70 | } 71 | ); 72 | 73 | $steps->Given( '/^a custom wp-content directory$/', 74 | function ( $world ) { 75 | $wp_config_path = $world->variables['RUN_DIR'] . "/wp-config.php"; 76 | 77 | $wp_config_code = file_get_contents( $wp_config_path ); 78 | 79 | $world->move_files( 'wp-content', 'my-content' ); 80 | $world->add_line_to_wp_config( $wp_config_code, 81 | "define( 'WP_CONTENT_DIR', dirname(__FILE__) . '/my-content' );" ); 82 | 83 | $world->move_files( 'my-content/plugins', 'my-plugins' ); 84 | $world->add_line_to_wp_config( $wp_config_code, 85 | "define( 'WP_PLUGIN_DIR', __DIR__ . '/my-plugins' );" ); 86 | 87 | file_put_contents( $wp_config_path, $wp_config_code ); 88 | } 89 | ); 90 | 91 | $steps->Given( '/^download:$/', 92 | function ( $world, TableNode $table ) { 93 | foreach ( $table->getHash() as $row ) { 94 | $path = $world->replace_variables( $row['path'] ); 95 | if ( file_exists( $path ) ) { 96 | // assume it's the same file and skip re-download 97 | continue; 98 | } 99 | 100 | Process::create( \WP_CLI\Utils\esc_cmd( 'curl -sSL %s > %s', $row['url'], $path ) )->run_check(); 101 | } 102 | } 103 | ); 104 | 105 | $steps->Given( '/^save (STDOUT|STDERR) ([\'].+[^\'])?as \{(\w+)\}$/', 106 | function ( $world, $stream, $output_filter, $key ) { 107 | 108 | $stream = strtolower( $stream ); 109 | 110 | if ( $output_filter ) { 111 | $output_filter = '/' . trim( str_replace( '%s', '(.+[^\b])', $output_filter ), "' " ) . '/'; 112 | if ( false !== preg_match( $output_filter, $world->result->$stream, $matches ) ) 113 | $output = array_pop( $matches ); 114 | else 115 | $output = ''; 116 | } else { 117 | $output = $world->result->$stream; 118 | } 119 | $world->variables[ $key ] = trim( $output, "\n" ); 120 | } 121 | ); 122 | 123 | $steps->Given( '/^a new Phar(?: with version "([^"]+)")$/', 124 | function ( $world, $version ) { 125 | $world->build_phar( $version ); 126 | } 127 | ); 128 | 129 | $steps->Given( '/^save the (.+) file ([\'].+[^\'])?as \{(\w+)\}$/', 130 | function ( $world, $filepath, $output_filter, $key ) { 131 | $full_file = file_get_contents( $world->replace_variables( $filepath ) ); 132 | 133 | if ( $output_filter ) { 134 | $output_filter = '/' . trim( str_replace( '%s', '(.+[^\b])', $output_filter ), "' " ) . '/'; 135 | if ( false !== preg_match( $output_filter, $full_file, $matches ) ) 136 | $output = array_pop( $matches ); 137 | else 138 | $output = ''; 139 | } else { 140 | $output = $full_file; 141 | } 142 | $world->variables[ $key ] = trim( $output, "\n" ); 143 | } 144 | ); 145 | 146 | $steps->Given('/^a misconfigured WP_CONTENT_DIR constant directory$/', 147 | function($world) { 148 | $wp_config_path = $world->variables['RUN_DIR'] . "/wp-config.php"; 149 | 150 | $wp_config_code = file_get_contents( $wp_config_path ); 151 | 152 | $world->add_line_to_wp_config( $wp_config_code, 153 | "define( 'WP_CONTENT_DIR', '' );" ); 154 | 155 | file_put_contents( $wp_config_path, $wp_config_code ); 156 | } 157 | ); -------------------------------------------------------------------------------- /features/steps/then.php: -------------------------------------------------------------------------------- 1 | Then( '/^the return code should be (\d+)$/', 7 | function ( $world, $return_code ) { 8 | if ( $return_code != $world->result->return_code ) { 9 | throw new RuntimeException( $world->result ); 10 | } 11 | } 12 | ); 13 | 14 | $steps->Then( '/^(STDOUT|STDERR) should (be|contain|not contain):$/', 15 | function ( $world, $stream, $action, PyStringNode $expected ) { 16 | 17 | $stream = strtolower( $stream ); 18 | 19 | $expected = $world->replace_variables( (string) $expected ); 20 | 21 | checkString( $world->result->$stream, $expected, $action, $world->result ); 22 | } 23 | ); 24 | 25 | $steps->Then( '/^(STDOUT|STDERR) should be a number$/', 26 | function ( $world, $stream ) { 27 | 28 | $stream = strtolower( $stream ); 29 | 30 | assertNumeric( trim( $world->result->$stream, "\n" ) ); 31 | } 32 | ); 33 | 34 | $steps->Then( '/^(STDOUT|STDERR) should not be a number$/', 35 | function ( $world, $stream ) { 36 | 37 | $stream = strtolower( $stream ); 38 | 39 | assertNotNumeric( trim( $world->result->$stream, "\n" ) ); 40 | } 41 | ); 42 | 43 | $steps->Then( '/^STDOUT should be a table containing rows:$/', 44 | function ( $world, TableNode $expected ) { 45 | $output = $world->result->stdout; 46 | $actual_rows = explode( "\n", rtrim( $output, "\n" ) ); 47 | 48 | $expected_rows = array(); 49 | foreach ( $expected->getRows() as $row ) { 50 | $expected_rows[] = $world->replace_variables( implode( "\t", $row ) ); 51 | } 52 | 53 | compareTables( $expected_rows, $actual_rows, $output ); 54 | } 55 | ); 56 | 57 | $steps->Then( '/^STDOUT should end with a table containing rows:$/', 58 | function ( $world, TableNode $expected ) { 59 | $output = $world->result->stdout; 60 | $actual_rows = explode( "\n", rtrim( $output, "\n" ) ); 61 | 62 | $expected_rows = array(); 63 | foreach ( $expected->getRows() as $row ) { 64 | $expected_rows[] = $world->replace_variables( implode( "\t", $row ) ); 65 | } 66 | 67 | $start = array_search( $expected_rows[0], $actual_rows ); 68 | 69 | if ( false === $start ) 70 | throw new \Exception( $world->result ); 71 | 72 | compareTables( $expected_rows, array_slice( $actual_rows, $start ), $output ); 73 | } 74 | ); 75 | 76 | $steps->Then( '/^STDOUT should be JSON containing:$/', 77 | function ( $world, PyStringNode $expected ) { 78 | $output = $world->result->stdout; 79 | $expected = $world->replace_variables( (string) $expected ); 80 | 81 | if ( !checkThatJsonStringContainsJsonString( $output, $expected ) ) { 82 | throw new \Exception( $world->result ); 83 | } 84 | }); 85 | 86 | $steps->Then( '/^STDOUT should be a JSON array containing:$/', 87 | function ( $world, PyStringNode $expected ) { 88 | $output = $world->result->stdout; 89 | $expected = $world->replace_variables( (string) $expected ); 90 | 91 | $actualValues = json_decode( $output ); 92 | $expectedValues = json_decode( $expected ); 93 | 94 | $missing = array_diff( $expectedValues, $actualValues ); 95 | if ( !empty( $missing ) ) { 96 | throw new \Exception( $world->result ); 97 | } 98 | }); 99 | 100 | $steps->Then( '/^STDOUT should be CSV containing:$/', 101 | function ( $world, TableNode $expected ) { 102 | $output = $world->result->stdout; 103 | 104 | $expected_rows = $expected->getRows(); 105 | foreach ( $expected as &$row ) { 106 | foreach ( $row as &$value ) { 107 | $value = $world->replace_variables( $value ); 108 | } 109 | } 110 | 111 | if ( ! checkThatCsvStringContainsValues( $output, $expected_rows ) ) 112 | throw new \Exception( $world->result ); 113 | } 114 | ); 115 | 116 | $steps->Then( '/^STDOUT should be YAML containing:$/', 117 | function ( $world, PyStringNode $expected ) { 118 | $output = $world->result->stdout; 119 | $expected = $world->replace_variables( (string) $expected ); 120 | 121 | if ( !checkThatYamlStringContainsYamlString( $output, $expected ) ) { 122 | throw new \Exception( $world->result ); 123 | } 124 | }); 125 | 126 | $steps->Then( '/^(STDOUT|STDERR) should be empty$/', 127 | function ( $world, $stream ) { 128 | 129 | $stream = strtolower( $stream ); 130 | 131 | if ( !empty( $world->result->$stream ) ) { 132 | throw new \Exception( $world->result ); 133 | } 134 | } 135 | ); 136 | 137 | $steps->Then( '/^(STDOUT|STDERR) should not be empty$/', 138 | function ( $world, $stream ) { 139 | 140 | $stream = strtolower( $stream ); 141 | 142 | if ( '' === rtrim( $world->result->$stream, "\n" ) ) { 143 | throw new Exception( $world->result ); 144 | } 145 | } 146 | ); 147 | 148 | $steps->Then( '/^the (.+) (file|directory) should (exist|not exist|be:|contain:|not contain:)$/', 149 | function ( $world, $path, $type, $action, $expected = null ) { 150 | $path = $world->replace_variables( $path ); 151 | 152 | // If it's a relative path, make it relative to the current test dir 153 | if ( '/' !== $path[0] ) 154 | $path = $world->variables['RUN_DIR'] . "/$path"; 155 | 156 | if ( 'file' == $type ) { 157 | $test = 'file_exists'; 158 | } else if ( 'directory' == $type ) { 159 | $test = 'is_dir'; 160 | } 161 | 162 | switch ( $action ) { 163 | case 'exist': 164 | if ( ! $test( $path ) ) { 165 | throw new Exception( $world->result ); 166 | } 167 | break; 168 | case 'not exist': 169 | if ( $test( $path ) ) { 170 | throw new Exception( $world->result ); 171 | } 172 | break; 173 | default: 174 | if ( ! $test( $path ) ) { 175 | throw new Exception( "$path doesn't exist." ); 176 | } 177 | $action = substr( $action, 0, -1 ); 178 | $expected = $world->replace_variables( (string) $expected ); 179 | if ( 'file' == $type ) { 180 | $contents = file_get_contents( $path ); 181 | } else if ( 'directory' == $type ) { 182 | $files = glob( rtrim( $path, '/' ) . '/*' ); 183 | foreach( $files as &$file ) { 184 | $file = str_replace( $path . '/', '', $file ); 185 | } 186 | $contents = implode( PHP_EOL, $files ); 187 | } 188 | checkString( $contents, $expected, $action ); 189 | } 190 | } 191 | ); 192 | 193 | -------------------------------------------------------------------------------- /features/steps/when.php: -------------------------------------------------------------------------------- 1 | 'run_check', 10 | 'try' => 'run' 11 | ); 12 | $method = $map[ $mode ]; 13 | 14 | return $proc->$method(); 15 | } 16 | 17 | $steps->When( '/^I launch in the background `([^`]+)`$/', 18 | function ( $world, $cmd ) { 19 | $world->background_proc( $cmd ); 20 | } 21 | ); 22 | 23 | $steps->When( '/^I (run|try) `([^`]+)`$/', 24 | function ( $world, $mode, $cmd ) { 25 | $cmd = $world->replace_variables( $cmd ); 26 | $world->result = invoke_proc( $world->proc( $cmd ), $mode ); 27 | } 28 | ); 29 | 30 | $steps->When( "/^I (run|try) `([^`]+)` from '([^\s]+)'$/", 31 | function ( $world, $mode, $cmd, $subdir ) { 32 | $cmd = $world->replace_variables( $cmd ); 33 | $world->result = invoke_proc( $world->proc( $cmd, array(), $subdir ), $mode ); 34 | } 35 | ); 36 | 37 | $steps->When( '/^I (run|try) the previous command again$/', 38 | function ( $world, $mode ) { 39 | if ( !isset( $world->result ) ) 40 | throw new \Exception( 'No previous command.' ); 41 | 42 | $proc = Process::create( $world->result->command, $world->result->cwd, $world->result->env ); 43 | $world->result = invoke_proc( $proc, $mode ); 44 | } 45 | ); 46 | 47 | -------------------------------------------------------------------------------- /phpcs.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | A custom set of rules to check for a Awethemes WordPress project 4 | 5 | 6 | 7 | 8 | tests/* 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /utils/behat-tags.php: -------------------------------------------------------------------------------- 1 | ' ) 38 | ); 39 | 40 | # Skip Github API tests by default because of rate limiting. See https://github.com/wp-cli/wp-cli/issues/1612 41 | $skip_tags[] = '@github-api'; 42 | 43 | if ( !empty( $skip_tags ) ) { 44 | echo '--tags=~' . implode( '&&~', $skip_tags ); 45 | } 46 | 47 | --------------------------------------------------------------------------------