diff --git a/README.md b/README.md index c4260cd..f364e9d 100644 --- a/README.md +++ b/README.md @@ -102,7 +102,10 @@ return PyrameterConfig::defaults() ); ``` -`PyrameterConfig::defaults()` includes rules for common database, cache, filesystem, Symfony functional test, Panther, and WebDriver usage. +`PyrameterConfig::defaults()` includes rules for common database usage (including CodeIgniter database tests), +cache, filesystem, Symfony and CodeIgniter controller functional tests, Panther, and WebDriver usage. +CodeIgniter tests using both `ControllerTestTrait` and `DatabaseTestTrait` remain functional; database-only tests +are integration tests. Use `PyrameterConfig::create()` instead when you want to start with no rules or targets and define everything yourself: diff --git a/composer.json b/composer.json index 7824543..2303d7a 100644 --- a/composer.json +++ b/composer.json @@ -22,6 +22,7 @@ "autoload-dev": { "psr-4": { "Boundwize\\Pyrameter\\Tests\\": "tests/", + "CodeIgniter\\Test\\": "tests/Fixtures/Stubs/CodeIgniter/Test/", "Doctrine\\DBAL\\": "tests/Fixtures/Stubs/Doctrine/DBAL/", "Facebook\\WebDriver\\": "tests/Fixtures/Stubs/Facebook/WebDriver/", "Symfony\\Bundle\\FrameworkBundle\\Test\\": "tests/Fixtures/Stubs/Symfony/Bundle/FrameworkBundle/Test/", diff --git a/src/Config/PyrameterConfig.php b/src/Config/PyrameterConfig.php index fedebed..7446ba8 100644 --- a/src/Config/PyrameterConfig.php +++ b/src/Config/PyrameterConfig.php @@ -7,6 +7,8 @@ use Boundwize\Pyrameter\Rule\UsageRule; use Boundwize\Pyrameter\Rule\UsageType; use Boundwize\Pyrameter\TestKind; +use CodeIgniter\Test\ControllerTestTrait; +use CodeIgniter\Test\DatabaseTestTrait; use InvalidArgumentException; use mysqli; use PDO; @@ -108,9 +110,16 @@ public static function defaults(): self ->usesClass('RedisSentinel', TestKind::Integration) ->usesNamespace('Predis\\', TestKind::Integration) ->usesNamespace('Symfony\Bundle\FrameworkBundle\Test\\', TestKind::Functional) + ->usesClass(ControllerTestTrait::class, TestKind::Functional) ->usesNamespace('Symfony\Component\Panther\\', TestKind::E2E) ->usesNamespace('Facebook\WebDriver\\', TestKind::E2E); + $pyrameterConfig->usageRules[] = new UsageRule( + DatabaseTestTrait::class, + TestKind::Integration, + unless: [ControllerTestTrait::class], + ); + foreach (self::FILE_OPERATION_FUNCTIONS as $functionName) { $pyrameterConfig->usesFunction($functionName, TestKind::Integration); } diff --git a/src/Rule/UsageRule.php b/src/Rule/UsageRule.php index 7450b06..964a014 100644 --- a/src/Rule/UsageRule.php +++ b/src/Rule/UsageRule.php @@ -17,10 +17,14 @@ final readonly class UsageRule { + /** + * @param list $unless + */ public function __construct( public string $usage, public TestKind $kind, private UsageType $usageType = UsageType::ClassLike, + private array $unless = [], ) { } @@ -55,6 +59,20 @@ public function normalizedKey(): string return $this->usageKey($this->usageType, $this->normalizedUsage()); } + /** + * @return list + */ + public function normalizedUnlessKeys(): array + { + $normalizedUnlessKeys = []; + + foreach ($this->unless as $unlessUsage) { + $normalizedUnlessKeys[] = $this->usageKey($this->usageType, $this->normalize($unlessUsage)); + } + + return $normalizedUnlessKeys; + } + private function normalize(string $usage): string { return strtolower(ltrim($usage, '\\')); diff --git a/src/UsageClassifier.php b/src/UsageClassifier.php index 8bf4136..59833e3 100644 --- a/src/UsageClassifier.php +++ b/src/UsageClassifier.php @@ -7,6 +7,7 @@ use Boundwize\Pyrameter\Rule\UsageRule; use Boundwize\Pyrameter\Rule\UsageType; +use function array_keys; use function ltrim; use function sprintf; use function str_contains; @@ -18,10 +19,10 @@ final readonly class UsageClassifier { - /** @var array */ + /** @var array}>> */ private array $exactRules; - /** @var list */ + /** @var list}> */ private array $namespaceRules; /** @@ -35,14 +36,18 @@ public function __construct(array $rules) foreach ($rules as $rule) { if ($rule->isNamespaceRule()) { $namespaceRules[] = [ - 'usage' => $rule->normalizedKey(), - 'kind' => $rule->kind, + 'usage' => $rule->normalizedKey(), + 'kind' => $rule->kind, + 'unless' => $rule->normalizedUnlessKeys(), ]; continue; } - $this->addExactRule($exactRules, $rule->normalizedKey(), $rule->kind); + $exactRules[$rule->normalizedKey()][] = [ + 'kind' => $rule->kind, + 'unless' => $rule->normalizedUnlessKeys(), + ]; } $this->exactRules = $exactRules; @@ -54,13 +59,21 @@ public function __construct(array $rules) */ public function classify(array $consumedUsages): TestKind { - $kind = TestKind::Unit; + $kind = TestKind::Unit; + $normalizedConsumedUsages = []; foreach ($consumedUsages as $consumedUsage) { - $normalizedConsumedUsage = $this->normalizeConsumedUsage($consumedUsage); + $normalizedConsumedUsage = $this->normalizeConsumedUsage($consumedUsage); + $normalizedConsumedUsages[$normalizedConsumedUsage] = true; + } + + foreach (array_keys($normalizedConsumedUsages) as $normalizedConsumedUsage) { + foreach ($this->exactRules[$normalizedConsumedUsage] ?? [] as $exactRule) { + if ($this->isSuppressed($exactRule['unless'], $normalizedConsumedUsages)) { + continue; + } - if (isset($this->exactRules[$normalizedConsumedUsage])) { - $kind = $this->heaviest($kind, $this->exactRules[$normalizedConsumedUsage]); + $kind = $this->heaviest($kind, $exactRule['kind']); if ($kind === TestKind::E2E) { return $kind; @@ -68,7 +81,10 @@ public function classify(array $consumedUsages): TestKind } foreach ($this->namespaceRules as $namespaceRule) { - if (! $this->matchesNamespaceRule($normalizedConsumedUsage, $namespaceRule['usage'])) { + if ( + ! $this->matchesNamespaceRule($normalizedConsumedUsage, $namespaceRule['usage']) + || $this->isSuppressed($namespaceRule['unless'], $normalizedConsumedUsages) + ) { continue; } @@ -84,13 +100,18 @@ public function classify(array $consumedUsages): TestKind } /** - * @param array $rules + * @param list $unless + * @param array $normalizedConsumedUsages */ - private function addExactRule(array &$rules, string $usage, TestKind $testKind): void + private function isSuppressed(array $unless, array $normalizedConsumedUsages): bool { - $rules[$usage] = isset($rules[$usage]) - ? $this->heaviest($rules[$usage], $testKind) - : $testKind; + foreach ($unless as $unlessUsage) { + if (isset($normalizedConsumedUsages[$unlessUsage])) { + return true; + } + } + + return false; } private function heaviest(TestKind $left, TestKind $right): TestKind diff --git a/tests/Fixtures/CodeIgniterFunctionalFixture.php b/tests/Fixtures/CodeIgniterFunctionalFixture.php new file mode 100644 index 0000000..e2ed2b0 --- /dev/null +++ b/tests/Fixtures/CodeIgniterFunctionalFixture.php @@ -0,0 +1,20 @@ +addToAssertionCount(1); + } +} diff --git a/tests/Fixtures/CodeIgniterIntegrationFixture.php b/tests/Fixtures/CodeIgniterIntegrationFixture.php new file mode 100644 index 0000000..d08e455 --- /dev/null +++ b/tests/Fixtures/CodeIgniterIntegrationFixture.php @@ -0,0 +1,18 @@ +addToAssertionCount(1); + } +} diff --git a/tests/Fixtures/Stubs/CodeIgniter/Test/ControllerTestTrait.php b/tests/Fixtures/Stubs/CodeIgniter/Test/ControllerTestTrait.php new file mode 100644 index 0000000..6f5e482 --- /dev/null +++ b/tests/Fixtures/Stubs/CodeIgniter/Test/ControllerTestTrait.php @@ -0,0 +1,9 @@ +assertSame('class', UsageType::ClassLike->value); $this->assertSame('function', UsageType::Function->value); } + + public function testItNormalizesUnlessUsages(): void + { + $usageRule = new UsageRule( + 'Framework\DatabaseTrait', + TestKind::Integration, + unless: ['\FRAMEWORK\ControllerTrait'], + ); + + $this->assertSame(['class:framework\controllertrait'], $usageRule->normalizedUnlessKeys()); + } } diff --git a/tests/UsageClassificationTest.php b/tests/UsageClassificationTest.php index c9f553e..d77cc8b 100644 --- a/tests/UsageClassificationTest.php +++ b/tests/UsageClassificationTest.php @@ -8,6 +8,8 @@ use Boundwize\Pyrameter\Detection\ConsumedUsageExtractor; use Boundwize\Pyrameter\Detection\TestUsageScanner; use Boundwize\Pyrameter\TestKind; +use Boundwize\Pyrameter\Tests\Fixtures\CodeIgniterFunctionalFixture; +use Boundwize\Pyrameter\Tests\Fixtures\CodeIgniterIntegrationFixture; use Boundwize\Pyrameter\Tests\Fixtures\ContainerGetHeavyFixture; use Boundwize\Pyrameter\Tests\Fixtures\DoctrineUsageFixture; use Boundwize\Pyrameter\Tests\Fixtures\FileOperationUsageFixture; @@ -57,6 +59,14 @@ public static function classificationCases(): iterable yield 'Doctrine DBAL usage means integration' => [DoctrineUsageFixture::class, TestKind::Integration]; yield 'file operation usage means integration' => [FileOperationUsageFixture::class, TestKind::Integration]; yield 'Symfony WebTestCase means functional' => [SymfonyFunctionalFixture::class, TestKind::Functional]; + yield 'CodeIgniter controller and database traits mean functional' => [ + CodeIgniterFunctionalFixture::class, + TestKind::Functional, + ]; + yield 'CodeIgniter DatabaseTestTrait means integration' => [ + CodeIgniterIntegrationFixture::class, + TestKind::Integration, + ]; yield 'Panther usage means e2e' => [PantherE2EFixture::class, TestKind::E2E]; yield 'WebDriver usage means e2e' => [WebDriverE2EFixture::class, TestKind::E2E]; yield 'mocked heavy class stays unit' => [MockedHeavyFixture::class, TestKind::Unit]; diff --git a/tests/UsageClassifierTest.php b/tests/UsageClassifierTest.php index 403814f..cbff8a5 100644 --- a/tests/UsageClassifierTest.php +++ b/tests/UsageClassifierTest.php @@ -102,6 +102,24 @@ public function testDuplicateExactRulesUseTheHeaviestKind(): void $this->assertSame(TestKind::Integration, $usageClassifier->classify(['App\Service'])); } + public function testRuleCanBeSuppressedByAnotherConsumedUsage(): void + { + $usageClassifier = new UsageClassifier([ + new UsageRule('Framework\DatabaseTrait', TestKind::Integration, unless: ['Framework\ControllerTrait']), + new UsageRule('Framework\ControllerTrait', TestKind::Functional), + ]); + + $this->assertSame( + TestKind::Functional, + $usageClassifier->classify(['Framework\ControllerTrait', 'Framework\DatabaseTrait']), + ); + $this->assertSame( + TestKind::Functional, + $usageClassifier->classify(['Framework\DatabaseTrait', 'Framework\ControllerTrait']), + ); + $this->assertSame(TestKind::Integration, $usageClassifier->classify(['Framework\DatabaseTrait'])); + } + public function testHeaviestMatchingRuleWinsAfterRulesArePrecompiled(): void { $usageClassifier = new UsageClassifier([