Skip to content

Commit 16ee1c9

Browse files
authored
Merge pull request #234 from wp-cli/code-coverage
Behat code coverage support
2 parents 558698b + 6458187 commit 16ee1c9

File tree

4 files changed

+152
-8
lines changed

4 files changed

+152
-8
lines changed

phpcs.xml.dist

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,4 +73,13 @@
7373
<rule ref="WordPress.NamingConventions.PrefixAllGlobals">
7474
<exclude-pattern>*/utils/polyfills\.php$</exclude-pattern>
7575
</rule>
76+
77+
<!-- This is a procedural stand-alone file that is never loaded in a WordPress context,
78+
so this file does not have to comply with WP naming conventions. -->
79+
<rule ref="WordPress.NamingConventions.PrefixAllGlobals">
80+
<exclude-pattern>*/generate-coverage\.php$</exclude-pattern>
81+
</rule>
82+
<rule ref="WordPress.WP.GlobalVariablesOverride">
83+
<exclude-pattern>*/generate-coverage\.php$</exclude-pattern>
84+
</rule>
7685
</ruleset>

src/Context/FeatureContext.php

Lines changed: 84 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
use Behat\Behat\Hook\Scope\BeforeScenarioScope;
88
use Behat\Testwork\Hook\Scope\AfterSuiteScope;
99
use Behat\Testwork\Hook\Scope\BeforeSuiteScope;
10+
use Behat\Behat\Hook\Scope\AfterFeatureScope;
11+
use Behat\Behat\Hook\Scope\BeforeFeatureScope;
1012
use RuntimeException;
1113
use WP_CLI\Process;
1214
use WP_CLI\Utils;
@@ -110,6 +112,48 @@ class FeatureContext implements SnippetAcceptingContext {
110112

111113
private $mocked_requests = [];
112114

115+
/**
116+
* The current feature.
117+
*
118+
* @var \Behat\Gherkin\Node\FeatureNode|null
119+
*/
120+
private static $feature;
121+
122+
/**
123+
* The current scenario.
124+
*
125+
* @var \Behat\Gherkin\Node\ScenarioInterface|null
126+
*/
127+
private $scenario;
128+
129+
/**
130+
* @BeforeFeature
131+
*/
132+
public static function store_feature( BeforeFeatureScope $scope ) {
133+
self::$feature = $scope->getFeature();
134+
}
135+
136+
/**
137+
* @BeforeScenario
138+
*/
139+
public function store_scenario( BeforeScenarioScope $scope ) {
140+
$this->scenario = $scope->getScenario();
141+
}
142+
143+
/**
144+
* @AfterScenario
145+
*/
146+
public function forget_scenario( AfterScenarioScope $scope ) {
147+
$this->scenario = null;
148+
}
149+
150+
/**
151+
* @AfterFeature
152+
*/
153+
public static function forget_feature( AfterFeatureScope $scope ) {
154+
self::$feature = null;
155+
}
156+
113157
/**
114158
* Get the path to the Composer vendor folder.
115159
*
@@ -333,9 +377,9 @@ private static function get_behat_internal_variables() {
333377
}
334378

335379
/**
336-
* Download and extract a single copy of the sqlite-database-integration plugin
337-
* for use in subsequent WordPress copies
338-
*/
380+
* Download and extract a single copy of the sqlite-database-integration plugin
381+
* for use in subsequent WordPress copies
382+
*/
339383
private static function download_sqlite_plugin( $dir ) {
340384
$download_url = 'https://downloads.wordpress.org/plugin/sqlite-database-integration.zip';
341385
$download_location = $dir . '/sqlite-database-integration.zip';
@@ -370,9 +414,9 @@ private static function download_sqlite_plugin( $dir ) {
370414
}
371415

372416
/**
373-
* Given a WordPress installation with the sqlite-database-integration plugin,
374-
* configure it to use SQLite as the database by placing the db.php dropin file
375-
*/
417+
* Given a WordPress installation with the sqlite-database-integration plugin,
418+
* configure it to use SQLite as the database by placing the db.php dropin file
419+
*/
376420
private static function configure_sqlite( $dir ) {
377421
$db_copy = $dir . '/wp-content/mu-plugins/sqlite-database-integration/db.copy';
378422
$db_dropin = $dir . '/wp-content/db.php';
@@ -639,6 +683,23 @@ public function __construct() {
639683
$this->set_cache_dir();
640684
}
641685

686+
/**
687+
* Enhances a `wp <command>` string with an additional `--require` for code coverage collection.
688+
*
689+
* Only applies if `WP_CLI_TEST_COVERAGE` is set.
690+
*
691+
* @param string $cmd Command string.
692+
* @return string Possibly enhanced command string.
693+
*/
694+
public function get_command_with_coverage( $cmd ) {
695+
$with_code_coverage = (string) getenv( 'WP_CLI_TEST_COVERAGE' );
696+
if ( \in_array( $with_code_coverage, [ 'true', '1' ], true ) ) {
697+
return preg_replace( '/(^wp )|( wp )|(\/wp )/', '$1$2$3--require={SRC_DIR}/utils/generate-coverage.php ', $cmd );
698+
}
699+
700+
return $cmd;
701+
}
702+
642703
/**
643704
* Replace standard {VARIABLE_NAME} variables and the special {INVOKE_WP_CLI_WITH_PHP_ARGS-args} and {WP_VERSION-version-latest} variables.
644705
* 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 = '' ) {
877938
}
878939

879940
$env = self::get_process_env_variables();
941+
880942
if ( isset( $this->variables['SUITE_CACHE_DIR'] ) ) {
881943
$env['WP_CLI_CACHE_DIR'] = $this->variables['SUITE_CACHE_DIR'];
882944
}
883945

946+
if ( isset( $this->variables['PROJECT_DIR'] ) ) {
947+
$env['BEHAT_PROJECT_DIR'] = $this->variables['PROJECT_DIR'];
948+
}
949+
950+
if ( self::$feature ) {
951+
$env['BEHAT_FEATURE_TITLE'] = self::$feature->getTitle();
952+
}
953+
954+
if ( $this->scenario ) {
955+
$env['BEHAT_SCENARIO_TITLE'] = $this->scenario->getTitle();
956+
}
957+
958+
$env['WP_CLI_TEST_DBTYPE'] = self::$db_type;
959+
884960
if ( isset( $this->variables['RUN_DIR'] ) ) {
885961
$cwd = "{$this->variables['RUN_DIR']}/{$path}";
886962
} else {
@@ -1242,8 +1318,8 @@ private static function dir_diff_copy( $upd_dir, $src_dir, $cop_dir ) {
12421318
}
12431319
self::copy_dir( $upd_file, $cop_file );
12441320
} elseif ( ! copy( $upd_file, $cop_file ) ) {
1245-
$error = error_get_last();
1246-
throw new RuntimeException( sprintf( "Failed to copy '%s' to '%s': %s. " . __FILE__ . ':' . __LINE__, $upd_file, $cop_file, $error['message'] ) );
1321+
$error = error_get_last();
1322+
throw new RuntimeException( sprintf( "Failed to copy '%s' to '%s': %s. " . __FILE__ . ':' . __LINE__, $upd_file, $cop_file, $error['message'] ) );
12471323
}
12481324
} elseif ( is_dir( $upd_file ) ) {
12491325
self::dir_diff_copy( $upd_file, $src_file, $cop_file );

src/Context/WhenStepDefinitions.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ public function when_i_launch_in_the_background( $cmd ) {
3535
* @When /^I (run|try) `([^`]+)`$/
3636
*/
3737
public function when_i_run( $mode, $cmd ) {
38+
$cmd = $this->get_command_with_coverage( $cmd );
3839
$cmd = $this->replace_variables( $cmd );
3940
$this->result = $this->wpcli_tests_invoke_proc( $this->proc( $cmd ), $mode );
4041
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 ) {
4445
* @When /^I (run|try) `([^`]+)` from '([^\s]+)'$/
4546
*/
4647
public function when_i_run_from_a_subfolder( $mode, $cmd, $subdir ) {
48+
$cmd = $this->get_command_with_coverage( $cmd );
4749
$cmd = $this->replace_variables( $cmd );
4850
$this->result = $this->wpcli_tests_invoke_proc( $this->proc( $cmd, array(), $subdir ), $mode );
4951
list( $this->result->stdout, $this->email_sends ) = $this->wpcli_tests_capture_email_sends( $this->result->stdout );

utils/generate-coverage.php

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<?php
2+
3+
/**
4+
* This script is added via `--require` to the WP-CLI commands executed by the Behat test runner.
5+
* It starts coverage collection right away and registers a shutdown hook to complete it
6+
* after the respective WP-CLI command has finished.
7+
*/
8+
9+
use SebastianBergmann\CodeCoverage\CodeCoverage;
10+
use SebastianBergmann\CodeCoverage\Driver\Selector;
11+
use SebastianBergmann\CodeCoverage\Filter;
12+
use SebastianBergmann\CodeCoverage\Report\Clover;
13+
14+
$root_folder = realpath( dirname( __DIR__ ) );
15+
16+
if ( ! class_exists( 'SebastianBergmann\CodeCoverage\Filter' ) ) {
17+
require "{$root_folder}/vendor/autoload.php";
18+
}
19+
20+
$filter = new Filter();
21+
$filter->includeDirectory( "{$root_folder}/includes" );
22+
$filter->includeFiles( array( "{$root_folder}/plugin.php" ) );
23+
24+
$coverage = new CodeCoverage(
25+
( new Selector() )->forLineCoverage( $filter ),
26+
$filter
27+
);
28+
29+
/*
30+
* The names of the current feature and scenario are passed on from the Behat test runner
31+
* to this script through environment variables `BEHAT_FEATURE_TITLE` & `BEHAT_SCENARIO_TITLE`.
32+
*/
33+
$feature = getenv( 'BEHAT_FEATURE_TITLE' );
34+
$scenario = getenv( 'BEHAT_SCENARIO_TITLE' );
35+
$name = "{$feature} - {$scenario}";
36+
37+
$coverage->start( $name );
38+
39+
register_shutdown_function(
40+
static function () use ( $coverage, $feature, $scenario, $name ) {
41+
$coverage->stop();
42+
43+
$project_dir = (string) getenv( 'BEHAT_PROJECT_DIR' );
44+
45+
$feature_suffix = preg_replace( '/[^a-z0-9]+/', '-', strtolower( $feature ) );
46+
$scenario_suffix = preg_replace( '/[^a-z0-9]+/', '-', strtolower( $scenario ) );
47+
$db_type = strtolower( getenv( 'WP_CLI_TEST_DBTYPE' ) );
48+
$destination = "$project_dir/build/logs/$feature_suffix-$scenario_suffix-$db_type.xml";
49+
50+
$dir = dirname( $destination );
51+
if ( ! file_exists( $dir ) ) {
52+
mkdir( $dir, 0777, true /*recursive*/ );
53+
}
54+
55+
( new Clover() )->process( $coverage, $destination, $name );
56+
}
57+
);

0 commit comments

Comments
 (0)