Skip to content

Commit f81305d

Browse files
authored
[rule] add duplicated scenario names (#20)
* add duplicate scenario names * make use of match all * RM
1 parent 1f46171 commit f81305d

12 files changed

Lines changed: 166 additions & 15 deletions

README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,24 @@ This rule spots definitions that are no longer needed, so you can remove them.
108108

109109
<br>
110110

111+
### 4. Find duplicate scenario names (`duplicate-scenario-names`)
112+
113+
In Behat, each scenario should have a unique name to ensure clarity and avoid confusion during test execution and later debugging. This rule identifies scenarios that share the same name within your feature files:
114+
115+
```yaml
116+
Feature: User Authentication
117+
118+
Scenario: User logs in successfully
119+
When the user enters valid credentials
120+
Then login should be successful
121+
122+
Scenario: User logs in successfully
123+
When the user enters invalid credentials
124+
Then an error message should be displayed
125+
```
126+
127+
<br>
128+
111129
*Protip*: Add this command to your CI, to get instant feedback of any changes in every pull-request.
112130
113131
That's it!
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\Behastan\Analyzer;
6+
7+
use Entropy\Attributes\RelatedTest;
8+
use Entropy\Utils\Regex;
9+
use Rector\Behastan\Tests\Analyzer\DuplicatedScenarioNamesAnalyzer\DuplicatedScenarioNamesAnalyzerTest;
10+
use Symfony\Component\Finder\SplFileInfo;
11+
12+
#[RelatedTest(DuplicatedScenarioNamesAnalyzerTest::class)]
13+
final class DuplicatedScenarioNamesAnalyzer
14+
{
15+
private const string SCENARIO_NAME_REGEX = '#\s+Scenario:\s+(?<name>.*?)\n#';
16+
17+
/**
18+
* @param SplFileInfo[] $featureFiles
19+
* @return array<string, string[]>
20+
*/
21+
public function analyze(array $featureFiles): array
22+
{
23+
$scenarioNamesToFiles = [];
24+
25+
foreach ($featureFiles as $featureFile) {
26+
// match Scenario: "<name>"
27+
$matches = Regex::matchAll($featureFile->getContents(), self::SCENARIO_NAME_REGEX);
28+
29+
foreach ($matches as $match) {
30+
$scenarioName = $match['name'];
31+
$scenarioNamesToFiles[$scenarioName][] = $featureFile->getRealPath();
32+
}
33+
}
34+
35+
return array_filter($scenarioNamesToFiles, function (array $files): bool {
36+
return count($files) > 1;
37+
});
38+
}
39+
}

src/Analyzer/UnusedDefinitionsAnalyzer.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,11 @@ public function __construct(
2828
}
2929

3030
/**
31-
* @param SplFileInfo[] $contextFiles
3231
* @param SplFileInfo[] $featureFiles
3332
*
3433
* @return AbstractPattern[]
3534
*/
36-
public function analyse(array $contextFiles, array $featureFiles, PatternCollection $patternCollection): array
35+
public function analyse(array $featureFiles, PatternCollection $patternCollection): array
3736
{
3837
foreach ($featureFiles as $featureFile) {
3938
Assert::endsWith($featureFile->getFilename(), '.feature');

src/Command/AnalyzeCommand.php

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,9 @@ public function run(?string $projectDirectory = null, array $skip = []): int
4343
$contextFileInfos = BehatMetafilesFinder::findContextFiles([$projectDirectory]);
4444
if ($contextFileInfos === []) {
4545
$this->outputPrinter->redBackground(sprintf(
46-
'No *.Context files found in "%s". Please provide correct directory',
47-
$projectDirectory
46+
'No *.Context files found in "%s".%sPlease provide correct directory',
47+
$projectDirectory,
48+
PHP_EOL
4849
));
4950

5051
return ExitCode::ERROR;
@@ -53,8 +54,9 @@ public function run(?string $projectDirectory = null, array $skip = []): int
5354
$featureFileInfos = BehatMetafilesFinder::findFeatureFiles([$projectDirectory]);
5455
if ($featureFileInfos === []) {
5556
$this->outputPrinter->redBackground(sprintf(
56-
'No *.feature files found in "%s". Please provide correct directory',
57-
$projectDirectory
57+
'No *.feature files found in "%s".%sPlease provide correct directory',
58+
$projectDirectory,
59+
PHP_EOL
5860
));
5961

6062
return ExitCode::ERROR;
@@ -79,7 +81,7 @@ public function run(?string $projectDirectory = null, array $skip = []): int
7981
/** @var RuleError[] $allRuleErrors */
8082
$allRuleErrors = [];
8183
foreach ($this->rulesRegistry->all() as $rule) {
82-
if ($skip !== [] && in_array($rule->getIdentifier(), $skip, true)) {
84+
if (in_array($rule->getIdentifier(), $skip, true)) {
8385
$this->outputPrinter->writeln(sprintf('<fg=cyan>Skipping "%s" rule</>', $rule->getIdentifier()));
8486
$this->outputPrinter->newLine();
8587
continue;

src/Enum/RuleIdentifier.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ final class RuleIdentifier
88
{
99
public const string DUPLICATED_CONTENTS = 'duplicated-contents';
1010

11+
public const string DUPLICATED_SCENARIO_NAMES = 'duplicated-scenario-names';
12+
1113
public const string DUPLICATED_PATTERNS = 'duplicated-patterns';
1214

1315
public const string UNUSED_DEFINITIONS = 'unused-definitions';
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\Behastan\Rule;
6+
7+
use Rector\Behastan\Analyzer\DuplicatedScenarioNamesAnalyzer;
8+
use Rector\Behastan\Contract\RuleInterface;
9+
use Rector\Behastan\Enum\RuleIdentifier;
10+
use Rector\Behastan\ValueObject\PatternCollection;
11+
use Rector\Behastan\ValueObject\RuleError;
12+
use Symfony\Component\Finder\SplFileInfo;
13+
14+
final readonly class DuplicatedScenarioNamesRule implements RuleInterface
15+
{
16+
public function __construct(
17+
private DuplicatedScenarioNamesAnalyzer $duplicatedScenarioNamesAnalyzer
18+
) {
19+
}
20+
21+
/**
22+
* @param SplFileInfo[] $contextFiles
23+
* @param SplFileInfo[] $featureFiles
24+
*
25+
* @return RuleError[]
26+
*/
27+
public function process(
28+
array $contextFiles,
29+
array $featureFiles,
30+
PatternCollection $patternCollection,
31+
string $projectDirectory
32+
): array {
33+
$scenarioNamesToFiles = $this->duplicatedScenarioNamesAnalyzer->analyze($featureFiles);
34+
35+
$ruleErrors = [];
36+
foreach ($scenarioNamesToFiles as $scenarioName => $files) {
37+
// it can be used multiple times in single file
38+
$uniqueFiles = array_unique($files);
39+
$uniqueCount = count($uniqueFiles);
40+
41+
$errorMessage = sprintf('Scenario name "%s" is duplicated %d-times', $scenarioName, $uniqueCount);
42+
43+
$ruleErrors[] = new RuleError($errorMessage, $uniqueFiles, $this->getIdentifier());
44+
}
45+
46+
return $ruleErrors;
47+
}
48+
49+
public function getIdentifier(): string
50+
{
51+
return RuleIdentifier::DUPLICATED_SCENARIO_NAMES;
52+
}
53+
}

src/Rule/UnusedContextDefinitionsRule.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public function process(
2929
PatternCollection $patternCollection,
3030
string $projectDirectory
3131
): array {
32-
$unusedPatterns = $this->unusedDefinitionsAnalyzer->analyse($contextFiles, $featureFiles, $patternCollection);
32+
$unusedPatterns = $this->unusedDefinitionsAnalyzer->analyse($featureFiles, $patternCollection);
3333

3434
$ruleErrors = [];
3535

src/RulesRegistry.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ public function __construct(
1717
) {
1818
Assert::allObject($rules);
1919
Assert::allIsInstanceOf($rules, RuleInterface::class);
20-
Assert::greaterThan(count($rules), 2);
20+
Assert::greaterThan(count($rules), 3);
2121
}
2222

2323
/**
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\Behastan\Tests\Analyzer\DuplicatedScenarioNamesAnalyzer;
6+
7+
use Rector\Behastan\Analyzer\DuplicatedScenarioNamesAnalyzer;
8+
use Rector\Behastan\Finder\BehatMetafilesFinder;
9+
use Rector\Behastan\Tests\AbstractTestCase;
10+
11+
final class DuplicatedScenarioNamesAnalyzerTest extends AbstractTestCase
12+
{
13+
private DuplicatedScenarioNamesAnalyzer $duplicatedScenarioNamesAnalyzer;
14+
15+
protected function setUp(): void
16+
{
17+
parent::setUp();
18+
19+
$this->duplicatedScenarioNamesAnalyzer = $this->make(DuplicatedScenarioNamesAnalyzer::class);
20+
}
21+
22+
public function test(): void
23+
{
24+
$featureFiles = BehatMetafilesFinder::findFeatureFiles([__DIR__ . '/Fixture']);
25+
$this->assertCount(2, $featureFiles);
26+
27+
$duplicatedScenarioNamesToFiles = $this->duplicatedScenarioNamesAnalyzer->analyze($featureFiles);
28+
29+
$this->assertCount(1, $duplicatedScenarioNamesToFiles);
30+
$this->assertArrayHasKey('Same scenario name', $duplicatedScenarioNamesToFiles);
31+
32+
$givenFiles = $duplicatedScenarioNamesToFiles['Same scenario name'];
33+
34+
$this->assertSame([__DIR__ . '/Fixture/some.feature', __DIR__ . '/Fixture/another.feature'], $givenFiles);
35+
}
36+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Feature: Some feature
2+
3+
Scenario: Same scenario name

0 commit comments

Comments
 (0)