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 );
+ }
+);