diff --git a/phpcs.xml.dist b/phpcs.xml.dist index 62a1a672..79a149e2 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -73,4 +73,13 @@ */utils/polyfills\.php$ + + + + */generate-coverage\.php$ + + + */generate-coverage\.php$ + diff --git a/src/Context/FeatureContext.php b/src/Context/FeatureContext.php index 99ecac78..2eb061db 100644 --- a/src/Context/FeatureContext.php +++ b/src/Context/FeatureContext.php @@ -7,6 +7,8 @@ use Behat\Behat\Hook\Scope\BeforeScenarioScope; 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 RuntimeException; use WP_CLI\Process; use WP_CLI\Utils; @@ -110,6 +112,48 @@ class FeatureContext implements SnippetAcceptingContext { private $mocked_requests = []; + /** + * The current feature. + * + * @var \Behat\Gherkin\Node\FeatureNode|null + */ + private static $feature; + + /** + * The current scenario. + * + * @var \Behat\Gherkin\Node\ScenarioInterface|null + */ + private $scenario; + + /** + * @BeforeFeature + */ + public static function store_feature( BeforeFeatureScope $scope ) { + self::$feature = $scope->getFeature(); + } + + /** + * @BeforeScenario + */ + public function store_scenario( BeforeScenarioScope $scope ) { + $this->scenario = $scope->getScenario(); + } + + /** + * @AfterScenario + */ + public function forget_scenario( AfterScenarioScope $scope ) { + $this->scenario = null; + } + + /** + * @AfterFeature + */ + public static function forget_feature( AfterFeatureScope $scope ) { + self::$feature = null; + } + /** * Get the path to the Composer vendor folder. * @@ -333,9 +377,9 @@ 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 - */ + * Download and extract a single copy of the sqlite-database-integration plugin + * for use in subsequent WordPress copies + */ private static function download_sqlite_plugin( $dir ) { $download_url = 'https://downloads.wordpress.org/plugin/sqlite-database-integration.zip'; $download_location = $dir . '/sqlite-database-integration.zip'; @@ -370,9 +414,9 @@ 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 - */ + * 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 + */ private static function configure_sqlite( $dir ) { $db_copy = $dir . '/wp-content/mu-plugins/sqlite-database-integration/db.copy'; $db_dropin = $dir . '/wp-content/db.php'; @@ -639,6 +683,23 @@ public function __construct() { $this->set_cache_dir(); } + /** + * Enhances a `wp ` string with an additional `--require` for code coverage collection. + * + * Only applies if `WP_CLI_TEST_COVERAGE` is set. + * + * @param string $cmd Command string. + * @return string Possibly enhanced command string. + */ + public function get_command_with_coverage( $cmd ) { + $with_code_coverage = (string) getenv( 'WP_CLI_TEST_COVERAGE' ); + if ( \in_array( $with_code_coverage, [ 'true', '1' ], true ) ) { + return preg_replace( '/(^wp )|( wp )|(\/wp )/', '$1$2$3--require={SRC_DIR}/utils/generate-coverage.php ', $cmd ); + } + + return $cmd; + } + /** * 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. @@ -877,10 +938,25 @@ public function proc( $command, $assoc_args = [], $path = '' ) { } $env = self::get_process_env_variables(); + if ( isset( $this->variables['SUITE_CACHE_DIR'] ) ) { $env['WP_CLI_CACHE_DIR'] = $this->variables['SUITE_CACHE_DIR']; } + if ( isset( $this->variables['PROJECT_DIR'] ) ) { + $env['BEHAT_PROJECT_DIR'] = $this->variables['PROJECT_DIR']; + } + + if ( self::$feature ) { + $env['BEHAT_FEATURE_TITLE'] = self::$feature->getTitle(); + } + + if ( $this->scenario ) { + $env['BEHAT_SCENARIO_TITLE'] = $this->scenario->getTitle(); + } + + $env['WP_CLI_TEST_DBTYPE'] = self::$db_type; + if ( isset( $this->variables['RUN_DIR'] ) ) { $cwd = "{$this->variables['RUN_DIR']}/{$path}"; } else { @@ -1242,8 +1318,8 @@ private static function dir_diff_copy( $upd_dir, $src_dir, $cop_dir ) { } self::copy_dir( $upd_file, $cop_file ); } elseif ( ! copy( $upd_file, $cop_file ) ) { - $error = error_get_last(); - throw new RuntimeException( sprintf( "Failed to copy '%s' to '%s': %s. " . __FILE__ . ':' . __LINE__, $upd_file, $cop_file, $error['message'] ) ); + $error = error_get_last(); + throw new RuntimeException( sprintf( "Failed to copy '%s' to '%s': %s. " . __FILE__ . ':' . __LINE__, $upd_file, $cop_file, $error['message'] ) ); } } elseif ( is_dir( $upd_file ) ) { self::dir_diff_copy( $upd_file, $src_file, $cop_file ); diff --git a/src/Context/WhenStepDefinitions.php b/src/Context/WhenStepDefinitions.php index a727e8c7..3fac6b75 100644 --- a/src/Context/WhenStepDefinitions.php +++ b/src/Context/WhenStepDefinitions.php @@ -35,6 +35,7 @@ public function when_i_launch_in_the_background( $cmd ) { * @When /^I (run|try) `([^`]+)`$/ */ public function when_i_run( $mode, $cmd ) { + $cmd = $this->get_command_with_coverage( $cmd ); $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 ); @@ -44,6 +45,7 @@ public function when_i_run( $mode, $cmd ) { * @When /^I (run|try) `([^`]+)` from '([^\s]+)'$/ */ public function when_i_run_from_a_subfolder( $mode, $cmd, $subdir ) { + $cmd = $this->get_command_with_coverage( $cmd ); $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 ); diff --git a/utils/generate-coverage.php b/utils/generate-coverage.php new file mode 100644 index 00000000..6a19e806 --- /dev/null +++ b/utils/generate-coverage.php @@ -0,0 +1,57 @@ +includeDirectory( "{$root_folder}/includes" ); +$filter->includeFiles( array( "{$root_folder}/plugin.php" ) ); + +$coverage = new CodeCoverage( + ( new Selector() )->forLineCoverage( $filter ), + $filter +); + +/* + * The names of the current feature and scenario are passed on from the Behat test runner + * to this script through environment variables `BEHAT_FEATURE_TITLE` & `BEHAT_SCENARIO_TITLE`. + */ +$feature = getenv( 'BEHAT_FEATURE_TITLE' ); +$scenario = getenv( 'BEHAT_SCENARIO_TITLE' ); +$name = "{$feature} - {$scenario}"; + +$coverage->start( $name ); + +register_shutdown_function( + static function () use ( $coverage, $feature, $scenario, $name ) { + $coverage->stop(); + + $project_dir = (string) getenv( 'BEHAT_PROJECT_DIR' ); + + $feature_suffix = preg_replace( '/[^a-z0-9]+/', '-', strtolower( $feature ) ); + $scenario_suffix = preg_replace( '/[^a-z0-9]+/', '-', strtolower( $scenario ) ); + $db_type = strtolower( getenv( 'WP_CLI_TEST_DBTYPE' ) ); + $destination = "$project_dir/build/logs/$feature_suffix-$scenario_suffix-$db_type.xml"; + + $dir = dirname( $destination ); + if ( ! file_exists( $dir ) ) { + mkdir( $dir, 0777, true /*recursive*/ ); + } + + ( new Clover() )->process( $coverage, $destination, $name ); + } +);