Skip to content

Commit

Permalink
Add the ability to define multiple Rules in the same test function
Browse files Browse the repository at this point in the history
  • Loading branch information
AnnaDamm committed Jun 22, 2024
1 parent 0cd000c commit ce5818b
Show file tree
Hide file tree
Showing 9 changed files with 162 additions and 20 deletions.
6 changes: 6 additions & 0 deletions ci/psalm.xml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@
<UndefinedClass>
<errorLevel type="suppress"><file name="../src/Test/TestParser.php" /></errorLevel>
</UndefinedClass>
<InvalidReturnType>
<errorLevel type="suppress"><file name="../src/Test/TestExtractor.php" /></errorLevel>
</InvalidReturnType>
<InvalidReturnStatement>
<errorLevel type="suppress"><file name="../src/Test/TestExtractor.php" /></errorLevel>
</InvalidReturnStatement>

<PropertyNotSetInConstructor>
<errorLevel type="suppress"><directory name="../tests/unit/rules/" /></errorLevel>
Expand Down
32 changes: 32 additions & 0 deletions docs/documentation/rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,35 @@ final class UserDomainTest extends AbstractDomainTest
```

Note that you would only need to register the `UserDomainTest` class as a PHPat test in the PHPStan config file.

## Dynamic Rule Sets
It is possible to dynamically create rules by returning an iterable of Rules from your method:

```php
namespace App\Tests\Architecture;

use PHPat\Selector\Selector;
use PHPat\Test\Builder\Rule;
use PHPat\Test\PHPat;

final class ConfigurationTest
{
private const DOMAINS = [
'App\Domain1',
'App\Domain2',
];

/**
* @return iterable<string, Rule>
*/
public function test_domain_independence(): iterable
{
foreach(self::DOMAINS as $domain) {
yield $domain => PHPat::rule()
->classes(Selector::inNamespace($domain))
->canOnlyDependOn()
->classes(Selector::inNamespace($domain));
}
}
}
```
5 changes: 1 addition & 4 deletions src/Test/RuleValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,8 @@

use PHPat\Rule\Assertion\Relation\RelationAssertion;

final class RuleValidator
final class RuleValidator implements RuleValidatorInterface
{
/**
* @throws \Exception
*/
public function validate(Rule $rule): void
{
if ($rule->getSubjects() === []) {
Expand Down
11 changes: 11 additions & 0 deletions src/Test/RuleValidatorInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php declare(strict_types=1);

namespace PHPat\Test;

interface RuleValidatorInterface
{
/**
* @throws \Exception
*/
public function validate(Rule $rule): void;
}
13 changes: 5 additions & 8 deletions src/Test/TestExtractor.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@

use PHPat\ShouldNotHappenException;
use PHPStan\DependencyInjection\Container;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\ReflectionProvider;

final class TestExtractor
final class TestExtractor implements TestExtractorInterface
{
private const TEST_TAG = 'phpat.test';

Expand All @@ -20,9 +19,6 @@ public function __construct(Container $container, ReflectionProvider $reflection
$this->reflectionProvider = $reflectionProvider;
}

/**
* @return iterable<ClassReflection>
*/
public function __invoke(): iterable
{
foreach ($this->container->getServicesByTag(self::TEST_TAG) as $test) {
Expand All @@ -38,14 +34,15 @@ public function __invoke(): iterable
}

/**
* @param class-string $test
* @param class-string $test
* @return null|\ReflectionClass<object>
*/
private function reflectTest(string $test): ?ClassReflection
private function reflectTest(string $test): ?\ReflectionClass
{
if (!$this->reflectionProvider->hasClass($test)) {
return null;
}

return $this->reflectionProvider->getClass($test);
return $this->reflectionProvider->getClass($test)->getNativeReflection();
}
}
11 changes: 11 additions & 0 deletions src/Test/TestExtractorInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php declare(strict_types=1);

namespace PHPat\Test;

interface TestExtractorInterface
{
/**
* @return iterable<\ReflectionClass<object>>
*/
public function __invoke(): iterable;
}
20 changes: 12 additions & 8 deletions src/Test/TestParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ class TestParser
{
/** @var array<Rule> */
private static array $result = [];
private TestExtractor $extractor;
private RuleValidator $ruleValidator;
private TestExtractorInterface $extractor;
private RuleValidatorInterface $ruleValidator;

public function __construct(TestExtractor $extractor, RuleValidator $ruleValidator)
public function __construct(TestExtractorInterface $extractor, RuleValidatorInterface $ruleValidator)
{
$this->extractor = $extractor;
$this->ruleValidator = $ruleValidator;
Expand All @@ -38,18 +38,22 @@ private function parse(): array
$tests = ($this->extractor)();

$rules = [];
foreach ($tests as $test) {
$methods = [];
$reflected = $test->getNativeReflection();
foreach ($tests as $reflected) {
$classname = $reflected->getName();
$object = $reflected->newInstanceWithoutConstructor();
foreach ($reflected->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) {
if (
!empty($method->getAttributes(TestRule::class))
method_exists($method, 'getAttributes') && !empty($method->getAttributes(TestRule::class))
|| preg_match('/^(test)[A-Za-z0-9_\x80-\xff]*/', $method->getName())
) {
$ruleBuilder = $object->{$method->getName()}();
$rules[$classname.':'.$method->getName()] = $ruleBuilder;
if (is_iterable($ruleBuilder)) {
foreach ($ruleBuilder as $name => $rule) {
$rules[$classname.':'.$method->getName().':'.$name] = $rule;
}
} else {
$rules[$classname.':'.$method->getName()] = $ruleBuilder;
}
}
}
}
Expand Down
52 changes: 52 additions & 0 deletions tests/integration/test/TestParser/ParsesTestsTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php declare(strict_types=1);

namespace Tests\PHPat\integration\test\TestParser;

use PHPat\Selector\Selector;
use PHPat\Test\PHPat;
use PHPat\Test\Rule;
use PHPat\Test\RuleValidatorInterface;
use PHPat\Test\TestExtractorInterface;
use PHPat\Test\TestParser;
use PHPUnit\Framework\TestCase;

/**
* @internal
* @coversNothing
*/
final class ParsesTestsTest extends TestCase
{
public function testClassCollectsMultipleRulesFromFunction(): void
{
$testParser = new TestParser(
new class() implements TestExtractorInterface {
public function __invoke(): iterable
{
yield new \ReflectionClass(TestClass::class);
}
},
new class() implements RuleValidatorInterface {
public function validate(Rule $rule): void {}
},
);

$rule1 = PHPat::rule()->classes(Selector::classname('1'))();
$rule1->ruleName = TestClass::class.':test_rules_from_iterator:one';

$rule2 = PHPat::rule()->classes(Selector::classname('2'))();
$rule2->ruleName = TestClass::class.':test_rules_from_iterator:two';

$rule3 = PHPat::rule()->classes(Selector::classname('3'))();
$rule3->ruleName = TestClass::class.':test_rule';

$rule4 = PHPat::rule()->classes(Selector::classname('4'))();
$rule4->ruleName = TestClass::class.':test_rule_from_attribute';

self::assertEquals([
$rule1,
$rule2,
$rule3,
$rule4,
], ($testParser)());
}
}
32 changes: 32 additions & 0 deletions tests/integration/test/TestParser/TestClass.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php declare(strict_types=1);

namespace Tests\PHPat\integration\test\TestParser;

use PHPat\Selector\Selector;
use PHPat\Test\Attributes\TestRule;
use PHPat\Test\Builder\Rule;
use PHPat\Test\PHPat;

final class TestClass
{
/**
* @return iterable<Rule>
*/
public function test_rules_from_iterator(): iterable
{
yield 'one' => PHPat::rule()->classes(Selector::classname('1'));

yield 'two' => PHPat::rule()->classes(Selector::classname('2'));
}

public function test_rule(): Rule
{
return PHPat::rule()->classes(Selector::classname('3'));
}

#[TestRule]
public function test_rule_from_attribute(): Rule
{
return PHPat::rule()->classes(Selector::classname('4'));
}
}

0 comments on commit ce5818b

Please sign in to comment.