From de449f93144dfdde1d9d3365b2d3cf0a2fd97440 Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Fri, 19 Jun 2026 07:22:55 +0700 Subject: [PATCH 1/2] Allow define unless on usesClas, usesNamespace, usesFunction --- README.md | 119 +++++++++++++++++---------- src/Config/PyrameterConfig.php | 39 ++++++--- tests/Config/PyrameterConfigTest.php | 18 ++++ 3 files changed, 122 insertions(+), 54 deletions(-) diff --git a/README.md b/README.md index f364e9d..c2f6e12 100644 --- a/README.md +++ b/README.md @@ -50,19 +50,17 @@ Total: 60 tests Your suite is getting heavier. ``` -Pyrameter is a PHPUnit extension that reports the shape of your test suite after PHPUnit runs. It classifies each executed test as `unit`, `functional`, `integration`, or `e2e`, compares the totals with your target shape, and can fail CI when the suite drifts too far. - -It works from the classes and namespaces used by your test files, so you can define what counts as "heavy" in your project instead of relying on test directory names. +Pyrameter classifies executed tests as `unit`, `functional`, `integration`, or `e2e` from the code they use, then compares the totals with your target shape. ## Quick start -Install Pyrameter as a dev dependency: +1. Install via `composer`: ```bash composer require --dev boundwize/pyrameter ``` -Register the extension in `phpunit.xml`: +2. Register extension to `phpunit.xml`: ```xml @@ -70,17 +68,44 @@ Register the extension in `phpunit.xml`: ``` -Run PHPUnit: +3. Run PHPUnit as usual: ```bash vendor/bin/phpunit ``` -Without extra configuration, Pyrameter uses its default rules and target shape. If a `pyrameter.php` file exists in the current working directory, it is loaded automatically. +This uses the default rules and target shape. ## Configure -Create `pyrameter.php` when you want to tune classification rules, target percentages, or CI behavior: +### Defaults or an empty config + +Choose the starting point before adding rules: + +| Start with | Behavior | +| --- | --- | +| `PyrameterConfig::defaults()` | Starts with built-in rules for common database, cache, filesystem, Symfony, CodeIgniter, Panther, and WebDriver usage, plus the default target shape. Rule calls add more rules; `targetShape()` replaces the default targets. | +| `PyrameterConfig::create()` | Starts with no rules and no targets. Only your chained rules classify tests as heavier than `unit`. | + +Extend the built-in configuration: + +```php +return PyrameterConfig::defaults() + ->usesClass(App\Search\ExternalSearch::class, TestKind::Integration); +``` + +Define the complete configuration yourself: + +```php +return PyrameterConfig::create() + ->usesClass(PDO::class, TestKind::Integration) + ->targetShape( + unit: ['min' => 80], + integration: ['max' => 20], + ); +``` + +A complete `pyrameter.php` can then combine rules, targets, and CI behavior: ```php 15], integration: ['max' => 7], e2e: ['max' => 2], - ); + ) + ->failOnViolation(); ``` -`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. +Rules can match a class or trait, a namespace prefix, or a function: -Use `PyrameterConfig::create()` instead when you want to start with no rules or targets and define everything yourself: +| Rule | Example | +| --- | --- | +| `usesClass()` | `->usesClass(PDO::class, TestKind::Integration)` | +| `usesNamespace()` | `->usesNamespace('App\Tests\Browser\\', TestKind::E2E)` | +| `usesFunction()` | `->usesFunction('file_put_contents', TestKind::Integration)` | -```php -usesClass(PDO::class, TestKind::Integration) - ->usesNamespace('App\Controller\\', TestKind::Functional) - ->usesNamespace('App\Browser\\', TestKind::E2E) - ->usesFunction('file_put_contents', TestKind::Integration) - ->targetShape( - unit: ['min' => 70], - functional: ['max' => 20], - integration: ['max' => 8], - e2e: ['max' => 2], - ); + ->usesClass( + InteractsWithDatabase::class, + TestKind::Integration, + unless: [MakesHttpRequests::class], + ) + ->usesClass(MakesHttpRequests::class, TestKind::Functional); ``` -With `create()`, only the rules you add are used for heavier classifications; all other executed tests stay `unit`. +| Traits used by the test | Result | +| --- | --- | +| `InteractsWithDatabase` | `integration` | +| `MakesHttpRequests` | `functional` | +| Both traits | `functional` | + +The optional `unless` argument is also available on `usesNamespace()` and `usesFunction()`. + +The equivalent CodeIgniter exception is already included in the defaults: -Use `usesClass()` for a specific class, `usesNamespace()` for a namespace prefix, and `usesFunction()` for a function call that should classify a test as a heavier kind. Names can be configured with or without a leading backslash. +```php +return PyrameterConfig::defaults(); +``` + +Tests using only `DatabaseTestTrait` are `integration`; tests that also use `ControllerTestTrait` remain `functional`. -To load a config file from another path, pass the `config` parameter to the PHPUnit extension: +Load a config from another path: ```xml @@ -146,8 +181,6 @@ To load a config file from another path, pass the `config` parameter to the PHPU ## Classification -Pyrameter scans the consumed classes, namespaces, and function calls in each test file: - | Usage | Kind | | --- | --- | | No configured heavy usage | `unit` | @@ -155,19 +188,21 @@ Pyrameter scans the consumed classes, namespaces, and function calls in each tes | Database, cache, queue, filesystem, or external boundary | `integration` | | Browser driver usage | `e2e` | -When multiple rules match, the heaviest kind wins. Mocked heavy dependencies stay `unit` unless the test file also consumes a class or namespace you configured as heavier. - -Counts follow PHPUnit's executed test count, so data-provider datasets are counted separately. +- The heaviest matching rule wins. +- Mocked dependencies do not trigger a rule by themselves. +- Data-provider datasets are counted separately. ## Targets and CI -Targets are percentages. Missing `min` means `0`; missing `max` means `100`. - -By default, Pyrameter is report-only. To fail PHPUnit when the target shape is violated, enable `failOnViolation()`: - ```php return PyrameterConfig::defaults() + ->targetShape( + unit: ['min' => 70], + functional: ['max' => 20], + integration: ['max' => 8], + e2e: ['max' => 2], + ) ->failOnViolation(); ``` -Pyrameter is a pressure gauge for suite shape, not a perfect taxonomy judge. Tune the rules to match how your team defines unit, functional, integration, and e2e tests. +Targets are percentages. An omitted `min` is `0`; an omitted `max` is `100`. Without `failOnViolation()`, Pyrameter only reports violations. diff --git a/src/Config/PyrameterConfig.php b/src/Config/PyrameterConfig.php index 7446ba8..7ed5560 100644 --- a/src/Config/PyrameterConfig.php +++ b/src/Config/PyrameterConfig.php @@ -111,15 +111,14 @@ public static function defaults(): self ->usesNamespace('Predis\\', TestKind::Integration) ->usesNamespace('Symfony\Bundle\FrameworkBundle\Test\\', TestKind::Functional) ->usesClass(ControllerTestTrait::class, TestKind::Functional) + ->usesClass( + DatabaseTestTrait::class, + TestKind::Integration, + unless: [ControllerTestTrait::class], + ) ->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); } @@ -134,24 +133,40 @@ functional: ['max' => 18], /** * @param class-string $className + * @param list $unless */ - public function usesClass(string $className, TestKind $testKind): self + public function usesClass(string $className, TestKind $testKind, array $unless = []): self { - $this->usageRules[] = new UsageRule(ltrim($className, '\\'), $testKind); + $this->usageRules[] = new UsageRule(ltrim($className, '\\'), $testKind, unless: $unless); return $this; } - public function usesNamespace(string $namespace, TestKind $testKind): self + /** + * @param list $unless + */ + public function usesNamespace(string $namespace, TestKind $testKind, array $unless = []): self { - $this->usageRules[] = new UsageRule(rtrim(ltrim($namespace, '\\'), '\\') . '\\', $testKind); + $this->usageRules[] = new UsageRule( + rtrim(ltrim($namespace, '\\'), '\\') . '\\', + $testKind, + unless: $unless, + ); return $this; } - public function usesFunction(string $functionName, TestKind $testKind): self + /** + * @param list $unless + */ + public function usesFunction(string $functionName, TestKind $testKind, array $unless = []): self { - $this->usageRules[] = new UsageRule(ltrim($functionName, '\\'), $testKind, UsageType::Function); + $this->usageRules[] = new UsageRule( + ltrim($functionName, '\\'), + $testKind, + usageType: UsageType::Function, + unless: $unless, + ); return $this; } diff --git a/tests/Config/PyrameterConfigTest.php b/tests/Config/PyrameterConfigTest.php index 0c97ea2..2132059 100644 --- a/tests/Config/PyrameterConfigTest.php +++ b/tests/Config/PyrameterConfigTest.php @@ -95,4 +95,22 @@ public function testUsesFunctionMatchesFunctionUsageCaseInsensitively(): void $this->assertSame(TestKind::Integration, $usageClassifier->classify(['function:\FILE_GET_CONTENTS'])); } + + public function testUsesClassCanBeSuppressedByAnotherConsumedClass(): void + { + $pyrameterConfig = PyrameterConfig::create() + ->usesClass( + PDO::class, + TestKind::Integration, + unless: [self::class], + ) + ->usesClass(self::class, TestKind::Functional); + $usageClassifier = new UsageClassifier($pyrameterConfig->usageRules()); + + $this->assertSame( + TestKind::Functional, + $usageClassifier->classify([PDO::class, self::class]), + ); + $this->assertSame(TestKind::Integration, $usageClassifier->classify([PDO::class])); + } } From 96e835f958bb5f8f2f6c053825dacaa87972b07e Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Fri, 19 Jun 2026 07:25:43 +0700 Subject: [PATCH 2/2] fix grammar --- README.md | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index c2f6e12..1094a6b 100644 --- a/README.md +++ b/README.md @@ -50,17 +50,17 @@ Total: 60 tests Your suite is getting heavier. ``` -Pyrameter classifies executed tests as `unit`, `functional`, `integration`, or `e2e` from the code they use, then compares the totals with your target shape. +Pyrameter classifies executed tests as `unit`, `functional`, `integration`, or `e2e` based on the code they use, then compares the totals with your target shape. ## Quick start -1. Install via `composer`: +1. Install with Composer: ```bash composer require --dev boundwize/pyrameter ``` -2. Register extension to `phpunit.xml`: +2. Register the extension in `phpunit.xml`: ```xml @@ -78,14 +78,14 @@ This uses the default rules and target shape. ## Configure -### Defaults or an empty config +### Default or empty configuration Choose the starting point before adding rules: | Start with | Behavior | | --- | --- | -| `PyrameterConfig::defaults()` | Starts with built-in rules for common database, cache, filesystem, Symfony, CodeIgniter, Panther, and WebDriver usage, plus the default target shape. Rule calls add more rules; `targetShape()` replaces the default targets. | -| `PyrameterConfig::create()` | Starts with no rules and no targets. Only your chained rules classify tests as heavier than `unit`. | +| `PyrameterConfig::defaults()` | Starts with built-in rules and the default target shape. The rules cover common database, cache, and filesystem usage; Symfony and CodeIgniter functional tests; and Panther and WebDriver browser tests. Classification methods add rules; `targetShape()` replaces the targets. | +| `PyrameterConfig::create()` | Starts with no rules or targets. Only rules you add can classify tests as heavier than `unit`. | Extend the built-in configuration: @@ -105,7 +105,7 @@ return PyrameterConfig::create() ); ``` -A complete `pyrameter.php` can then combine rules, targets, and CI behavior: +A complete `pyrameter.php` can combine rules, targets, and CI behavior: ```php @@ -183,7 +183,7 @@ Load a config from another path: | Usage | Kind | | --- | --- | -| No configured heavy usage | `unit` | +| No matching heavier rule | `unit` | | Framework test runtime | `functional` | | Database, cache, queue, filesystem, or external boundary | `integration` | | Browser driver usage | `e2e` | @@ -205,4 +205,4 @@ return PyrameterConfig::defaults() ->failOnViolation(); ``` -Targets are percentages. An omitted `min` is `0`; an omitted `max` is `100`. Without `failOnViolation()`, Pyrameter only reports violations. +Targets are percentages. An omitted `min` defaults to `0`; an omitted `max` defaults to `100`. Without `failOnViolation()`, Pyrameter only reports violations.