Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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/",
Expand Down
9 changes: 9 additions & 0 deletions src/Config/PyrameterConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
Expand Down
18 changes: 18 additions & 0 deletions src/Rule/UsageRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,14 @@

final readonly class UsageRule
{
/**
* @param list<string> $unless
*/
public function __construct(
public string $usage,
public TestKind $kind,
private UsageType $usageType = UsageType::ClassLike,
private array $unless = [],
) {
}

Expand Down Expand Up @@ -55,6 +59,20 @@ public function normalizedKey(): string
return $this->usageKey($this->usageType, $this->normalizedUsage());
}

/**
* @return list<string>
*/
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, '\\'));
Expand Down
51 changes: 36 additions & 15 deletions src/UsageClassifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -18,10 +19,10 @@

final readonly class UsageClassifier
{
/** @var array<string, TestKind> */
/** @var array<string, list<array{kind: TestKind, unless: list<string>}>> */
private array $exactRules;

/** @var list<array{usage: string, kind: TestKind}> */
/** @var list<array{usage: string, kind: TestKind, unless: list<string>}> */
private array $namespaceRules;

/**
Expand All @@ -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;
Expand All @@ -54,21 +59,32 @@ 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;
}
}

foreach ($this->namespaceRules as $namespaceRule) {
if (! $this->matchesNamespaceRule($normalizedConsumedUsage, $namespaceRule['usage'])) {
if (
! $this->matchesNamespaceRule($normalizedConsumedUsage, $namespaceRule['usage'])
|| $this->isSuppressed($namespaceRule['unless'], $normalizedConsumedUsages)
) {
continue;
}

Expand All @@ -84,13 +100,18 @@ public function classify(array $consumedUsages): TestKind
}

/**
* @param array<string, TestKind> $rules
* @param list<string> $unless
* @param array<string, true> $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
Expand Down
20 changes: 20 additions & 0 deletions tests/Fixtures/CodeIgniterFunctionalFixture.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

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
{
$this->addToAssertionCount(1);
}
}
18 changes: 18 additions & 0 deletions tests/Fixtures/CodeIgniterIntegrationFixture.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

declare(strict_types=1);

namespace Boundwize\Pyrameter\Tests\Fixtures;

use CodeIgniter\Test\DatabaseTestTrait;
use PHPUnit\Framework\TestCase;

final class CodeIgniterIntegrationFixture extends TestCase
{
use DatabaseTestTrait;

public function testItUsesTheDatabaseTestRuntime(): void
{
$this->addToAssertionCount(1);
}
}
9 changes: 9 additions & 0 deletions tests/Fixtures/Stubs/CodeIgniter/Test/ControllerTestTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

declare(strict_types=1);

namespace CodeIgniter\Test;

trait ControllerTestTrait
{
}
9 changes: 9 additions & 0 deletions tests/Fixtures/Stubs/CodeIgniter/Test/DatabaseTestTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

declare(strict_types=1);

namespace CodeIgniter\Test;

trait DatabaseTestTrait
{
}
11 changes: 11 additions & 0 deletions tests/Rule/UsageRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
}
10 changes: 10 additions & 0 deletions tests/UsageClassificationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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];
Expand Down
18 changes: 18 additions & 0 deletions tests/UsageClassifierTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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([
Expand Down
Loading