Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Behat code coverage support #234

Merged
merged 20 commits into from
Mar 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions phpcs.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -73,4 +73,13 @@
<rule ref="WordPress.NamingConventions.PrefixAllGlobals">
<exclude-pattern>*/utils/polyfills\.php$</exclude-pattern>
</rule>

<!-- This is a procedural stand-alone file that is never loaded in a WordPress context,
so this file does not have to comply with WP naming conventions. -->
<rule ref="WordPress.NamingConventions.PrefixAllGlobals">
<exclude-pattern>*/generate-coverage\.php$</exclude-pattern>
</rule>
<rule ref="WordPress.WP.GlobalVariablesOverride">
<exclude-pattern>*/generate-coverage\.php$</exclude-pattern>
</rule>
</ruleset>
92 changes: 84 additions & 8 deletions src/Context/FeatureContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -110,6 +112,48 @@

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();

Check warning on line 133 in src/Context/FeatureContext.php

View check run for this annotation

Codecov / codecov/patch

src/Context/FeatureContext.php#L132-L133

Added lines #L132 - L133 were not covered by tests
}

/**
* @BeforeScenario
*/
public function store_scenario( BeforeScenarioScope $scope ) {
$this->scenario = $scope->getScenario();

Check warning on line 140 in src/Context/FeatureContext.php

View check run for this annotation

Codecov / codecov/patch

src/Context/FeatureContext.php#L139-L140

Added lines #L139 - L140 were not covered by tests
}

/**
* @AfterScenario
*/
public function forget_scenario( AfterScenarioScope $scope ) {
$this->scenario = null;

Check warning on line 147 in src/Context/FeatureContext.php

View check run for this annotation

Codecov / codecov/patch

src/Context/FeatureContext.php#L146-L147

Added lines #L146 - L147 were not covered by tests
}

/**
* @AfterFeature
*/
public static function forget_feature( AfterFeatureScope $scope ) {
self::$feature = null;

Check warning on line 154 in src/Context/FeatureContext.php

View check run for this annotation

Codecov / codecov/patch

src/Context/FeatureContext.php#L153-L154

Added lines #L153 - L154 were not covered by tests
}

/**
* Get the path to the Composer vendor folder.
*
Expand Down Expand Up @@ -333,9 +377,9 @@
}

/**
* 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';
Expand Down Expand Up @@ -370,9 +414,9 @@
}

/**
* 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';
Expand Down Expand Up @@ -639,6 +683,23 @@
$this->set_cache_dir();
}

/**
* Enhances a `wp <command>` 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 );

Check warning on line 697 in src/Context/FeatureContext.php

View check run for this annotation

Codecov / codecov/patch

src/Context/FeatureContext.php#L694-L697

Added lines #L694 - L697 were not covered by tests
}

return $cmd;

Check warning on line 700 in src/Context/FeatureContext.php

View check run for this annotation

Codecov / codecov/patch

src/Context/FeatureContext.php#L700

Added line #L700 was not covered by tests
}

/**
* 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.
Expand Down Expand Up @@ -877,10 +938,25 @@
}

$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'];

Check warning on line 947 in src/Context/FeatureContext.php

View check run for this annotation

Codecov / codecov/patch

src/Context/FeatureContext.php#L946-L947

Added lines #L946 - L947 were not covered by tests
}

if ( self::$feature ) {
$env['BEHAT_FEATURE_TITLE'] = self::$feature->getTitle();

Check warning on line 951 in src/Context/FeatureContext.php

View check run for this annotation

Codecov / codecov/patch

src/Context/FeatureContext.php#L950-L951

Added lines #L950 - L951 were not covered by tests
}

if ( $this->scenario ) {
$env['BEHAT_SCENARIO_TITLE'] = $this->scenario->getTitle();

Check warning on line 955 in src/Context/FeatureContext.php

View check run for this annotation

Codecov / codecov/patch

src/Context/FeatureContext.php#L954-L955

Added lines #L954 - L955 were not covered by tests
}

$env['WP_CLI_TEST_DBTYPE'] = self::$db_type;

Check warning on line 958 in src/Context/FeatureContext.php

View check run for this annotation

Codecov / codecov/patch

src/Context/FeatureContext.php#L958

Added line #L958 was not covered by tests

if ( isset( $this->variables['RUN_DIR'] ) ) {
$cwd = "{$this->variables['RUN_DIR']}/{$path}";
} else {
Expand Down Expand Up @@ -1242,8 +1318,8 @@
}
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'] ) );

Check warning on line 1322 in src/Context/FeatureContext.php

View check run for this annotation

Codecov / codecov/patch

src/Context/FeatureContext.php#L1321-L1322

Added lines #L1321 - L1322 were not covered by tests
}
} elseif ( is_dir( $upd_file ) ) {
self::dir_diff_copy( $upd_file, $src_file, $cop_file );
Expand Down
2 changes: 2 additions & 0 deletions src/Context/WhenStepDefinitions.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
* @When /^I (run|try) `([^`]+)`$/
*/
public function when_i_run( $mode, $cmd ) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not the only step that triggers a WP-CLI command. For proper coverage, we'll need to cover all instances that trigger WP-CLI commands, like (for example) the below when_i_run_from_a_subfolder() and when_i_run_the_previous_command_again(). There might be others as well...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's only when_i_run_from_a_subfolder AFAICT, because when_i_run_the_previous_command_again just re-runs the previous command without any substitutions.

I don't think there are any others.

when_i_launch_in_the_background is used for launching wp server in the background, but it doesn't make sense to invoke coverage collection there. That means no code coverage for server-command but I think we can live with that .

$cmd = $this->get_command_with_coverage( $cmd );

Check warning on line 38 in src/Context/WhenStepDefinitions.php

View check run for this annotation

Codecov / codecov/patch

src/Context/WhenStepDefinitions.php#L38

Added line #L38 was not covered by tests
$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 );
Expand All @@ -44,6 +45,7 @@
* @When /^I (run|try) `([^`]+)` from '([^\s]+)'$/
*/
public function when_i_run_from_a_subfolder( $mode, $cmd, $subdir ) {
$cmd = $this->get_command_with_coverage( $cmd );

Check warning on line 48 in src/Context/WhenStepDefinitions.php

View check run for this annotation

Codecov / codecov/patch

src/Context/WhenStepDefinitions.php#L48

Added line #L48 was not covered by tests
$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 );
Expand Down
57 changes: 57 additions & 0 deletions utils/generate-coverage.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php

/**
* This script is added via `--require` to the WP-CLI commands executed by the Behat test runner.
* It starts coverage collection right away and registers a shutdown hook to complete it
* after the respective WP-CLI command has finished.
*/

use SebastianBergmann\CodeCoverage\CodeCoverage;
use SebastianBergmann\CodeCoverage\Driver\Selector;
use SebastianBergmann\CodeCoverage\Filter;
use SebastianBergmann\CodeCoverage\Report\Clover;

$root_folder = realpath( dirname( __DIR__ ) );

if ( ! class_exists( 'SebastianBergmann\CodeCoverage\Filter' ) ) {
require "{$root_folder}/vendor/autoload.php";
}

$filter = new Filter();
$filter->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 );
}
);