From 5fa95a84c469df44cdbe2ca456a00959d5553cc0 Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Fri, 19 Jun 2026 06:38:23 +0700 Subject: [PATCH 1/3] Add CodeIgniter4 ControllerTestTrait mark as functional test in default config --- README.md | 3 ++- composer.json | 1 + src/Config/PyrameterConfig.php | 2 ++ .../Fixtures/CodeIgniterFunctionalFixture.php | 20 +++++++++++++++++++ .../CodeIgniter/Test/ControllerTestTrait.php | 9 +++++++++ .../CodeIgniter/Test/DatabaseTestTrait.php | 9 +++++++++ tests/UsageClassificationTest.php | 5 +++++ 7 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 tests/Fixtures/CodeIgniterFunctionalFixture.php create mode 100644 tests/Fixtures/Stubs/CodeIgniter/Test/ControllerTestTrait.php create mode 100644 tests/Fixtures/Stubs/CodeIgniter/Test/DatabaseTestTrait.php diff --git a/README.md b/README.md index c4260cd..7f92d8c 100644 --- a/README.md +++ b/README.md @@ -102,7 +102,8 @@ 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, cache, filesystem, Symfony and CodeIgniter +functional tests, Panther, and WebDriver usage. 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..b6c5035 100644 --- a/src/Config/PyrameterConfig.php +++ b/src/Config/PyrameterConfig.php @@ -7,6 +7,7 @@ use Boundwize\Pyrameter\Rule\UsageRule; use Boundwize\Pyrameter\Rule\UsageType; use Boundwize\Pyrameter\TestKind; +use CodeIgniter\Test\ControllerTestTrait; use InvalidArgumentException; use mysqli; use PDO; @@ -108,6 +109,7 @@ 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); 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/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 @@ + [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 ControllerTestTrait means functional' => [ + CodeIgniterFunctionalFixture::class, + TestKind::Functional, + ]; 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]; From 3f03c8bb137adb5e46b70fb8541042198bf87f87 Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Fri, 19 Jun 2026 06:41:55 +0700 Subject: [PATCH 2/3] include CodeIgniter4 Database trait --- README.md | 4 ++-- src/Config/PyrameterConfig.php | 2 ++ .../Fixtures/CodeIgniterFunctionalFixture.php | 2 -- .../Fixtures/CodeIgniterIntegrationFixture.php | 18 ++++++++++++++++++ tests/UsageClassificationTest.php | 5 +++++ 5 files changed, 27 insertions(+), 4 deletions(-) create mode 100644 tests/Fixtures/CodeIgniterIntegrationFixture.php diff --git a/README.md b/README.md index 7f92d8c..9dc765c 100644 --- a/README.md +++ b/README.md @@ -102,8 +102,8 @@ return PyrameterConfig::defaults() ); ``` -`PyrameterConfig::defaults()` includes rules for common database, cache, filesystem, Symfony and CodeIgniter -functional tests, 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. Use `PyrameterConfig::create()` instead when you want to start with no rules or targets and define everything yourself: diff --git a/src/Config/PyrameterConfig.php b/src/Config/PyrameterConfig.php index b6c5035..07270f4 100644 --- a/src/Config/PyrameterConfig.php +++ b/src/Config/PyrameterConfig.php @@ -8,6 +8,7 @@ use Boundwize\Pyrameter\Rule\UsageType; use Boundwize\Pyrameter\TestKind; use CodeIgniter\Test\ControllerTestTrait; +use CodeIgniter\Test\DatabaseTestTrait; use InvalidArgumentException; use mysqli; use PDO; @@ -108,6 +109,7 @@ public static function defaults(): self ->usesClass('RedisCluster', TestKind::Integration) ->usesClass('RedisSentinel', TestKind::Integration) ->usesNamespace('Predis\\', TestKind::Integration) + ->usesClass(DatabaseTestTrait::class, TestKind::Integration) ->usesNamespace('Symfony\Bundle\FrameworkBundle\Test\\', TestKind::Functional) ->usesClass(ControllerTestTrait::class, TestKind::Functional) ->usesNamespace('Symfony\Component\Panther\\', TestKind::E2E) diff --git a/tests/Fixtures/CodeIgniterFunctionalFixture.php b/tests/Fixtures/CodeIgniterFunctionalFixture.php index e2ed2b0..43de2ee 100644 --- a/tests/Fixtures/CodeIgniterFunctionalFixture.php +++ b/tests/Fixtures/CodeIgniterFunctionalFixture.php @@ -5,13 +5,11 @@ namespace Boundwize\Pyrameter\Tests\Fixtures; use CodeIgniter\Test\ControllerTestTrait; -use CodeIgniter\Test\DatabaseTestTrait; use PHPUnit\Framework\TestCase; final class CodeIgniterFunctionalFixture extends TestCase { use ControllerTestTrait; - use DatabaseTestTrait; public function testItUsesTheControllerTestRuntime(): void { 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/UsageClassificationTest.php b/tests/UsageClassificationTest.php index 579e2c0..f52fede 100644 --- a/tests/UsageClassificationTest.php +++ b/tests/UsageClassificationTest.php @@ -9,6 +9,7 @@ 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; @@ -62,6 +63,10 @@ public static function classificationCases(): iterable 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]; From 1e1f06e9aac4b449fdc74e1f7b38de15a9a05f0e Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Fri, 19 Jun 2026 06:46:36 +0700 Subject: [PATCH 3/3] update functional --- README.md | 2 + src/Config/PyrameterConfig.php | 7 ++- src/Rule/UsageRule.php | 18 +++++++ src/UsageClassifier.php | 51 +++++++++++++------ .../Fixtures/CodeIgniterFunctionalFixture.php | 2 + tests/Rule/UsageRuleTest.php | 11 ++++ tests/UsageClassificationTest.php | 2 +- tests/UsageClassifierTest.php | 18 +++++++ 8 files changed, 94 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 9dc765c..f364e9d 100644 --- a/README.md +++ b/README.md @@ -104,6 +104,8 @@ return PyrameterConfig::defaults() `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/src/Config/PyrameterConfig.php b/src/Config/PyrameterConfig.php index 07270f4..7446ba8 100644 --- a/src/Config/PyrameterConfig.php +++ b/src/Config/PyrameterConfig.php @@ -109,12 +109,17 @@ public static function defaults(): self ->usesClass('RedisCluster', TestKind::Integration) ->usesClass('RedisSentinel', TestKind::Integration) ->usesNamespace('Predis\\', TestKind::Integration) - ->usesClass(DatabaseTestTrait::class, 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 index 43de2ee..e2ed2b0 100644 --- a/tests/Fixtures/CodeIgniterFunctionalFixture.php +++ b/tests/Fixtures/CodeIgniterFunctionalFixture.php @@ -5,11 +5,13 @@ namespace Boundwize\Pyrameter\Tests\Fixtures; use CodeIgniter\Test\ControllerTestTrait; +use CodeIgniter\Test\DatabaseTestTrait; use PHPUnit\Framework\TestCase; final class CodeIgniterFunctionalFixture extends TestCase { use ControllerTestTrait; + use DatabaseTestTrait; public function testItUsesTheControllerTestRuntime(): void { diff --git a/tests/Rule/UsageRuleTest.php b/tests/Rule/UsageRuleTest.php index 6863fb8..553bc5c 100644 --- a/tests/Rule/UsageRuleTest.php +++ b/tests/Rule/UsageRuleTest.php @@ -61,4 +61,15 @@ public function testUsageTypesExposeStablePrefixes(): void $this->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 f52fede..d77cc8b 100644 --- a/tests/UsageClassificationTest.php +++ b/tests/UsageClassificationTest.php @@ -59,7 +59,7 @@ 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 ControllerTestTrait means functional' => [ + yield 'CodeIgniter controller and database traits mean functional' => [ CodeIgniterFunctionalFixture::class, TestKind::Functional, ]; 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([