Skip to content

Commit

Permalink
Merge pull request #7 from vever001/filters
Browse files Browse the repository at this point in the history
Include/exclude filters + test against concrete implementation.
  • Loading branch information
nicwortel authored Jul 11, 2024
2 parents fbf3671 + c0ce3b4 commit 274d76a
Show file tree
Hide file tree
Showing 19 changed files with 274 additions and 88 deletions.
41 changes: 35 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,23 +40,52 @@ listed per suite after the suite has finished.

### Filtering the results

#### Include / exclude

There are projects where it's important to avoid some step definitions to be
detected. For instance, when a project wants to avoid scanning unused step
definitions from the third-party packages/libraries and show only results from
the custom code. The extension allows to configure a _regular expression_ filter
in the `behat.yml` configuration file. The filter will only allow definitions
whose context class name satisfies the regular expression:
the custom code. The extension allows to configure a list of
_regular expressions_ to include or exclude step definitions in the `behat.yml`
configuration file. Expressions are compared against the FQCN + method name
(`My\Namespace\ClassName::methodName`):

```yaml
default:
extensions:
NicWortel\BehatUnusedStepDefinitionsExtension\Extension:
filter: '#\\MyProject\\Behat\\Contexts#'
filters:
include:
- '/MyProject\\Behat\\Contexts/'
- '/OtherProject\\Behat\\(Foo|Bar)Context/'
exclude:
- '/MyProject\\Behat\\Contexts\\FeatureContext/'
- '/::excludedMethod/'
- '/OtherProject\\Behat\\FooContext::.+Method/'
```
In this example only unused step definitions from classes with the namespace
containing `\MyProject\Behat\Contexts` will be outputted.
#### Ignore pattern aliases
Example:
```php
/**
* @Then I take a screenshot
* @Then I take a screenshot :name
*/
public function takeScreenshot(?string $name = NULL): void {
// Step implementation.
}
```
If `I take a screenshot` is used but `I take a screenshot :name` is not,
enabling `ignorePatternAliases: true` will prevent the latter from being
reported as unused.

```yaml
default:
extensions:
NicWortel\BehatUnusedStepDefinitionsExtension\Extension:
ignorePatternAliases: true
```
## Extending
Expand Down
6 changes: 2 additions & 4 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,15 @@
"license": "MIT",
"require": {
"php": "^8.1",
"behat/behat": "^3.0",
"behat/behat": "^3.9",
"symfony/config": "^4.4 || ^5.4 || ^6.0 || ^7.0"
},
"require-dev": {
"nicwortel/coding-standard": "^2.0",
"nicwortel/coding-standard": "^2.3",
"phpstan/phpstan": "^1.8",
"phpstan/phpstan-symfony": "^1.2",
"phpunit/phpunit": "^10.3",
"rector/rector": "^0.18.4",
"slevomat/coding-standard": ">=6.3.11",
"squizlabs/php_codesniffer": "^3.6",
"symfony/process": "^6.0"
},
"config": {
Expand Down
2 changes: 2 additions & 0 deletions phpcs.xml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
<file>tests/</file>

<exclude-pattern>tests/fixtures/features/bootstrap/FeatureContext.php</exclude-pattern>
<exclude-pattern>tests/fixtures/features/bootstrap/FeatureContextBase.php</exclude-pattern>

<rule ref="NicWortel"/>

</ruleset>
52 changes: 49 additions & 3 deletions src/Extension.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;

use function sprintf;
use function trigger_error;

use const E_USER_DEPRECATED;

final class Extension implements BehatExtension
{
public function process(ContainerBuilder $container): void
Expand All @@ -32,23 +37,64 @@ public function initialize(ExtensionManager $extensionManager): void
public function configure(ArrayNodeDefinition $builder): void
{
$builder->children()
->scalarNode('printer')->defaultValue('unused_step_definitions_printer')->end()
->scalarNode('printer')
->defaultValue('unused_step_definitions_printer')
->end()
->booleanNode('ignorePatternAliases')
->defaultFalse()
->end()
// @todo Deprecated config key, remove when possible.
->scalarNode('filter')->end()
->arrayNode('filters')
->info('Specifies include/exclude filters')
->performNoDeepMerging()
->children()
->arrayNode('include')
->defaultValue([])
->useAttributeAsKey('name')
->prototype('variable')->end()
->end()
->arrayNode('exclude')
->defaultValue([])
->useAttributeAsKey('name')
->prototype('variable')->end()
->end()
->end()
->end()
->end();
}

/**
* @param array{printer: string, filter: ?string} $config
* @param array{
* printer: string,
* filter: ?string,
* filters: array{include: string[], exclude: string[]}|null,
* ignorePatternAliases: bool
* } $config
*/
public function load(ContainerBuilder $container, array $config): void
{
// @todo Deprecated config key, remove when possible.
if (isset($config['filter'])) {
$config['filters']['include'][] = $config['filter'];
@trigger_error(
sprintf(
'Since %s %s: The "filter" config key is deprecated, use "filters.include" instead.',
'nicwortel/behat-unused-step-definitions-extension',
'1.1.2',
),
E_USER_DEPRECATED
);
}

$serviceDefinition = new Definition(
UnusedStepDefinitionsChecker::class,
[
new Reference(DefinitionExtension::FINDER_ID),
new Reference(DefinitionExtension::REPOSITORY_ID),
new Reference($config['printer']),
$config['filter'] ?? null,
$config['ignorePatternAliases'],
$config['filters'] ?? null,
]
);
$serviceDefinition->addTag(EventDispatcherExtension::SUBSCRIBER_TAG);
Expand Down
76 changes: 70 additions & 6 deletions src/UnusedStepDefinitionsChecker.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,14 @@
use Behat\Behat\Definition\DefinitionRepository;
use Behat\Behat\EventDispatcher\Event\AfterStepTested;
use Behat\Testwork\EventDispatcher\Event\AfterSuiteTested;
use ReflectionFunctionAbstract;
use ReflectionMethod;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

use function array_diff;
use function array_filter;
use function preg_match;
use function sprintf;

final class UnusedStepDefinitionsChecker implements EventSubscriberInterface
{
Expand All @@ -22,11 +25,15 @@ final class UnusedStepDefinitionsChecker implements EventSubscriberInterface
*/
private array $usedDefinitions = [];

/**
* @param array{include: string[], exclude: string[]}|null $filters
*/
public function __construct(
private readonly DefinitionFinder $definitionFinder,
private readonly DefinitionRepository $definitionRepository,
private readonly UnusedStepDefinitionsPrinter $printer,
private readonly ?string $filter
private readonly bool $ignorePatternAliases,
private readonly ?array $filters
) {
}

Expand Down Expand Up @@ -63,13 +70,70 @@ public function checkUnusedStepDefinitions(AfterSuiteTested $event): void
/** @var Definition[] $unusedDefinitions */
$unusedDefinitions = array_diff($definitions, $this->usedDefinitions);

if ($this->filter) {
$unusedDefinitions = array_filter(
$unusedDefinitions,
fn(Definition $definition): bool => (bool) preg_match((string) $this->filter, $definition->getPath())
);
if ($this->ignorePatternAliases) {
$unusedDefinitions = $this->filterPatternAliases($unusedDefinitions);
}

if ($this->filters) {
$unusedDefinitions = $this->filterIncludeExclude($unusedDefinitions);
}

$this->printer->printUnusedStepDefinitions($unusedDefinitions);
}

/**
* @param Definition[] $unusedDefinitions
* @return Definition[]
*/
private function filterPatternAliases(array $unusedDefinitions): array
{
$usedPaths = [];
foreach ($this->usedDefinitions as $def) {
$usedPaths[$def->getPath()] = true;
}

return array_filter($unusedDefinitions, function (Definition $definition) use ($usedPaths) {
return !isset($usedPaths[$definition->getPath()]);
});
}

/**
* @param Definition[] $unusedDefinitions
* @return Definition[]
*/
private function filterIncludeExclude(array $unusedDefinitions): array
{
return array_filter($unusedDefinitions, function (Definition $definition) {
// Get the concrete path reference for this definition.
$path = $this->getConcretePath($definition->getReflection());

$includePatterns = $this->filters['include'] ?? null;
$includeMatch = $includePatterns ? $this->matchesPatterns($path, $includePatterns) : true;

$excludePatterns = $this->filters['exclude'] ?? null;
$excludeMatch = $excludePatterns ? $this->matchesPatterns($path, $excludePatterns) : false;

return $includeMatch && !$excludeMatch;
});
}

/**
* @param string[] $patterns
*/
private function matchesPatterns(string $path, array $patterns): bool
{
foreach ($patterns as $pattern) {
if ((bool) preg_match($pattern, $path)) {
return true;
}
}
return false;
}

private function getConcretePath(ReflectionFunctionAbstract $function): string
{
return $function instanceof ReflectionMethod
? sprintf('%s::%s()', $function->getDeclaringClass()->getName(), $function->getName())
: sprintf('%s()', $function->getName());
}
}
89 changes: 32 additions & 57 deletions tests/BehatExtensionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,78 +9,53 @@

use function file_get_contents;
use function sys_get_temp_dir;
use function unlink;

class BehatExtensionTest extends TestCase
{
public function testPrintsUnusedStepDefinitions(): void
/**
* @return array<array{0: string}>
*/
public static function behatProfilesProvider(): array
{
$behat = new Process(['../../vendor/bin/behat', '--config', 'behat.yml'], __DIR__ . '/fixtures/');
$behat->mustRun();

$this->assertStringContainsString('2 unused step definitions:', $behat->getOutput());

$this->assertStringContainsString(
'Given some precondition that is never used in a feature # FeatureContext::somePrecondition()',
$behat->getOutput()
);
$this->assertStringContainsString(
'Then some step that is never used by a feature # FeatureContext::someStepThatIsNeverUsedByAFeature()',
$behat->getOutput()
);
return [
['default'],
['ignore_pattern_aliases'],
['filter_bc'],
['filter_inheritance'],
['filter_include'],
['filter_exclude'],
['filter_include_exclude'],
];
}

public function testDoesNotPrintStepDefinitionsThatAreUsed(): void
/**
* @dataProvider behatProfilesProvider
*/
public function testBehatProfiles(string $profile): void
{
$behat = new Process(['../../vendor/bin/behat', '--config', 'behat.yml'], __DIR__ . '/fixtures/');
$behat->mustRun();

$this->assertStringNotContainsString(
'When some action by the actor # FeatureContext::someActionByTheActor()',
$behat->getOutput()
);
$actual = $this->runBehat($profile);
$expected = file_get_contents(__DIR__ . "/fixtures/expectations/$profile.txt");
$this->assertStringContainsString($expected, $actual);
}

public function testCustomPrinter(): void
{
$behat = new Process(['../../vendor/bin/behat', '--config', 'behat_extended.yml'], __DIR__ . '/fixtures/');
$behat->mustRun();

$profile = 'custom_printer';
$outputFile = sys_get_temp_dir() . '/unused_step_defs.txt';
$this->runBehat($profile);

$this->assertFileExists($outputFile);
$fileContents = file_get_contents($outputFile);
$this->assertStringContainsString(
'Given some precondition that is never used in a feature # FeatureContext::somePrecondition()',
$fileContents
);
$this->assertStringContainsString(
'Then some step that is never used by a feature # FeatureContext::someStepThatIsNeverUsedByAFeature()',
$fileContents
);
$this->assertStringNotContainsString(
'When some action by the actor # FeatureContext::someActionByTheActor()',
$fileContents
);
$actual = file_get_contents($outputFile);
unlink($outputFile);

$expected = file_get_contents(__DIR__ . "/fixtures/expectations/$profile.txt");
$this->assertEquals($expected, $actual);
}

public function testWithFilter(): void
private function runBehat(string $profile): string
{
$behat = new Process(['../../vendor/bin/behat', '--config', 'behat_filtered.yml'], __DIR__ . '/fixtures/');
$behat->mustRun();

$this->assertStringContainsString('1 unused step definitions:', $behat->getOutput());

$this->assertStringNotContainsString(
'Given some precondition that is never used in a feature # FeatureContext::somePrecondition()',
$behat->getOutput()
);
$this->assertStringNotContainsString(
'When some action by the actor # FeatureContext::someActionByTheActor()',
$behat->getOutput()
);
// Only this step definition is passing the filter.
$this->assertStringContainsString(
'Then some step that is never used by a feature # FeatureContext::someStepThatIsNeverUsedByAFeature()',
$behat->getOutput()
);
$behat = new Process(['../../vendor/bin/behat', '--profile', $profile], __DIR__ . '/fixtures/');
return $behat->mustRun()->getOutput();
}
}
Loading

0 comments on commit 274d76a

Please sign in to comment.