Skip to content
Open
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
20 changes: 20 additions & 0 deletions .github/workflows/static-analysis-plugins.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,14 @@ jobs:
with:
working-directory: "qa/StaticAnalysis/phpstan/v2"

- uses: "ramsey/composer-install@v2"
with:
working-directory: "qa/StaticAnalysis/phpunit/v10"

- uses: "ramsey/composer-install@v2"
with:
working-directory: "qa/StaticAnalysis/phpunit/v11"

- name: Running Psalm v6 (without plugin installed)
run: cd qa/StaticAnalysis/psalm/v6 && php vendor/bin/psalm --config=../../../../tests/StaticAnalysis/psalm-without-plugin.xml --no-cache

Expand All @@ -70,3 +78,15 @@ jobs:

- name: Running PHPStan v1 (with extension installed)
run: cd qa/StaticAnalysis/phpstan/v1 && php vendor/bin/phpstan --configuration=../../../../tests/StaticAnalysis/PHPStan/v1/phpstan-with-extension.neon.dist

- name: Running PHPUnit v11 (without extension installed)
run: cd qa/StaticAnalysis/phpunit/v11 && php vendor/bin/phpunit --configuration=../../../../tests/StaticAnalysis/PHPUnit/phpunit-without-extension.dist.xml --bootstrap vendor/autoload.php

- name: Running PHPUnit v11 (with extension installed)
run: cd qa/StaticAnalysis/phpunit/v11 && php vendor/bin/phpunit --configuration=../../../../tests/StaticAnalysis/PHPUnit/phpunit-with-extension.dist.xml --bootstrap vendor/autoload.php

- name: Running PHPUnit v10 (without extension installed)
run: cd qa/StaticAnalysis/phpunit/v10 && php vendor/bin/phpunit --configuration=../../../../tests/StaticAnalysis/PHPUnit/phpunit-without-extension.dist.xml --bootstrap vendor/autoload.php

- name: Running PHPUnit v10 (with extension installed)
run: cd qa/StaticAnalysis/phpunit/v10 && php vendor/bin/phpunit --configuration=../../../../tests/StaticAnalysis/PHPUnit/phpunit-with-extension.dist.xml --bootstrap vendor/autoload.php
3 changes: 3 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@
"rector/rector": "^2.0",
"phpbench/phpbench": "^1.3"
},
"suggest": {
"symfony/console": "To use the PHPUnit extension for formatting mapping errors"
},
"autoload": {
"psr-4": {
"CuyZ\\Valinor\\": "src"
Expand Down
1 change: 1 addition & 0 deletions docs/mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ nav:
- Performance & caching: other/performance-and-caching.md
- App & framework integration: other/app-and-framework-integration.md
- Static analysis — PHPStan/Psalm: other/static-analysis.md
- PHPUnit extension: other/phpunit-extension.md
- Project:
- Upgrading: project/upgrading.md
- Alternatives: project/alternatives.md
Expand Down
68 changes: 68 additions & 0 deletions docs/pages/other/phpunit-extension.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# PHPUnit extension

To help debug mapping errors thrown during your test suite, an extension for
[PHPUnit] is provided. It will collect any uncaught instances of `MappingError`
and display them per test in a formatted table at the end of the test run.

If installed correctly and a mapping error is thrown, PHPUnit will show a table
after execution for each test case with errors:

```
The following Valinor mapping errors were thrown:

+---------- CuyZ\Valinor\QA\StaticAnalysis\phpunit\MappingTest -----------+
| Method | Path | Error |
+-----------------------------+------+------------------------------------+
| testMappingWhichThrowsError | foo | Value null is not a valid string. |
| testMappingWhichThrowsError | bar | Value null is not a valid boolean. |
+-----------------------------+------+------------------------------------+
```

**Activating**

To activate this feature, the extension must be registered with PHPUnit:

```xml title="phpunit.dist.xml"
<extensions>
<bootstrap class="CuyZ\Valinor\QA\PHPUnit\PrettyPrintMappingErrorsExtension"/>
</extensions>
```

Additionally, every test case where `MappingError` may be thrown also needs to
have a trait added to it:

```php title="MyTestCase.php"
<?php

use CuyZ\Valinor\QA\PHPUnit\CollectValinorMappingErrors;
use PHPUnit\Framework\TestCase;

final class MappingTest extends TestCase
{
use CollectValinorMappingErrors;

// ...
}
```

This trait overrides the `TestCase::transformException` method, so if you have
overridden this method already you will have to call the trait method yourself.

```php title="MyTestCase.php"
<?php

use CuyZ\Valinor\QA\PHPUnit\CollectValinorMappingErrors;
use PHPUnit\Framework\TestCase;
use Throwable;

final class MappingTest extends TestCase
{
use CollectValinorMappingErrors { transformException as protected collectValinorMappingErrors; }

protected function transformException(Throwable $t): Throwable
{
// ...

return $this->collectValinorMappingErrors($t);
}
```
30 changes: 30 additions & 0 deletions qa/PHPUnit/CollectValinorMappingErrors.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

declare(strict_types=1);

namespace CuyZ\Valinor\QA\PHPUnit;

use CuyZ\Valinor\Mapper\MappingError;
use Throwable;

/**
* @require-extends \PHPUnit\Framework\TestCase
*/
trait CollectValinorMappingErrors
{
protected function transformException(Throwable $t): Throwable
{
$originalThrowable = $t;

while ($t !== null && !$t instanceof MappingError) {
$t = $t->getPrevious();
}

if ($t instanceof MappingError) {
MappingErrorsCollector::getInstance()
->publish(static::class, $this->name(), $t->messages());
}

return $originalThrowable;
}
}
59 changes: 59 additions & 0 deletions qa/PHPUnit/MappingErrorsCollector.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php

declare(strict_types=1);

namespace CuyZ\Valinor\QA\PHPUnit;

use CuyZ\Valinor\Mapper\Tree\Message\Messages;

use function array_key_exists;
use function count;

final class MappingErrorsCollector
{
private static MappingErrorsCollector | null $instance = null;

/**
* @var array<class-string, array<string, Messages>>
*/
private array $mappingErrorsPerTest = [];

public static function getInstance(): MappingErrorsCollector
{
if (self::$instance === null) {
self::$instance = new MappingErrorsCollector();
}

return self::$instance;
}

/**
* @param class-string $testClass
*/
public function publish(string $testClass, string $method, Messages $messages): void
{
if (!array_key_exists($testClass, $this->mappingErrorsPerTest)) {
$this->mappingErrorsPerTest[$testClass] = [];
}

$this->mappingErrorsPerTest[$testClass][$method] = $messages;
}

public function clear(): void
{
$this->mappingErrorsPerTest = [];
}

public function hasErrors(): bool
{
return count($this->mappingErrorsPerTest) > 0;
}

/**
* @return array<class-string, array<string, Messages>>
*/
public function getMappingErrorsPerClass(): array
{
return $this->mappingErrorsPerTest;
}
}
71 changes: 71 additions & 0 deletions qa/PHPUnit/PrettyPrintMappingErrorsExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?php

declare(strict_types=1);

namespace CuyZ\Valinor\QA\PHPUnit;

use PHPUnit\Event\Application\Finished;
use PHPUnit\Event\Application\FinishedSubscriber;
use PHPUnit\Runner\Extension\Extension;
use PHPUnit\Runner\Extension\Facade;
use PHPUnit\Runner\Extension\ParameterCollection;
use PHPUnit\TextUI\Configuration\Configuration;
use RuntimeException;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Output\ConsoleOutput;
use Symfony\Component\Console\Output\OutputInterface;

use function class_exists;

final readonly class PrettyPrintMappingErrorsExtension implements Extension
{
public function __construct()
{
if (!class_exists(ConsoleOutput::class)) {
throw new RuntimeException('In order to use the Valinor PHPUnit extension you should install the symfony/console package.');
}
}

public function bootstrap(Configuration $configuration, Facade $facade, ParameterCollection $parameters): void
{
$facade->registerSubscriber(
new class () implements FinishedSubscriber {
private OutputInterface $output;

public function __construct()
{
$this->output = new ConsoleOutput();
}

public function notify(Finished $event): void
{
if (!MappingErrorsCollector::getInstance()->hasErrors()) {
return;
}

$this->output->writeln('');
$this->output->writeln('<comment>The following Valinor mapping errors were thrown:</>');
$this->output->writeln('');

foreach (MappingErrorsCollector::getInstance()->getMappingErrorsPerClass() as $testClass => $mappingErrorsPerMethod) {
$table = (new Table($this->output))
->setHeaderTitle($testClass)
->setHeaders(['Method', 'Path', 'Error']);

foreach ($mappingErrorsPerMethod as $methodName => $messages) {
foreach ($messages->toArray() as $message) {
$table->addRow([
$methodName,
$message->path(),
$message->toString(),
]);
}
}

$table->render();
}
}
}
);
}
}
14 changes: 14 additions & 0 deletions qa/StaticAnalysis/phpunit/v10/composer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"require-dev": {
"phpunit/phpunit": "^10",
"symfony/console": "^7"
},
"autoload": {
"psr-4": {
"CuyZ\\Valinor\\": "../../../../src",
"CuyZ\\Valinor\\QA\\PHPUnit\\": "../../../PHPUnit",
"CuyZ\\Valinor\\Tests\\": "../../../../tests",
"Psr\\SimpleCache\\": "../../../../vendor/psr/simple-cache/src"
}
}
}
Loading