diff --git a/bin/run-phpstan-tests b/bin/run-phpstan-tests new file mode 100755 index 000000000..21f276900 --- /dev/null +++ b/bin/run-phpstan-tests @@ -0,0 +1,7 @@ +#!/bin/sh + +# Run the code style check only if a configuration file exists. +if [ -f "phpstan.dist.neon" ] || [ -f "phpstan.neon.dist" ] || [ -f "phpstan.neon" ] +then + vendor/bin/phpstan --memory-limit=2048M analyse "$@" +fi diff --git a/composer.json b/composer.json index 4826e0136..8e678002b 100644 --- a/composer.json +++ b/composer.json @@ -15,6 +15,9 @@ "php-parallel-lint/php-console-highlighter": "^1.0", "php-parallel-lint/php-parallel-lint": "^1.3.1", "phpcompatibility/php-compatibility": "dev-develop", + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.26", + "szepeviktor/phpstan-wordpress": "^v1.3.5", "wp-cli/config-command": "^1 || ^2", "wp-cli/core-command": "^1 || ^2", "wp-cli/eval-command": "^1 || ^2", @@ -28,7 +31,8 @@ "config": { "allow-plugins": { "dealerdirect/phpcodesniffer-composer-installer": true, - "johnpbloch/wordpress-core-installer": true + "johnpbloch/wordpress-core-installer": true, + "phpstan/extension-installer": true }, "sort-packages": true, "lock": false @@ -63,7 +67,8 @@ "bin/run-linter-tests", "bin/run-php-unit-tests", "bin/run-phpcs-tests", - "bin/run-phpcbf-cleanup" + "bin/run-phpcbf-cleanup", + "bin/run-phpstan-tests" ], "scripts": { "behat": "run-behat-tests", @@ -71,11 +76,13 @@ "lint": "run-linter-tests", "phpcs": "run-phpcs-tests", "phpcbf": "run-phpcbf-cleanup", + "phpstan": "run-phpstan-tests", "phpunit": "run-php-unit-tests", "prepare-tests": "install-package-tests", "test": [ "@lint", "@phpcs", + "@phpstan", "@phpunit", "@behat" ] diff --git a/phpcs.xml.dist b/phpcs.xml.dist index 966056354..24888b4b8 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -59,6 +59,8 @@ ############################################################################# --> + tests/phpstan/scan-files.php + diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 000000000..01b2d732d --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,19 @@ +parameters: + level: 6 + paths: + - src + - tests + scanDirectories: + - vendor/wp-cli/wp-cli + - vendor/phpunit/php-code-coverage + - vendor/behat/behat + scanFiles: + - tests/phpstan/scan-files.php + treatPhpDocTypesAsCertain: false + dynamicConstantNames: + - WP_DEBUG + - WP_DEBUG_LOG + - WP_DEBUG_DISPLAY + ignoreErrors: + # Needs fixing in WP-CLI. + - message: '#Parameter \#1 \$cmd of function WP_CLI\\Utils\\esc_cmd expects array#' diff --git a/src/Context/FeatureContext.php b/src/Context/FeatureContext.php index df28bb3dd..150f0d30b 100644 --- a/src/Context/FeatureContext.php +++ b/src/Context/FeatureContext.php @@ -3,13 +3,17 @@ namespace WP_CLI\Tests\Context; use Behat\Behat\Context\SnippetAcceptingContext; +use Behat\Behat\EventDispatcher\Event\OutlineTested; use Behat\Behat\Hook\Scope\AfterScenarioScope; use Behat\Behat\Hook\Scope\BeforeScenarioScope; +use Behat\Behat\Hook\Scope\FeatureScope; +use Behat\Behat\Hook\Scope\ScenarioScope; use Behat\Testwork\Hook\Scope\AfterSuiteScope; use Behat\Testwork\Hook\Scope\BeforeSuiteScope; use Behat\Behat\Hook\Scope\AfterFeatureScope; use Behat\Behat\Hook\Scope\BeforeFeatureScope; use Behat\Behat\Hook\Scope\BeforeStepScope; +use Behat\Testwork\Hook\Scope\HookScope; use SebastianBergmann\CodeCoverage\Report\Clover; use SebastianBergmann\CodeCoverage\Driver\Selector; use SebastianBergmann\CodeCoverage\Driver\Xdebug; @@ -19,6 +23,7 @@ use RuntimeException; use DirectoryIterator; use WP_CLI\Process; +use WP_CLI\ProcessRun; use WP_CLI\Utils; use WP_CLI\WpOrgApi; @@ -33,53 +38,73 @@ class FeatureContext implements SnippetAcceptingContext { /** * The result of the last command run with `When I run` or `When I try`. Lives until the end of the scenario. + * + * @var ?ProcessRun */ protected $result; /** * The number of emails sent by the last command run with `When I run` or `When I try`. Lives until the end of the scenario. + * + * @var int */ protected $email_sends; /** * The current working directory for scenarios that have a "Given a WP installation" or "Given an empty directory" step. Variable RUN_DIR. Lives until the end of the scenario. + * + * @var ?string */ private static $run_dir; /** * The Directory that 'composer behat' is run from, assumed to always be the top level project folder + * + * @var string */ private static $behat_run_dir; /** * Where WordPress core is downloaded to for caching, and which is copied to RUN_DIR during a "Given a WP installation" step. Lives until manually deleted. + * + * @var string */ private static $cache_dir; /** * The directory that holds the install cache, and which is copied to RUN_DIR during a "Given a WP installation" step. Recreated on each suite run. + * + * @var string */ private static $install_cache_dir; /** * The directory that holds a copy of the sqlite-database-integration plugin, and which is copied to RUN_DIR during a "Given a WP installation" step. Lives until manually deleted. + * + * @var ?string */ private static $sqlite_cache_dir; /** * The directory that the WP-CLI cache (WP_CLI_CACHE_DIR, normally "$HOME/.wp-cli/cache") is set to on a "Given an empty cache" step. * Variable SUITE_CACHE_DIR. Lives until the end of the scenario (or until another "Given an empty cache" step within the scenario). + * + * @var ?string */ private static $suite_cache_dir; /** * Where the current WP-CLI source repository is copied to for Composer-based tests with a "Given a dependency on current wp-cli" step. * Variable COMPOSER_LOCAL_REPOSITORY. Lives until the end of the suite. + * + * @var ?string */ private static $composer_local_repository; /** * The test database settings. All but `dbname` can be set via environment variables. The database is dropped at the start of each scenario and created on a "Given a WP installation" step. + * + * @var array */ private static $db_settings = [ 'dbname' => 'wp_cli_test', @@ -90,16 +115,22 @@ class FeatureContext implements SnippetAcceptingContext { /** * What type of database should WordPress use for the test installations. Default to MySQL + * + * @var string */ private static $db_type = 'mysql'; /** * Name of mysql binary to use (mysql or mariadb). Default to mysql + * + * @var string */ private static $mysql_binary = 'mysql'; /** * Array of background process ids started by the current scenario. Used to terminate them at the end of the scenario. + * + * @var array */ private $running_procs = []; @@ -107,27 +138,77 @@ class FeatureContext implements SnippetAcceptingContext { * Array of variables available as {VARIABLE_NAME}. Some are always set: CORE_CONFIG_SETTINGS, DB_USER, DB_PASSWORD, DB_HOST, SRC_DIR, CACHE_DIR, WP_VERSION-version-latest. * Some are step-dependent: RUN_DIR, SUITE_CACHE_DIR, COMPOSER_LOCAL_REPOSITORY, PHAR_PATH. One is set on use: INVOKE_WP_CLI_WITH_PHP_ARGS-args. * Scenarios can define their own variables using "Given save" steps. Variables are reset for each scenario. + * + * @var array */ public $variables = []; /** * The current feature file and scenario line number as '.'. Used in RUN_DIR and SUITE_CACHE_DIR directory names. Set at the start of each scenario. + * + * @var ?string */ private static $temp_dir_infix; /** - * Settings and variables for WP_CLI_TEST_LOG_RUN_TIMES run time logging. + * Whether to log run times - WP_CLI_TEST_LOG_RUN_TIMES env var. Set on `@BeforeScenario'. + * + * @var false|string */ - private static $log_run_times; // Whether to log run times - WP_CLI_TEST_LOG_RUN_TIMES env var. Set on `@BeforeScenario'. - private static $suite_start_time; // When the suite started, set on `@BeforeScenario'. - private static $output_to; // Where to output log - stdout|error_log. Set on `@BeforeSuite`. - private static $num_top_processes; // Number of processes/methods to output by longest run times. Set on `@BeforeSuite`. - private static $num_top_scenarios; // Number of scenarios to output by longest run times. Set on `@BeforeSuite`. + private static $log_run_times; - private static $scenario_run_times = []; // Scenario run times (top `self::$num_top_scenarios` only). - private static $scenario_count = 0; // Scenario count, incremented on `@AfterScenario`. - private static $proc_method_run_times = []; // Array of run time info for proc methods, keyed by method name and arg, each a 2-element array containing run time and run count. + /** + * When the suite started, set on `@BeforeScenario'. + * + * @var float + */ + private static $suite_start_time; + /** + * Where to output log - stdout|error_log. Set on `@BeforeSuite`. + * + * @var string + */ + private static $output_to; + + /** + * Number of processes/methods to output by longest run times. Set on `@BeforeSuite`. + * + * @var int + */ + private static $num_top_processes; + + /** + * Number of scenarios to output by longest run times. Set on `@BeforeSuite`. + * + * @var int + */ + private static $num_top_scenarios; + + /** + * Scenario run times (top `self::$num_top_scenarios` only). + * + * @var array + */ + private static $scenario_run_times = []; + + /** + * Scenario count, incremented on `@AfterScenario`. + * + * @var int + */ + private static $scenario_count = 0; + + /** + * Array of run time info for proc methods, keyed by method name and arg, each a 2-element array containing run time and run count. + * + * @var array + */ + private static $proc_method_run_times = []; + + /** + * @var array + */ private $mocked_requests = []; /** @@ -154,28 +235,28 @@ class FeatureContext implements SnippetAcceptingContext { /** * @BeforeFeature */ - public static function store_feature( BeforeFeatureScope $scope ) { + public static function store_feature( BeforeFeatureScope $scope ): void { self::$feature = $scope->getFeature(); } /** * @BeforeScenario */ - public function store_scenario( BeforeScenarioScope $scope ) { + public function store_scenario( BeforeScenarioScope $scope ): void { $this->scenario = $scope->getScenario(); } /** * @BeforeStep */ - public function store_step( BeforeStepScope $scope ) { + public function store_step( BeforeStepScope $scope ): void { $this->step_line = $scope->getStep()->getLine(); } /** * @AfterScenario */ - public function forget_scenario( AfterScenarioScope $scope ) { + public function forget_scenario( AfterScenarioScope $scope ): void { $this->step_line = 0; $this->scenario = null; } @@ -183,14 +264,14 @@ public function forget_scenario( AfterScenarioScope $scope ) { /** * @AfterFeature */ - public static function forget_feature( AfterFeatureScope $scope ) { + public static function forget_feature( AfterFeatureScope $scope ): void { self::$feature = null; } /** * @AfterSuite */ - public static function merge_coverage_reports() { + public static function merge_coverage_reports(): void { $with_code_coverage = (string) getenv( 'WP_CLI_TEST_COVERAGE' ); if ( ! \in_array( $with_code_coverage, [ 'true', '1' ], true ) ) { @@ -221,7 +302,7 @@ class_exists( Selector::class ) ? ( new Selector() )->forLineCoverage( $filter ) * * @return string Absolute path to the Composer vendor folder. */ - public static function get_vendor_dir() { + public static function get_vendor_dir(): ?string { static $vendor_folder = null; if ( null !== $vendor_folder ) { @@ -233,9 +314,9 @@ public static function get_vendor_dir() { // wp-cli/wp-cli-tests is a dependency of the current working dir. getcwd() . '/vendor', // wp-cli/wp-cli-tests is the root project. - dirname( dirname( __DIR__ ) ) . '/vendor', + dirname( __DIR__, 2 ) . '/vendor', // wp-cli/wp-cli-tests is a dependency. - dirname( dirname( dirname( dirname( __DIR__ ) ) ) ), + dirname( __DIR__, 4 ), ]; $vendor_folder = ''; @@ -258,7 +339,7 @@ public static function get_vendor_dir() { * * @return string Absolute path to the WP-CLI framework folder. */ - public static function get_framework_dir() { + public static function get_framework_dir(): ?string { static $framework_folder = null; if ( null !== $framework_folder ) { @@ -290,13 +371,12 @@ public static function get_framework_dir() { return $framework_folder; } - /** * Get the path to the WP-CLI binary. * * @return string Absolute path to the WP-CLI binary. */ - public static function get_bin_path() { + public static function get_bin_path(): ?string { static $bin_path = null; if ( null !== $bin_path ) { @@ -325,9 +405,11 @@ public static function get_bin_path() { } /** - * Get the environment variables required for launched `wp` processes + * Get the environment variables required for launched `wp` processes. + * + * @return array */ - private static function get_process_env_variables() { + private static function get_process_env_variables(): array { static $env = null; if ( null !== $env ) { @@ -426,10 +508,10 @@ private static function get_process_env_variables() { /** * Get the internal variables to use within tests. * - * @return array Associative array of internal variables that will be mapped - * into tests. + * @return array Associative array of internal variables that will be mapped + * into tests. */ - private static function get_behat_internal_variables() { + private static function get_behat_internal_variables(): array { static $variables = null; if ( null !== $variables ) { @@ -437,12 +519,12 @@ private static function get_behat_internal_variables() { } $paths = [ - dirname( dirname( dirname( dirname( __DIR__ ) ) ) ) . '/wp-cli/wp-cli/VERSION', - dirname( dirname( dirname( dirname( dirname( __DIR__ ) ) ) ) ) . '/VERSION', - dirname( dirname( __DIR__ ) ) . '/vendor/wp-cli/wp-cli/VERSION', + dirname( __DIR__, 4 ) . '/wp-cli/wp-cli/VERSION', + dirname( __DIR__, 5 ) . '/VERSION', + dirname( __DIR__, 2 ) . '/vendor/wp-cli/wp-cli/VERSION', ]; - $framework_root = dirname( dirname( __DIR__ ) ); + $framework_root = dirname( __DIR__, 2 ); foreach ( $paths as $path ) { if ( file_exists( $path ) ) { $framework_root = (string) realpath( dirname( $path ) ); @@ -452,8 +534,8 @@ private static function get_behat_internal_variables() { $variables = [ 'FRAMEWORK_ROOT' => realpath( $framework_root ), - 'SRC_DIR' => realpath( dirname( dirname( __DIR__ ) ) ), - 'PROJECT_DIR' => realpath( dirname( dirname( dirname( dirname( dirname( __DIR__ ) ) ) ) ) ), + 'SRC_DIR' => realpath( dirname( __DIR__, 2 ) ), + 'PROJECT_DIR' => realpath( dirname( __DIR__, 5 ) ), ]; return $variables; @@ -462,8 +544,10 @@ private static function get_behat_internal_variables() { /** * Download and extract a single copy of the sqlite-database-integration plugin * for use in subsequent WordPress copies + * + * @param string $dir */ - private static function download_sqlite_plugin( $dir ) { + private static function download_sqlite_plugin( $dir ): void { $download_url = 'https://downloads.wordpress.org/plugin/sqlite-database-integration.zip'; $download_location = $dir . '/sqlite-database-integration.zip'; @@ -499,8 +583,10 @@ private static function download_sqlite_plugin( $dir ) { /** * Given a WordPress installation with the sqlite-database-integration plugin, * configure it to use SQLite as the database by placing the db.php dropin file + * + * @param string $dir */ - private static function configure_sqlite( $dir ) { + private static function configure_sqlite( $dir ): void { $db_copy = $dir . '/wp-content/mu-plugins/sqlite-database-integration/db.copy'; $db_dropin = $dir . '/wp-content/db.php'; @@ -526,7 +612,7 @@ private static function configure_sqlite( $dir ) { * 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() { + private static function cache_wp_files(): void { $wp_version = getenv( 'WP_VERSION' ); $wp_version_suffix = ( false !== $wp_version ) ? "-$wp_version" : ''; self::$cache_dir = sys_get_temp_dir() . '/wp-cli-test-core-download-cache' . $wp_version_suffix; @@ -552,7 +638,7 @@ private static function cache_wp_files() { /** * @BeforeSuite */ - public static function prepare( BeforeSuiteScope $scope ) { + public static function prepare( BeforeSuiteScope $scope ): void { // Test performance statistics - useful for detecting slow tests. self::$log_run_times = getenv( 'WP_CLI_TEST_LOG_RUN_TIMES' ); if ( false !== self::$log_run_times ) { @@ -588,7 +674,7 @@ public static function prepare( BeforeSuiteScope $scope ) { /** * @AfterSuite */ - public static function afterSuite( AfterSuiteScope $scope ) { + public static function afterSuite( AfterSuiteScope $scope ): void { if ( self::$composer_local_repository ) { self::remove_dir( self::$composer_local_repository ); self::$composer_local_repository = null; @@ -602,7 +688,7 @@ public static function afterSuite( AfterSuiteScope $scope ) { /** * @BeforeScenario */ - public function beforeScenario( BeforeScenarioScope $scope ) { + public function beforeScenario( BeforeScenarioScope $scope ): void { if ( self::$log_run_times ) { self::log_run_times_before_scenario( $scope ); } @@ -628,7 +714,7 @@ public function beforeScenario( BeforeScenarioScope $scope ) { /** * @AfterScenario */ - public function afterScenario( AfterScenarioScope $scope ) { + public function afterScenario( AfterScenarioScope $scope ): void { if ( self::$run_dir ) { // Remove altered WP install, unless there's an error. @@ -668,8 +754,10 @@ public function afterScenario( AfterScenarioScope $scope ) { /** * Terminate a process and any of its children. + * + * @param int $master_pid */ - private static function terminate_proc( $master_pid ) { + private static function terminate_proc( $master_pid ): void { $output = `ps -o ppid,pid,command | grep $master_pid`; @@ -679,7 +767,7 @@ private static function terminate_proc( $master_pid ) { $child = $matches[2]; if ( (int) $parent === (int) $master_pid ) { - self::terminate_proc( $child ); + self::terminate_proc( (int) $child ); } } } @@ -696,7 +784,7 @@ private static function terminate_proc( $master_pid ) { /** * Create a temporary WP_CLI_CACHE_DIR. Exposed as SUITE_CACHE_DIR in "Given an empty cache" step. */ - public static function create_cache_dir() { + public static function create_cache_dir(): string { if ( self::$suite_cache_dir ) { self::remove_dir( self::$suite_cache_dir ); } @@ -776,6 +864,9 @@ public function __construct() { /** * Replace standard {VARIABLE_NAME} variables and the special {INVOKE_WP_CLI_WITH_PHP_ARGS-args} and {WP_VERSION-version-latest} variables. * Note that standard variable names can only contain uppercase letters, digits and underscores and cannot begin with a digit. + * + * @param string $str + * @return string */ public function replace_variables( $str ) { if ( false !== strpos( $str, '{INVOKE_WP_CLI_WITH_PHP_ARGS-' ) ) { @@ -790,6 +881,9 @@ public function replace_variables( $str ) { /** * Substitute {INVOKE_WP_CLI_WITH_PHP_ARGS-args} variables. + * + * @param string $str + * @return string */ private function replace_invoke_wp_cli_with_php_args( $str ) { static $phar_path = null, $shell_path = null; @@ -802,7 +896,7 @@ private function replace_invoke_wp_cli_with_php_args( $str ) { if ( false !== $bin_dir && file_exists( $bin_dir . '/wp' ) && file_get_contents( $bin_dir . '/wp', false, null, 0, $phar_begin_len ) === $phar_begin ) { $phar_path = $bin_dir . '/wp'; } else { - $src_dir = dirname( dirname( __DIR__ ) ); + $src_dir = dirname( __DIR__, 2 ); $bin_path = $src_dir . '/bin/wp'; $vendor_bin_path = $src_dir . '/vendor/bin/wp'; if ( file_exists( $bin_path ) && is_executable( $bin_path ) ) { @@ -817,7 +911,7 @@ private function replace_invoke_wp_cli_with_php_args( $str ) { $str = preg_replace_callback( '/{INVOKE_WP_CLI_WITH_PHP_ARGS-([^}]*)}/', - function ( $matches ) use ( $phar_path, $shell_path ) { + static function ( $matches ) use ( $phar_path, $shell_path ) { return $phar_path ? "php {$matches[1]} {$phar_path}" : ( 'WP_CLI_PHP_ARGS=' . escapeshellarg( $matches[1] ) . ' ' . $shell_path ); }, $str @@ -828,6 +922,9 @@ function ( $matches ) use ( $phar_path, $shell_path ) { /** * Replace variables callback. + * + * @param array $matches + * @return string */ private function replace_var( $matches ) { $str = $matches[0]; @@ -845,8 +942,11 @@ private function replace_var( $matches ) { /** * Substitute {WP_VERSION-version-latest} variables. + * + * @param string $str + * @return string */ - private function replace_wp_versions( $str ) { + private function replace_wp_versions( $str ): string { static $wp_versions = null; if ( null === $wp_versions ) { $wp_versions = []; @@ -878,8 +978,13 @@ private function replace_wp_versions( $str ) { /** * Get the file and line number for the current behat scope. + * + * @param ScenarioScope|FeatureScope|OutlineTested $scope + * @param int $line + * + * @param-out int $line */ - private static function get_event_file( $scope, &$line ) { + private static function get_event_file( $scope, &$line ): ?string { if ( method_exists( $scope, 'getScenario' ) ) { $scenario_feature = $scope->getScenario(); } elseif ( method_exists( $scope, 'getFeature' ) ) { @@ -902,7 +1007,7 @@ private static function get_event_file( $scope, &$line ) { /** * Create the RUN_DIR directory, unless already set for this scenario. */ - public function create_run_dir() { + public function create_run_dir(): void { if ( ! isset( $this->variables['RUN_DIR'] ) ) { self::$run_dir = sys_get_temp_dir() . '/' . uniqid( 'wp-cli-test-run-' . self::$temp_dir_infix . '-', true ); $this->variables['RUN_DIR'] = self::$run_dir; @@ -910,7 +1015,10 @@ public function create_run_dir() { } } - public function build_phar( $version = 'same' ) { + /** + * @param string $version + */ + public function build_phar( $version = 'same' ): void { $this->variables['PHAR_PATH'] = $this->variables['RUN_DIR'] . '/' . uniqid( 'wp-cli-build-', true ) . '.phar'; // Test running against a package installed as a WP-CLI dependency @@ -931,7 +1039,10 @@ public function build_phar( $version = 'same' ) { )->run_check(); } - public function download_phar( $version = 'same' ) { + /** + * @param string $version + */ + public function download_phar( $version = 'same' ): void { if ( 'same' === $version ) { $version = WP_CLI_VERSION; } @@ -957,7 +1068,7 @@ public function download_phar( $version = 'same' ) { /** * CACHE_DIR is a cache for downloaded test data such as images. Lives until manually deleted. */ - private function set_cache_dir() { + private function set_cache_dir(): void { $path = sys_get_temp_dir() . '/wp-cli-test-cache'; if ( ! file_exists( $path ) ) { mkdir( $path ); @@ -968,11 +1079,11 @@ private function set_cache_dir() { /** * Run a MySQL command with `$db_settings`. * - * @param string $sql_cmd Command to run. - * @param array $assoc_args Optional. Associative array of options. Default empty. - * @param bool $add_database Optional. Whether to add dbname to the $sql_cmd. Default false. + * @param string $sql_cmd Command to run. + * @param array $assoc_args Optional. Associative array of options. Default empty. + * @param bool $add_database Optional. Whether to add dbname to the $sql_cmd. Default false. */ - private static function run_sql( $sql_cmd, $assoc_args = [], $add_database = false ) { + private static function run_sql( $sql_cmd, $assoc_args = [], $add_database = false ): void { $default_assoc_args = [ 'host' => self::$db_settings['dbhost'], 'user' => self::$db_settings['dbuser'], @@ -988,7 +1099,7 @@ private static function run_sql( $sql_cmd, $assoc_args = [], $add_database = fal } } - public function create_db() { + public function create_db(): void { if ( 'sqlite' === self::$db_type ) { return; } @@ -997,7 +1108,7 @@ public function create_db() { self::run_sql( self::$mysql_binary . ' --no-defaults', [ 'execute' => "CREATE DATABASE IF NOT EXISTS $dbname" ] ); } - public function drop_db() { + public function drop_db(): void { if ( 'sqlite' === self::$db_type ) { return; } @@ -1005,7 +1116,13 @@ public function drop_db() { self::run_sql( self::$mysql_binary . ' --no-defaults', [ 'execute' => "DROP DATABASE IF EXISTS $dbname" ] ); } - public function proc( $command, $assoc_args = [], $path = '' ) { + /** + * @param string $command + * @param array $assoc_args + * @param string $path + * @return Process + */ + public function proc( $command, $assoc_args = [], $path = '' ): Process { if ( ! empty( $assoc_args ) ) { $command .= Utils\assoc_args_to_str( $assoc_args ); } @@ -1043,8 +1160,10 @@ public function proc( $command, $assoc_args = [], $path = '' ) { /** * Start a background process. Will automatically be closed when the tests finish. + * + * @param string $cmd */ - public function background_proc( $cmd ) { + public function background_proc( $cmd ): void { $descriptors = [ 0 => STDIN, 1 => [ 'pipe', 'w' ], @@ -1060,39 +1179,55 @@ public function background_proc( $cmd ) { if ( ! $status['running'] ) { $stderr = is_resource( $pipes[2] ) ? ( ': ' . stream_get_contents( $pipes[2] ) ) : ''; throw new RuntimeException( sprintf( "Failed to start background process '%s'%s.", $cmd, $stderr ) ); - } else { - $this->running_procs[] = $proc; } + + $this->running_procs[] = $proc; } - public function move_files( $src, $dest ) { + /** + * @param string $src + * @param string $dest + */ + public function move_files( $src, $dest ): void { rename( $this->variables['RUN_DIR'] . "/$src", $this->variables['RUN_DIR'] . "/$dest" ); } /** * Remove a directory (recursive). + * + * @param string $dir */ - public static function remove_dir( $dir ) { + public static function remove_dir( $dir ): void { Process::create( Utils\esc_cmd( 'rm -rf %s', $dir ) )->run_check(); } /** * Copy a directory (recursive). Destination directory must exist. + * + * @param string $src_dir + * @param string $dest_dir */ - public static function copy_dir( $src_dir, $dest_dir ) { + public static function copy_dir( $src_dir, $dest_dir ): void { $shell_command = ( 'Darwin' === PHP_OS ) ? Utils\esc_cmd( 'cp -R %s/* %s', $src_dir, $dest_dir ) : Utils\esc_cmd( 'cp -r %s/* %s', $src_dir, $dest_dir ); Process::create( $shell_command )->run_check(); } - public function add_line_to_wp_config( &$wp_config_code, $line ) { + /** + * @param string $wp_config_code + * @param string $line + */ + public function add_line_to_wp_config( &$wp_config_code, $line ): void { $token = "/* That's all, stop editing!"; $wp_config_code = str_replace( $token, "$line\n\n$token", $wp_config_code ); } - public function download_wp( $subdir = '' ) { + /** + * @param string $subdir + */ + public function download_wp( $subdir = '' ): void { $dest_dir = $this->variables['RUN_DIR'] . "/$subdir"; if ( $subdir ) { @@ -1106,10 +1241,10 @@ public function download_wp( $subdir = '' ) { } // Disable emailing. - copy( dirname( dirname( __DIR__ ) ) . '/utils/no-mail.php', $dest_dir . '/wp-content/mu-plugins/no-mail.php' ); + copy( dirname( __DIR__, 2 ) . '/utils/no-mail.php', $dest_dir . '/wp-content/mu-plugins/no-mail.php' ); // Add polyfills. - copy( dirname( dirname( __DIR__ ) ) . '/utils/polyfills.php', $dest_dir . '/wp-content/mu-plugins/polyfills.php' ); + copy( dirname( __DIR__, 2 ) . '/utils/polyfills.php', $dest_dir . '/wp-content/mu-plugins/polyfills.php' ); if ( 'sqlite' === self::$db_type ) { self::copy_dir( self::$sqlite_cache_dir, $dest_dir . '/wp-content/mu-plugins' ); @@ -1117,7 +1252,13 @@ public function download_wp( $subdir = '' ) { } } - public function create_config( $subdir = '', $extra_php = false ) { + /** + * Create a wp-config.php file. + * + * @param string $subdir + * @param string|false $extra_php + */ + public function create_config( $subdir = '', $extra_php = false ): void { $params = self::$db_settings; // Replaces all characters that are not alphanumeric or an underscore into an underscore. @@ -1150,7 +1291,10 @@ public function create_config( $subdir = '', $extra_php = false ) { } } - public function install_wp( $subdir = '' ) { + /** + * @param string $subdir + */ + public function install_wp( $subdir = '' ): void { $wp_version = getenv( 'WP_VERSION' ); $wp_version_suffix = ( false !== $wp_version ) ? "-$wp_version" : ''; self::$install_cache_dir = sys_get_temp_dir() . '/wp-cli-test-core-install-cache' . $wp_version_suffix; @@ -1179,14 +1323,11 @@ public function install_wp( $subdir = '' ) { 'skip-email' => true, ]; - $run_dir = '' !== $subdir ? ( $this->variables['RUN_DIR'] . "/$subdir" ) : $this->variables['RUN_DIR']; - $install_cache_path = ''; + $run_dir = '' !== $subdir ? ( $this->variables['RUN_DIR'] . "/$subdir" ) : $this->variables['RUN_DIR']; - if ( self::$install_cache_dir ) { - $install_cache_path = self::$install_cache_dir . '/install_' . md5( implode( ':', $install_args ) . ':subdir=' . $subdir ); - } + $install_cache_path = self::$install_cache_dir . '/install_' . md5( implode( ':', $install_args ) . ':subdir=' . $subdir ); - if ( $install_cache_path && file_exists( $install_cache_path ) ) { + if ( file_exists( $install_cache_path ) ) { self::copy_dir( $install_cache_path, $run_dir ); // This is the sqlite equivalent of restoring a database dump in MySQL @@ -1198,31 +1339,32 @@ public function install_wp( $subdir = '' ) { } else { $this->proc( 'wp core install', $install_args, $subdir )->run_check(); - if ( $install_cache_path ) { - mkdir( $install_cache_path ); + mkdir( $install_cache_path ); - self::dir_diff_copy( $run_dir, self::$cache_dir, $install_cache_path ); + self::dir_diff_copy( $run_dir, self::$cache_dir, $install_cache_path ); - if ( 'sqlite' !== self::$db_type ) { - $mysqldump_binary = Utils\get_sql_dump_command(); - $mysqldump_binary = Utils\force_env_on_nix_systems( $mysqldump_binary ); - $support_column_statistics = exec( "{$mysqldump_binary} --help | grep 'column-statistics'" ); - $command = "{$mysqldump_binary} --no-defaults --no-tablespaces"; - if ( $support_column_statistics ) { - $command .= ' --skip-column-statistics'; - } - self::run_sql( $command, [ 'result-file' => "{$install_cache_path}.sql" ], true /*add_database*/ ); + if ( 'sqlite' !== self::$db_type ) { + $mysqldump_binary = Utils\get_sql_dump_command(); + $mysqldump_binary = Utils\force_env_on_nix_systems( $mysqldump_binary ); + $support_column_statistics = exec( "{$mysqldump_binary} --help | grep 'column-statistics'" ); + $command = "{$mysqldump_binary} --no-defaults --no-tablespaces"; + if ( $support_column_statistics ) { + $command .= ' --skip-column-statistics'; } + self::run_sql( $command, [ 'result-file' => "{$install_cache_path}.sql" ], true /*add_database*/ ); + } - if ( 'sqlite' === self::$db_type ) { - // This is the sqlite equivalent of creating a database dump in MySQL - copy( "$run_dir/wp-content/database/.ht.sqlite", "{$install_cache_path}.sqlite" ); - } + if ( 'sqlite' === self::$db_type ) { + // This is the sqlite equivalent of creating a database dump in MySQL + copy( "$run_dir/wp-content/database/.ht.sqlite", "{$install_cache_path}.sqlite" ); } } } - public function install_wp_with_composer( $vendor_directory = 'vendor' ) { + /** + * @param string $vendor_directory + */ + public function install_wp_with_composer( $vendor_directory = 'vendor' ): void { $this->create_run_dir(); $this->create_db(); @@ -1266,15 +1408,13 @@ public function install_wp_with_composer( $vendor_directory = 'vendor' ) { $this->proc( 'wp core install', $install_args )->run_check(); } - public function composer_add_wp_cli_local_repository() { + public function composer_add_wp_cli_local_repository(): void { if ( ! self::$composer_local_repository ) { self::$composer_local_repository = sys_get_temp_dir() . '/' . uniqid( 'wp-cli-composer-local-', true ); mkdir( self::$composer_local_repository ); $env = self::get_process_env_variables(); - $src = isset( $env['TRAVIS_BUILD_DIR'] ) - ? $env['TRAVIS_BUILD_DIR'] - : realpath( self::get_vendor_dir() . '/../' ); + $src = $env['TRAVIS_BUILD_DIR'] ?? realpath( self::get_vendor_dir() . '/../' ); self::copy_dir( $src, self::$composer_local_repository . '/' ); self::remove_dir( self::$composer_local_repository . '/.git' ); @@ -1285,13 +1425,16 @@ public function composer_add_wp_cli_local_repository() { $this->variables['COMPOSER_LOCAL_REPOSITORY'] = self::$composer_local_repository; } - public function composer_require_current_wp_cli() { + public function composer_require_current_wp_cli(): void { $this->composer_add_wp_cli_local_repository(); // TODO: Specific alias version should be deduced to keep up-to-date. $this->composer_command( 'require "wp-cli/wp-cli:dev-main as 2.5.x-dev" --optimize-autoloader' ); } - public function start_php_server( $subdir = '' ) { + /** + * @param string $subdir + */ + public function start_php_server( $subdir = '' ): void { $dir = $this->variables['RUN_DIR'] . '/'; if ( $subdir ) { $dir .= trim( $subdir, '/' ) . '/'; @@ -1307,7 +1450,10 @@ public function start_php_server( $subdir = '' ) { $this->background_proc( $cmd ); } - private function composer_command( $cmd ) { + /** + * @param string $cmd + */ + private function composer_command( $cmd ): void { if ( ! isset( $this->variables['COMPOSER_PATH'] ) ) { $this->variables['COMPOSER_PATH'] = exec( 'which composer' ); } @@ -1316,8 +1462,10 @@ private function composer_command( $cmd ) { /** * Initialize run time logging. + * + * @param BeforeSuiteScope $scope */ - private static function log_run_times_before_suite( BeforeSuiteScope $scope ) { + private static function log_run_times_before_suite( BeforeSuiteScope $scope ): void { self::$suite_start_time = microtime( true ); Process::$log_run_times = true; @@ -1345,8 +1493,10 @@ private static function log_run_times_before_suite( BeforeSuiteScope $scope ) { /** * Record the start time of the scenario into the `$scenario_run_times` array. + * + * @param ScenarioScope|FeatureScope|OutlineTested $scope */ - private static function log_run_times_before_scenario( $scope ) { + private static function log_run_times_before_scenario( $scope ): void { $scenario_key = self::get_scenario_key( $scope ); if ( $scenario_key ) { self::$scenario_run_times[ $scenario_key ] = -microtime( true ); @@ -1355,8 +1505,10 @@ private static function log_run_times_before_scenario( $scope ) { /** * Save the run time of the scenario into the `$scenario_run_times` array. Only the top `self::$num_top_scenarios` are kept. + * + * @param ScenarioScope|FeatureScope|OutlineTested $scope */ - private static function log_run_times_after_scenario( $scope ) { + private static function log_run_times_after_scenario( $scope ): void { $scenario_key = self::get_scenario_key( $scope ); if ( $scenario_key ) { self::$scenario_run_times[ $scenario_key ] += microtime( true ); @@ -1376,7 +1528,7 @@ private static function log_run_times_after_scenario( $scope ) { * @param string $src_dir The directory to be compared to `$upd_dir`. * @param string $cop_dir Where to copy any files/directories in `$upd_dir` but not in `$src_dir` to. */ - private static function dir_diff_copy( $upd_dir, $src_dir, $cop_dir ) { + private static function dir_diff_copy( $upd_dir, $src_dir, $cop_dir ): void { $files = scandir( $upd_dir ); if ( false === $files ) { $error = error_get_last(); @@ -1406,12 +1558,14 @@ private static function dir_diff_copy( $upd_dir, $src_dir, $cop_dir ) { /** * Get the scenario key used for `$scenario_run_times` array. * Format " :", eg "core-command core-update.feature:221". + * + * @param ScenarioScope|FeatureScope|OutlineTested $scope */ - private static function get_scenario_key( $scope ) { + private static function get_scenario_key( $scope ): string { $scenario_key = ''; $file = self::get_event_file( $scope, $line ); if ( isset( $file ) ) { - $scenario_grandparent = Utils\basename( dirname( dirname( $file ) ) ); + $scenario_grandparent = Utils\basename( dirname( $file, 2 ) ); $scenario_key = $scenario_grandparent . ' ' . Utils\basename( $file ) . ':' . $line; } return $scenario_key; @@ -1420,7 +1574,7 @@ private static function get_scenario_key( $scope ) { /** * Print out stats on the run times of processes and scenarios. */ - private static function log_run_times_after_suite( AfterSuiteScope $scope ) { + private static function log_run_times_after_suite( AfterSuiteScope $scope ): void { $suite = ''; if ( self::$scenario_run_times ) { @@ -1429,10 +1583,10 @@ private static function log_run_times_after_suite( AfterSuiteScope $scope ) { $suite = substr( $keys[0], 0, strpos( $keys[0], ' ' ) ); } - $run_from = Utils\basename( dirname( dirname( __DIR__ ) ) ); + $run_from = Utils\basename( dirname( __DIR__, 2 ) ); // Format same as Behat, if have minutes. - $fmt = function ( $time ) { + $fmt = static function ( $time ) { $mins = floor( $time / 60 ); return round( $time, 3 ) . ( $mins ? ( ' (' . $mins . 'm' . round( $time - ( $mins * 60 ), 3 ) . 's)' ) : '' ); }; @@ -1443,7 +1597,7 @@ private static function log_run_times_after_suite( AfterSuiteScope $scope ) { // Process and proc method run times. $run_times = array_merge( Process::$run_times, self::$proc_method_run_times ); - $reduce_callback = function ( $carry, $item ) { + $reduce_callback = static function ( $carry, $item ) { return [ $carry[0] + $item[0], $carry[1] + $item[1] ]; }; @@ -1465,14 +1619,14 @@ private static function log_run_times_after_suite( AfterSuiteScope $scope ) { $run_from ); - $sort_callback = function ( $a, $b ) { + $sort_callback = static function ( $a, $b ) { return $a[0] === $b[0] ? 0 : ( $a[0] < $b[0] ? 1 : -1 ); // Reverse sort. }; uasort( $run_times, $sort_callback ); $tops = array_slice( $run_times, 0, self::$num_top_processes, true ); - $runtime_callback = function ( $k, $v, $i ) { + $runtime_callback = static function ( $k, $v, $i ) { return sprintf( ' %3d. %7.3f %3d %s', $i + 1, round( $v[0], 3 ), $v[1], $k ); }; @@ -1492,7 +1646,7 @@ private static function log_run_times_after_suite( AfterSuiteScope $scope ) { $tops = array_slice( self::$scenario_run_times, 0, self::$num_top_scenarios, true ); - $scenario_runtime_callback = function ( $k, $v, $i ) { + $scenario_runtime_callback = static function ( $k, $v, $i ) { return sprintf( ' %3d. %7.3f %s', $i + 1, round( $v, 3 ), substr( $k, strpos( $k, ' ' ) + 1 ) ); }; @@ -1519,8 +1673,11 @@ private static function log_run_times_after_suite( AfterSuiteScope $scope ) { /** * Log the run time of a proc method (one that doesn't use Process but does (use a function that does) a `proc_open()`). + * + * @param string $key + * @param int|float $start_time */ - private static function log_proc_method_run_time( $key, $start_time ) { + private static function log_proc_method_run_time( $key, $start_time ): void { $run_time = microtime( true ) - $start_time; if ( ! isset( self::$proc_method_run_times[ $key ] ) ) { self::$proc_method_run_times[ $key ] = [ 0, 0 ]; @@ -1530,8 +1687,11 @@ private static function log_proc_method_run_time( $key, $start_time ) { } } -// phpcs:ignore Universal.Files.SeparateFunctionsFromOO.Mixed -function wp_cli_behat_env_debug( $message ) { + +/** + * @param string $message + */ +function wp_cli_behat_env_debug( $message ): void { // phpcs:ignore Universal.Files.SeparateFunctionsFromOO.Mixed if ( ! getenv( 'WP_CLI_TEST_DEBUG_BEHAT_ENV' ) ) { return; } @@ -1542,7 +1702,7 @@ function wp_cli_behat_env_debug( $message ) { /** * Load required support files as needed before heading into the Behat context. */ -function wpcli_bootstrap_behat_feature_context() { +function wpcli_bootstrap_behat_feature_context(): void { $vendor_folder = FeatureContext::get_vendor_dir(); wp_cli_behat_env_debug( "Vendor folder location: {$vendor_folder}" ); @@ -1557,11 +1717,6 @@ function wpcli_bootstrap_behat_feature_context() { $framework_folder = FeatureContext::get_framework_dir(); wp_cli_behat_env_debug( "Framework folder location: {$framework_folder}" ); - // Didn't manage to detect a valid framework folder. - if ( empty( $vendor_folder ) ) { - return; - } - // Load helper functionality that is needed for the tests. require_once "{$framework_folder}/php/utils.php"; require_once "{$framework_folder}/php/WP_CLI/Process.php"; diff --git a/src/Context/GivenStepDefinitions.php b/src/Context/GivenStepDefinitions.php index d4e8b5fb8..3df2d5324 100644 --- a/src/Context/GivenStepDefinitions.php +++ b/src/Context/GivenStepDefinitions.php @@ -23,7 +23,7 @@ trait GivenStepDefinitions { * * @Given an empty directory */ - public function given_an_empty_directory() { + public function given_an_empty_directory(): void { $this->create_run_dir(); } @@ -40,8 +40,11 @@ public function given_an_empty_directory() { * @access public * * @Given /^an? (empty|non-existent) ([^\s]+) directory$/ + * + * @param string $empty_or_nonexistent + * @param string $dir */ - public function given_a_specific_directory( $empty_or_nonexistent, $dir ) { + public function given_a_specific_directory( $empty_or_nonexistent, $dir ): void { $dir = $this->replace_variables( $dir ); if ( ! Utils\is_path_absolute( $dir ) ) { $dir = $this->variables['RUN_DIR'] . "/$dir"; @@ -82,7 +85,7 @@ public function given_a_specific_directory( $empty_or_nonexistent, $dir ) { * * @Given an empty cache */ - public function given_an_empty_cache() { + public function given_an_empty_cache(): void { $this->variables['SUITE_CACHE_DIR'] = FeatureContext::create_cache_dir(); } @@ -105,8 +108,12 @@ public function given_an_empty_cache() { * @access public * * @Given /^an? ([^\s]+) (file|cache file):$/ + * + * @param string $path + * @param string $type + * @param PyStringNode $content */ - public function given_a_specific_file( $path, $type, PyStringNode $content ) { + public function given_a_specific_file( $path, $type, PyStringNode $content ): void { $path = $this->replace_variables( (string) $path ); $content = $this->replace_variables( (string) $content ) . "\n"; $full_path = 'cache file' === $type @@ -131,8 +138,12 @@ public function given_a_specific_file( $path, $type, PyStringNode $content ) { * @access public * * @Given /^"([^"]+)" replaced with "([^"]+)" in the ([^\s]+) file$/ + * + * @param string $search + * @param string $replace + * @param string $path */ - public function given_string_replaced_with_string_in_a_specific_file( $search, $replace, $path ) { + public function given_string_replaced_with_string_in_a_specific_file( $search, $replace, $path ): void { $full_path = $this->variables['RUN_DIR'] . "/$path"; $contents = file_get_contents( $full_path ); $contents = str_replace( $search, $replace, $contents ); @@ -156,8 +167,11 @@ public function given_string_replaced_with_string_in_a_specific_file( $search, $ * @access public * * @Given /^that HTTP requests to (.*?) will respond with:$/ + * + * @param string $url_or_pattern + * @param PyStringNode $content */ - public function given_a_request_to_a_url_respond_with_file( $url_or_pattern, PyStringNode $content ) { + public function given_a_request_to_a_url_respond_with_file( $url_or_pattern, PyStringNode $content ): void { if ( ! isset( $this->variables['RUN_DIR'] ) ) { $this->create_run_dir(); } @@ -325,7 +339,7 @@ static function( \$pre, \$parsed_args, \$url ) { * * @Given WP files */ - public function given_wp_files() { + public function given_wp_files(): void { $this->download_wp(); } @@ -343,7 +357,7 @@ public function given_wp_files() { * * @Given wp-config.php */ - public function given_wp_config_php() { + public function given_wp_config_php(): void { $this->create_config(); } @@ -362,7 +376,7 @@ public function given_wp_config_php() { * * @Given a database */ - public function given_a_database() { + public function given_a_database(): void { $this->create_db(); } @@ -383,7 +397,7 @@ public function given_a_database() { * * @Given a WP install(ation) */ - public function given_a_wp_installation() { + public function given_a_wp_installation(): void { $this->install_wp(); } @@ -403,8 +417,10 @@ public function given_a_wp_installation() { * @access public * * @Given a WP install(ation) in :subdir + * + * @param string $subdir */ - public function given_a_wp_installation_in_a_specific_folder( $subdir ) { + public function given_a_wp_installation_in_a_specific_folder( $subdir ): void { $this->install_wp( $subdir ); } @@ -425,7 +441,7 @@ public function given_a_wp_installation_in_a_specific_folder( $subdir ) { * * @Given a WP install(ation) with Composer */ - public function given_a_wp_installation_with_composer() { + public function given_a_wp_installation_with_composer(): void { $this->install_wp_with_composer(); } @@ -445,8 +461,10 @@ public function given_a_wp_installation_with_composer() { * @access public * * @Given a WP install(ation) with Composer and a custom vendor directory :vendor_directory + * + * @param string $vendor_directory */ - public function given_a_wp_installation_with_composer_and_a_custom_vendor_folder( $vendor_directory ) { + public function given_a_wp_installation_with_composer_and_a_custom_vendor_folder( $vendor_directory ): void { $this->install_wp_with_composer( $vendor_directory ); } @@ -468,8 +486,10 @@ public function given_a_wp_installation_with_composer_and_a_custom_vendor_folder * @access public * * @Given /^a WP multisite (subdirectory|subdomain)?\s?(install|installation)$/ + * + * @param string $type Multisite installation type. */ - public function given_a_wp_multisite_installation( $type = 'subdirectory' ) { + public function given_a_wp_multisite_installation( $type = 'subdirectory' ): void { $this->install_wp(); $subdomains = ! empty( $type ) && 'subdomain' === $type ? 1 : 0; $this->proc( @@ -497,8 +517,10 @@ public function given_a_wp_multisite_installation( $type = 'subdirectory' ) { * @access public * * @Given these installed and active plugins: + * + * @param string $stream */ - public function given_these_installed_and_active_plugins( $stream ) { + public function given_these_installed_and_active_plugins( $stream ): void { $plugins = implode( ' ', array_map( 'trim', explode( PHP_EOL, (string) $stream ) ) ); $plugins = $this->replace_variables( $plugins ); @@ -520,7 +542,7 @@ public function given_these_installed_and_active_plugins( $stream ) { * * @Given a custom wp-content directory */ - public function given_a_custom_wp_directory() { + public function given_a_custom_wp_directory(): void { $wp_config_path = $this->variables['RUN_DIR'] . '/wp-config.php'; $wp_config_code = file_get_contents( $wp_config_path ); @@ -574,7 +596,7 @@ public function given_a_custom_wp_directory() { * * @Given download: */ - public function given_a_download( TableNode $table ) { + public function given_a_download( TableNode $table ): void { foreach ( $table->getHash() as $row ) { $path = $this->replace_variables( $row['path'] ); if ( file_exists( $path ) ) { @@ -602,8 +624,12 @@ public function given_a_download( TableNode $table ) { * @access public * * @Given /^save (STDOUT|STDERR) ([\'].+[^\'])?\s?as \{(\w+)\}$/ + * + * @param string $stream + * @param string $output_filter + * @param string $key */ - public function given_saved_stdout_stderr( $stream, $output_filter, $key ) { + public function given_saved_stdout_stderr( $stream, $output_filter, $key ): void { $stream = strtolower( $stream ); if ( $output_filter ) { @@ -631,8 +657,10 @@ public function given_saved_stdout_stderr( $stream, $output_filter, $key ) { * @access public * * @Given /^a new Phar with (?:the same version|version "([^"]+)")$/ + * + * @param string $version */ - public function given_a_new_phar_with_a_specific_version( $version = 'same' ) { + public function given_a_new_phar_with_a_specific_version( $version = 'same' ): void { $this->build_phar( $version ); } @@ -652,8 +680,10 @@ public function given_a_new_phar_with_a_specific_version( $version = 'same' ) { * @access public * * @Given /^a downloaded Phar with (?:the same version|version "([^"]+)")$/ + * + * @param string $version */ - public function given_a_downloaded_phar_with_a_specific_version( $version = 'same' ) { + public function given_a_downloaded_phar_with_a_specific_version( $version = 'same' ): void { $this->download_phar( $version ); } @@ -669,8 +699,12 @@ public function given_a_downloaded_phar_with_a_specific_version( $version = 'sam * @access public * * @Given /^save the (.+) file ([\'].+[^\'])?as \{(\w+)\}$/ + * + * @param string $filepath + * @param string $output_filter + * @param string $key */ - public function given_saved_a_specific_file( $filepath, $output_filter, $key ) { + public function given_saved_a_specific_file( $filepath, $output_filter, $key ): void { $full_file = file_get_contents( $this->replace_variables( $filepath ) ); if ( $output_filter ) { @@ -699,7 +733,7 @@ public function given_saved_a_specific_file( $filepath, $output_filter, $key ) { * * @Given a misconfigured WP_CONTENT_DIR constant directory */ - public function given_a_misconfigured_wp_content_dir_constant_directory() { + public function given_a_misconfigured_wp_content_dir_constant_directory(): void { $wp_config_path = $this->variables['RUN_DIR'] . '/wp-config.php'; $wp_config_code = file_get_contents( $wp_config_path ); @@ -725,7 +759,7 @@ public function given_a_misconfigured_wp_content_dir_constant_directory() { * * @Given a dependency on current wp-cli */ - public function given_a_dependency_on_wp_cli() { + public function given_a_dependency_on_wp_cli(): void { $this->composer_require_current_wp_cli(); } @@ -742,7 +776,7 @@ public function given_a_dependency_on_wp_cli() { * * @Given a PHP built-in web server */ - public function given_a_php_built_in_web_server() { + public function given_a_php_built_in_web_server(): void { $this->start_php_server(); } @@ -758,8 +792,10 @@ public function given_a_php_built_in_web_server() { * @access public * * @Given a PHP built-in web server to serve :subdir + * + * @param string $subdir */ - public function given_a_php_built_in_web_server_to_serve_a_specific_folder( $subdir ) { + public function given_a_php_built_in_web_server_to_serve_a_specific_folder( $subdir ): void { $this->start_php_server( $subdir ); } } diff --git a/src/Context/Support.php b/src/Context/Support.php index 67a16ab40..cd3f34ca6 100644 --- a/src/Context/Support.php +++ b/src/Context/Support.php @@ -11,46 +11,89 @@ trait Support { - - protected function assert_regex( $regex, $actual ) { + /** + * @param string $regex + * @param string $actual + * @throws Exception + */ + protected function assert_regex( $regex, $actual ): void { if ( ! preg_match( $regex, $actual ) ) { throw new Exception( 'Actual value: ' . var_export( $actual, true ) ); } } - protected function assert_not_regex( $regex, $actual ) { + /** + * @param string $regex + * @param string $actual + * @throws Exception + */ + protected function assert_not_regex( $regex, $actual ): void { if ( preg_match( $regex, $actual ) ) { throw new Exception( 'Actual value: ' . var_export( $actual, true ) ); } } - protected function assert_equals( $expected, $actual ) { + /** + * Loose comparison. + * + * @param mixed $expected + * @param mixed $actual + * @throws Exception + */ + protected function assert_equals( $expected, $actual ): void { // phpcs:ignore Universal.Operators.StrictComparisons.LooseNotEqual -- Deliberate loose comparison. if ( $expected != $actual ) { throw new Exception( 'Actual value: ' . var_export( $actual, true ) ); } } - protected function assert_not_equals( $expected, $actual ) { + /** + * Loose comparison. + * + * @param mixed $expected + * @param mixed $actual + * @throws Exception + */ + protected function assert_not_equals( $expected, $actual ): void { // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual -- Deliberate loose comparison. if ( $expected == $actual ) { throw new Exception( 'Actual value: ' . var_export( $actual, true ) ); } } - protected function assert_numeric( $actual ) { + /** + * @param mixed $actual + * @throws Exception + * + * @phpstan-assert numeric-string|number $actual + */ + protected function assert_numeric( $actual ): void { if ( ! is_numeric( $actual ) ) { throw new Exception( 'Actual value: ' . var_export( $actual, true ) ); } } - protected function assert_not_numeric( $actual ) { + /** + * @param mixed $actual + * @throws Exception + * + * @phpstan-assert !(numeric-string|number) $actual + */ + protected function assert_not_numeric( $actual ): void { if ( is_numeric( $actual ) ) { throw new Exception( 'Actual value: ' . var_export( $actual, true ) ); } } - protected function check_string( $output, $expected, $action, $message = false, $strictly = false ) { + /** + * @param string $output + * @param string $expected + * @param string $action + * @param string|false $message + * @param bool $strictly + * @throws Exception + */ + protected function check_string( $output, $expected, $action, $message = false, $strictly = false ): void { // Strip ANSI color codes before comparing strings. if ( ! $strictly ) { $output = preg_replace( '/\e[[][A-Za-z0-9];?[0-9]*m?/', '', $output ); @@ -70,7 +113,7 @@ protected function check_string( $output, $expected, $action, $message = false, break; default: - throw new PendingException(); + throw new \Behat\Behat\Tester\Exception\PendingException(); } if ( ! $r ) { @@ -81,7 +124,13 @@ protected function check_string( $output, $expected, $action, $message = false, } } - protected function compare_tables( $expected_rows, $actual_rows, $output ) { + /** + * @param array $expected_rows + * @param array $actual_rows + * @param string $output + * @throws Exception + */ + protected function compare_tables( $expected_rows, $actual_rows, $output ): void { // The first row is the header and must be present. if ( $expected_rows[0] !== $actual_rows[0] ) { throw new Exception( $output ); @@ -96,6 +145,11 @@ protected function compare_tables( $expected_rows, $actual_rows, $output ) { } } + /** + * @param mixed $expected + * @param mixed $actual + * @return bool + */ protected function compare_contents( $expected, $actual ) { if ( gettype( $expected ) !== gettype( $actual ) ) { return false; @@ -167,8 +221,8 @@ protected function check_that_json_string_contains_json_string( $actual_json, $e * Both strings are expected to have headers for their CSVs. * $actualCSV must match all data rows in $expectedCSV * - * @param string $actual_csv A CSV string - * @param array $expected_csv A nested array of values + * @param string $actual_csv A CSV string + * @param array> $expected_csv A nested array of values * @return bool Whether $actual_csv contains $expected_csv */ protected function check_that_csv_string_contains_values( $actual_csv, $expected_csv ) { @@ -183,6 +237,10 @@ static function ( $str ) { return false; } + /** + * @var array> $actual_csv + */ + // Each sample must have headers. $actual_headers = array_values( array_shift( $actual_csv ) ); $expected_headers = array_values( array_shift( $expected_csv ) ); diff --git a/src/Context/ThenStepDefinitions.php b/src/Context/ThenStepDefinitions.php index 6a2cca92b..9ab0477f9 100644 --- a/src/Context/ThenStepDefinitions.php +++ b/src/Context/ThenStepDefinitions.php @@ -25,8 +25,11 @@ trait ThenStepDefinitions { * @access public * * @Then /^the return code should( not)? be (\d+)$/ + * + * @param bool $not + * @param numeric-string $return_code */ - public function then_the_return_code_should_be( $not, $return_code ) { + public function then_the_return_code_should_be( $not, $return_code ): void { if ( ( ! $not && (int) $return_code !== $this->result->return_code ) || ( $not && (int) $return_code === $this->result->return_code ) @@ -57,8 +60,13 @@ public function then_the_return_code_should_be( $not, $return_code ) { * @access public * * @Then /^(STDOUT|STDERR) should( strictly)? (be|contain|not contain):$/ + * + * @param string $stream + * @param bool $strictly + * @param string $action + * @param PyStringNode $expected */ - public function then_stdout_stderr_should_contain( $stream, $strictly, $action, PyStringNode $expected ) { + public function then_stdout_stderr_should_contain( $stream, $strictly, $action, PyStringNode $expected ): void { $stream = strtolower( $stream ); @@ -80,8 +88,10 @@ public function then_stdout_stderr_should_contain( $stream, $strictly, $action, * @access public * * @Then /^(STDOUT|STDERR) should be a number$/ + * + * @param string $stream */ - public function then_stdout_stderr_should_be_a_number( $stream ) { + public function then_stdout_stderr_should_be_a_number( $stream ): void { $stream = strtolower( $stream ); @@ -101,8 +111,10 @@ public function then_stdout_stderr_should_be_a_number( $stream ) { * @access public * * @Then /^(STDOUT|STDERR) should not be a number$/ + * + * @param string $stream */ - public function then_stdout_stderr_should_not_be_a_number( $stream ) { + public function then_stdout_stderr_should_not_be_a_number( $stream ): void { $stream = strtolower( $stream ); @@ -126,7 +138,7 @@ public function then_stdout_stderr_should_not_be_a_number( $stream ) { * * @Then /^STDOUT should be a table containing rows:$/ */ - public function then_stdout_should_be_a_table_containing_rows( TableNode $expected ) { + public function then_stdout_should_be_a_table_containing_rows( TableNode $expected ): void { $output = $this->result->stdout; $actual_rows = explode( "\n", rtrim( $output, "\n" ) ); @@ -162,7 +174,7 @@ public function then_stdout_should_be_a_table_containing_rows( TableNode $expect * * @Then /^STDOUT should end with a table containing rows:$/ */ - public function then_stdout_should_end_with_a_table_containing_rows( TableNode $expected ) { + public function then_stdout_should_end_with_a_table_containing_rows( TableNode $expected ): void { $output = $this->result->stdout; $actual_rows = explode( "\n", rtrim( $output, "\n" ) ); @@ -198,7 +210,7 @@ public function then_stdout_should_end_with_a_table_containing_rows( TableNode $ * * @Then /^STDOUT should be JSON containing:$/ */ - public function then_stdout_should_be_json_containing( PyStringNode $expected ) { + public function then_stdout_should_be_json_containing( PyStringNode $expected ): void { $output = $this->result->stdout; $expected = $this->replace_variables( (string) $expected ); @@ -225,7 +237,7 @@ public function then_stdout_should_be_json_containing( PyStringNode $expected ) * * @Then /^STDOUT should be a JSON array containing:$/ */ - public function then_stdout_should_be_a_json_array_containing( PyStringNode $expected ) { + public function then_stdout_should_be_a_json_array_containing( PyStringNode $expected ): void { $output = $this->result->stdout; $expected = $this->replace_variables( (string) $expected ); @@ -253,7 +265,7 @@ public function then_stdout_should_be_a_json_array_containing( PyStringNode $exp * * @Then /^STDOUT should be CSV containing:$/ */ - public function then_stdout_should_be_csv_containing( TableNode $expected ) { + public function then_stdout_should_be_csv_containing( TableNode $expected ): void { $output = $this->result->stdout; $expected_rows = $expected->getRows(); @@ -286,7 +298,7 @@ public function then_stdout_should_be_csv_containing( TableNode $expected ) { * * @Then /^STDOUT should be YAML containing:$/ */ - public function then_stdout_should_be_yaml_containing( PyStringNode $expected ) { + public function then_stdout_should_be_yaml_containing( PyStringNode $expected ): void { $output = $this->result->stdout; $expected = $this->replace_variables( (string) $expected ); @@ -308,8 +320,10 @@ public function then_stdout_should_be_yaml_containing( PyStringNode $expected ) * @access public * * @Then /^(STDOUT|STDERR) should be empty$/ + * + * @param string $stream */ - public function then_stdout_stderr_should_be_empty( $stream ) { + public function then_stdout_stderr_should_be_empty( $stream ): void { $stream = strtolower( $stream ); @@ -330,8 +344,10 @@ public function then_stdout_stderr_should_be_empty( $stream ) { * @access public * * @Then /^(STDOUT|STDERR) should not be empty$/ + * + * @param string $stream */ - public function then_stdout_stderr_should_not_be_empty( $stream ) { + public function then_stdout_stderr_should_not_be_empty( $stream ): void { $stream = strtolower( $stream ); @@ -353,8 +369,12 @@ public function then_stdout_stderr_should_not_be_empty( $stream ) { * @access public * * @Then /^(STDOUT|STDERR) should be a version string (<|<=|>|>=|==|=|!=|<>) ([+\w.{}-]+)$/ + * + * @param string $stream + * @param string $operator + * @param string $goal_ver */ - public function then_stdout_stderr_should_be_a_specific_version_string( $stream, $operator, $goal_ver ) { + public function then_stdout_stderr_should_be_a_specific_version_string( $stream, $operator, $goal_ver ): void { $goal_ver = $this->replace_variables( $goal_ver ); $stream = strtolower( $stream ); if ( false === version_compare( trim( $this->result->$stream, "\n" ), $goal_ver, $operator ) ) { @@ -384,8 +404,14 @@ public function then_stdout_stderr_should_be_a_specific_version_string( $stream, * @access public * * @Then /^the (.+) (file|directory) should( strictly)? (exist|not exist|be:|contain:|not contain:)$/ + * + * @param string $path File/directory path. + * @param string $type Type, either 'file' or 'directory'. + * @param string $strictly Whether it's a strict check. + * @param string $action Expected status. + * @param string $expected Expected content. */ - public function then_a_specific_file_folder_should_exist( $path, $type, $strictly, $action, $expected = null ) { + public function then_a_specific_file_folder_should_exist( $path, $type, $strictly, $action, $expected = null ): void { $path = $this->replace_variables( $path ); // If it's a relative path, make it relative to the current test dir. @@ -423,6 +449,7 @@ public function then_a_specific_file_folder_should_exist( $path, $type, $strictl } $action = substr( $action, 0, -1 ); $expected = $this->replace_variables( (string) $expected ); + $contents = ''; if ( 'file' === $type ) { $contents = file_get_contents( $path ); } elseif ( 'directory' === $type ) { @@ -448,8 +475,12 @@ public function then_a_specific_file_folder_should_exist( $path, $type, $strictl * @access public * * @Then /^the contents of the (.+) file should( not)? match (((\/.+\/)|(#.+#))([a-z]+)?)$/ + * + * @param string $path + * @param bool $not + * @param string $expected */ - public function then_the_contents_of_a_specific_file_should_match( $path, $not, $expected ) { + public function then_the_contents_of_a_specific_file_should_match( $path, $not, $expected ): void { $path = $this->replace_variables( $path ); $expected = $this->replace_variables( $expected ); @@ -477,8 +508,12 @@ public function then_the_contents_of_a_specific_file_should_match( $path, $not, * @access public * * @Then /^(STDOUT|STDERR) should( not)? match (((\/.+\/)|(#.+#))([a-z]+)?)$/ + * + * @param string $stream + * @param bool $not + * @param string $expected */ - public function then_stdout_stderr_should_match_a_string( $stream, $not, $expected ) { + public function then_stdout_stderr_should_match_a_string( $stream, $not, $expected ): void { $expected = $this->replace_variables( $expected ); $stream = strtolower( $stream ); @@ -501,8 +536,10 @@ public function then_stdout_stderr_should_match_a_string( $stream, $not, $expect * @access public * * @Then /^an email should (be sent|not be sent)$/ + * + * @param string $expected Expected status, either 'be sent' or 'not be sent'. */ - public function then_an_email_should_be_sent( $expected ) { + public function then_an_email_should_be_sent( $expected ): void { if ( 'be sent' === $expected ) { $this->assert_not_equals( 0, $this->email_sends ); } elseif ( 'not be sent' === $expected ) { @@ -525,8 +562,10 @@ public function then_an_email_should_be_sent( $expected ) { * @access public * * @Then the HTTP status code should be :code + * + * @param int $return_code Expected HTTP status code. */ - public function then_the_http_status_code_should_be( $return_code ) { + public function then_the_http_status_code_should_be( $return_code ): void { $response = Requests::request( 'http://localhost:8080' ); $this->assert_equals( $return_code, $response->status_code ); } diff --git a/src/Context/WhenStepDefinitions.php b/src/Context/WhenStepDefinitions.php index eaa14a141..2de8b3e2e 100644 --- a/src/Context/WhenStepDefinitions.php +++ b/src/Context/WhenStepDefinitions.php @@ -7,6 +7,11 @@ trait WhenStepDefinitions { + /** + * @param Process $proc Process instance. + * @param string $mode Mode, either 'run' or 'try'. + * @return mixed + */ public function wpcli_tests_invoke_proc( $proc, $mode ) { $map = array( 'run' => 'run_check_stderr', @@ -17,7 +22,13 @@ public function wpcli_tests_invoke_proc( $proc, $mode ) { return $proc->$method(); } - public function wpcli_tests_capture_email_sends( $stdout ) { + /** + * Capture the number of sent emails by parsing STDOUT. + * + * @param string $stdout + * @return array{string, int} + */ + public function wpcli_tests_capture_email_sends( $stdout ): array { $stdout = preg_replace( '#WP-CLI test suite: Sent email to.+\n?#', '', $stdout, -1, $email_sends ); return array( $stdout, $email_sends ); @@ -36,8 +47,10 @@ public function wpcli_tests_capture_email_sends( $stdout ) { * @access public * * @When /^I launch in the background `([^`]+)`$/ + * + * @param string $cmd Command to run. */ - public function when_i_launch_in_the_background( $cmd ) { + public function when_i_launch_in_the_background( $cmd ): void { $this->background_proc( $cmd ); } @@ -68,8 +81,11 @@ public function when_i_launch_in_the_background( $cmd ) { * @access public * * @When /^I (run|try) `([^`]+)`$/ + * + * @param string $mode Mode, either 'run' or 'try'. + * @param string $cmd Command to execute. */ - public function when_i_run( $mode, $cmd ) { + public function when_i_run( $mode, $cmd ): void { $cmd = $this->replace_variables( $cmd ); $this->result = $this->wpcli_tests_invoke_proc( $this->proc( $cmd ), $mode ); list( $this->result->stdout, $this->email_sends ) = $this->wpcli_tests_capture_email_sends( $this->result->stdout ); @@ -92,8 +108,12 @@ public function when_i_run( $mode, $cmd ) { * @access public * * @When /^I (run|try) `([^`]+)` from '([^\s]+)'$/ + * + * @param string $mode Mode, either 'run' or 'try'. + * @param string $cmd Command to execute. + * @param string $subdir Directory. */ - public function when_i_run_from_a_subfolder( $mode, $cmd, $subdir ) { + public function when_i_run_from_a_subfolder( $mode, $cmd, $subdir ): void { $cmd = $this->replace_variables( $cmd ); $this->result = $this->wpcli_tests_invoke_proc( $this->proc( $cmd, array(), $subdir ), $mode ); list( $this->result->stdout, $this->email_sends ) = $this->wpcli_tests_capture_email_sends( $this->result->stdout ); @@ -122,8 +142,10 @@ public function when_i_run_from_a_subfolder( $mode, $cmd, $subdir ) { * @access public * * @When /^I (run|try) the previous command again$/ + * + * @param string $mode Mode, either 'run' or 'try' */ - public function when_i_run_the_previous_command_again( $mode ) { + public function when_i_run_the_previous_command_again( $mode ): void { if ( ! isset( $this->result ) ) { throw new Exception( 'No previous command.' ); } diff --git a/tests/bootstrap.php b/tests/bootstrap.php index c66cd935e..80babbe09 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -31,7 +31,11 @@ require_once WP_CLI_ROOT . '/php/utils.php'; require_once __DIR__ . '/includes/TestCase.php'; -function wpcli_tests_include_config( array $config_filenames = [] ) { +/** + * @param string[] $config_filenames List of config file names to look for. + * @return void + */ +function wpcli_tests_include_config( array $config_filenames = [] ): void { $config_filename = false; foreach ( $config_filenames as $filename ) { if ( file_exists( PACKAGE_ROOT . '/' . $filename ) ) { diff --git a/tests/phpstan/scan-files.php b/tests/phpstan/scan-files.php new file mode 100644 index 000000000..ced169fe4 --- /dev/null +++ b/tests/phpstan/scan-files.php @@ -0,0 +1,6 @@ +temp_dir = Utils\get_temp_dir() . uniqid( 'wp-cli-test-behat-tags-', true ); @@ -15,7 +18,7 @@ protected function set_up() { mkdir( $this->temp_dir . '/features' ); } - protected function tear_down() { + protected function tear_down(): void { if ( $this->temp_dir && file_exists( $this->temp_dir ) ) { foreach ( glob( $this->temp_dir . '/features/*' ) as $feature_file ) { @@ -30,8 +33,11 @@ protected function tear_down() { /** * @dataProvider data_behat_tags_wp_version_github_token + * + * @param string $env + * @param string $expected */ - public function test_behat_tags_wp_version_github_token( $env, $expected ) { + public function test_behat_tags_wp_version_github_token( $env, $expected ): void { $env_wp_version = getenv( 'WP_VERSION' ); $env_github_token = getenv( 'GITHUB_TOKEN' ); $db_type = getenv( 'WP_CLI_TEST_DBTYPE' ); @@ -74,7 +80,10 @@ public function test_behat_tags_wp_version_github_token( $env, $expected ) { putenv( false === $env_github_token ? 'GITHUB_TOKEN' : "GITHUB_TOKEN=$env_github_token" ); } - public static function data_behat_tags_wp_version_github_token() { + /** + * @return array + */ + public static function data_behat_tags_wp_version_github_token(): array { return array( array( 'WP_VERSION=4.5', '~@require-wp-4.6&&~@require-wp-4.8&&~@require-wp-4.9&&~@github-api' ), array( 'WP_VERSION=4.6', '~@require-wp-4.8&&~@require-wp-4.9&&~@less-than-wp-4.6&&~@github-api' ), @@ -90,7 +99,7 @@ public static function data_behat_tags_wp_version_github_token() { ); } - public function test_behat_tags_php_version() { + public function test_behat_tags_php_version(): void { $env_github_token = getenv( 'GITHUB_TOKEN' ); putenv( 'GITHUB_TOKEN' ); @@ -146,7 +155,7 @@ public function test_behat_tags_php_version() { putenv( false === $env_github_token ? 'GITHUB_TOKEN' : "GITHUB_TOKEN=$env_github_token" ); } - public function test_behat_tags_extension() { + public function test_behat_tags_extension(): void { $env_github_token = getenv( 'GITHUB_TOKEN' ); $db_type = getenv( 'WP_CLI_TEST_DBTYPE' );