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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ return PyrameterConfig::create()

With `create()`, only the rules you add are used for heavier classifications; all other executed tests stay `unit`.

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. Function matching is case-insensitive and accepts names with or without a leading backslash.
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.

To load a config file from another path, pass the `config` parameter to the PHPUnit extension:

Expand Down
3 changes: 2 additions & 1 deletion src/Config/PyrameterConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace Boundwize\Pyrameter\Config;

use Boundwize\Pyrameter\Rule\UsageRule;
use Boundwize\Pyrameter\Rule\UsageType;
use Boundwize\Pyrameter\TestKind;
use InvalidArgumentException;
use mysqli;
Expand Down Expand Up @@ -141,7 +142,7 @@ public function usesNamespace(string $namespace, TestKind $testKind): self

public function usesFunction(string $functionName, TestKind $testKind): self
{
$this->usageRules[] = new UsageRule(ltrim($functionName, '\\'), $testKind, caseInsensitive: true);
$this->usageRules[] = new UsageRule(ltrim($functionName, '\\'), $testKind, UsageType::Function);

return $this;
}
Expand Down
11 changes: 9 additions & 2 deletions src/Detection/ConsumedUsageVisitor.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Boundwize\Pyrameter\Detection;

use Boundwize\Pyrameter\Rule\UsageType;
use PhpParser\Node;
use PhpParser\Node\Arg;
use PhpParser\Node\Attribute;
Expand Down Expand Up @@ -32,6 +33,7 @@
use function array_keys;
use function in_array;
use function ltrim;
use function sprintf;
use function strtolower;

final class ConsumedUsageVisitor extends NodeVisitorAbstract
Expand Down Expand Up @@ -180,7 +182,7 @@ private function addFunctionName(Name $name): void
return;
}

$this->consumedUsages[$functionName] = true;
$this->consumedUsages[$this->usageKey(UsageType::Function, $functionName)] = true;
}

private function addUsage(string $usage): void
Expand All @@ -191,7 +193,12 @@ private function addUsage(string $usage): void
return;
}

$this->consumedUsages[$usage] = true;
$this->consumedUsages[$this->usageKey(UsageType::ClassLike, $usage)] = true;
}

private function usageKey(UsageType $usageType, string $usage): string
{
return sprintf('%s:%s', $usageType->value, $usage);
}

private function isClassConstant(ClassConstFetch $classConstFetch): bool
Expand Down
10 changes: 5 additions & 5 deletions src/Detection/ScanResult.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,21 @@
final readonly class ScanResult
{
/**
* @param list<string> $consumedClasses Consumed class, namespace, and function usages.
* @param list<string> $consumedUsages Consumed class-like and function usages.
*/
private function __construct(
public bool $inspectable,
public array $consumedClasses,
public array $consumedUsages,
public ?string $errorMessage = null,
) {
}

/**
* @param list<string> $consumedClasses
* @param list<string> $consumedUsages
*/
public static function inspectable(array $consumedClasses): self
public static function inspectable(array $consumedUsages): self
{
return new self(true, $consumedClasses);
return new self(true, $consumedUsages);
}

public static function uninspectable(string $errorMessage): self
Expand Down
4 changes: 2 additions & 2 deletions src/Event/CollectTestResultSubscriber.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,13 @@ public function notify(Finished $event): void

$kind = $this->kindByTestClassName[$testClassName] ??=
$scanResult->inspectable
? $this->usageClassifier->classify($scanResult->consumedClasses)
? $this->usageClassifier->classify($scanResult->consumedUsages)
: TestKind::Unit;

$this->testCollector->add(new TestRecord(
testClassName: $testClassName,
testMethodName: $testMethodName,
consumedClasses: $scanResult->consumedClasses,
consumedUsages: $scanResult->consumedUsages,
kind: $kind,
));
}
Expand Down
69 changes: 58 additions & 11 deletions src/Rule/UsageRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,41 +7,88 @@
use Boundwize\Pyrameter\TestKind;

use function ltrim;
use function sprintf;
use function str_contains;
use function str_ends_with;
use function str_starts_with;
use function strlen;
use function strtolower;
use function substr;

final readonly class UsageRule
{
public function __construct(
public string $classOrNamespace,
public string $usage,
public TestKind $kind,
private bool $caseInsensitive = false,
private UsageType $usageType = UsageType::ClassLike,
) {
}

public function matches(string $consumedUsage): bool
{
$configuredUsage = $this->normalize($this->classOrNamespace);
$consumedUsage = $this->normalize($consumedUsage);
$configuredUsage = $this->normalizedUsage();
[$consumedUsageType, $consumedUsage] = $this->normalizeConsumedUsage($consumedUsage);

if ($consumedUsage === $configuredUsage) {
return true;
if ($consumedUsageType !== $this->usageType) {
return false;
}

if (! str_ends_with($configuredUsage, '\\')) {
return false;
return $consumedUsage === $configuredUsage;
}

return str_starts_with($consumedUsage, $configuredUsage);
return $this->matchesNamespacePrefix($consumedUsage, $configuredUsage);
}

public function normalizedUsage(): string
{
return $this->normalize($this->usage);
}

public function isNamespaceRule(): bool
{
return $this->usageType === UsageType::ClassLike && str_ends_with($this->normalizedUsage(), '\\');
}

public function normalizedKey(): string
{
return $this->usageKey($this->usageType, $this->normalizedUsage());
}

private function normalize(string $usage): string
{
if (! $this->caseInsensitive) {
return $usage;
return strtolower(ltrim($usage, '\\'));
}

private function matchesNamespacePrefix(string $consumedUsage, string $namespaceUsage): bool
{
return str_ends_with($namespaceUsage, '\\')
&& strlen($consumedUsage) > strlen($namespaceUsage)
&& str_starts_with($consumedUsage, $namespaceUsage);
}

/**
* @return array{UsageType, string}
*/
private function normalizeConsumedUsage(string $consumedUsage): array
{
if (! str_contains($consumedUsage, ':')) {
return [$this->usageType, $this->normalize($consumedUsage)];
}

return strtolower(ltrim($usage, '\\'));
foreach (UsageType::cases() as $usageType) {
$prefix = $usageType->value . ':';

if (str_starts_with($consumedUsage, $prefix)) {
return [$usageType, $this->normalize(substr($consumedUsage, strlen($prefix)))];
}
}

return [$this->usageType, $this->normalize($consumedUsage)];
}

private function usageKey(UsageType $usageType, string $normalizedUsage): string
{
return sprintf('%s:%s', $usageType->value, $normalizedUsage);
}
}
11 changes: 11 additions & 0 deletions src/Rule/UsageType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

declare(strict_types=1);

namespace Boundwize\Pyrameter\Rule;

enum UsageType: string
{
case ClassLike = 'class';
case Function = 'function';
}
4 changes: 2 additions & 2 deletions src/TestRecord.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@
final readonly class TestRecord
{
/**
* @param list<string> $consumedClasses Consumed class, namespace, and function usages.
* @param list<string> $consumedUsages Consumed class-like and function usages.
*/
public function __construct(
public string $testClassName,
public string $testMethodName,
public array $consumedClasses,
public array $consumedUsages,
public TestKind $kind,
) {
}
Expand Down
119 changes: 106 additions & 13 deletions src/UsageClassifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,40 +5,133 @@
namespace Boundwize\Pyrameter;

use Boundwize\Pyrameter\Rule\UsageRule;
use Boundwize\Pyrameter\Rule\UsageType;

use function ltrim;
use function sprintf;
use function str_contains;
use function str_ends_with;
use function str_starts_with;
use function strlen;
use function strtolower;
use function substr;

final readonly class UsageClassifier
{
/** @var array<string, TestKind> */
private array $exactRules;

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

/**
* @param list<UsageRule> $rules
*/
public function __construct(
private array $rules,
) {
public function __construct(array $rules)
{
$exactRules = [];
$namespaceRules = [];

foreach ($rules as $rule) {
if ($rule->isNamespaceRule()) {
$namespaceRules[] = [
'usage' => $rule->normalizedKey(),
'kind' => $rule->kind,
];

continue;
}

$this->addExactRule($exactRules, $rule->normalizedKey(), $rule->kind);
}

$this->exactRules = $exactRules;
$this->namespaceRules = $namespaceRules;
}

/**
* @param list<string> $consumedClasses
* @param list<string> $consumedUsages
*/
public function classify(array $consumedClasses): TestKind
public function classify(array $consumedUsages): TestKind
{
$kind = TestKind::Unit;

foreach ($consumedClasses as $consumedClass) {
foreach ($this->rules as $rule) {
if (! $rule->matches($consumedClass)) {
foreach ($consumedUsages as $consumedUsage) {
$normalizedConsumedUsage = $this->normalizeConsumedUsage($consumedUsage);

if (isset($this->exactRules[$normalizedConsumedUsage])) {
$kind = $this->heaviest($kind, $this->exactRules[$normalizedConsumedUsage]);

if ($kind === TestKind::E2E) {
return $kind;
}
}

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

if ($rule->kind->weight() > $kind->weight()) {
$kind = $rule->kind;
$kind = $this->heaviest($kind, $namespaceRule['kind']);

if ($kind === TestKind::E2E) {
return $kind;
}
if ($kind === TestKind::E2E) {
return $kind;
}
}
}

return $kind;
}

/**
* @param array<string, TestKind> $rules
*/
private function addExactRule(array &$rules, string $usage, TestKind $testKind): void
{
$rules[$usage] = isset($rules[$usage])
? $this->heaviest($rules[$usage], $testKind)
: $testKind;
}

private function heaviest(TestKind $left, TestKind $right): TestKind
{
return $right->weight() > $left->weight() ? $right : $left;
}

private function matchesNamespaceRule(string $consumedUsage, string $namespaceUsage): bool
{
return str_ends_with($namespaceUsage, '\\')
&& strlen($consumedUsage) > strlen($namespaceUsage)
&& str_starts_with($consumedUsage, $namespaceUsage);
}

private function normalizeConsumedUsage(string $consumedUsage): string
{
if (! str_contains($consumedUsage, ':')) {
return $this->usageKey(UsageType::ClassLike, $this->normalizeUsageName($consumedUsage));
}

foreach (UsageType::cases() as $usageType) {
$prefix = $usageType->value . ':';

if (str_starts_with($consumedUsage, $prefix)) {
return $this->usageKey(
$usageType,
$this->normalizeUsageName(substr($consumedUsage, strlen($prefix)))
);
}
}

return $this->usageKey(UsageType::ClassLike, $this->normalizeUsageName($consumedUsage));
}

private function normalizeUsageName(string $usageName): string
{
return strtolower(ltrim($usageName, '\\'));
}

private function usageKey(UsageType $usageType, string $normalizedUsage): string
{
return sprintf('%s:%s', $usageType->value, $normalizedUsage);
}
}
Loading
Loading