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
65 changes: 65 additions & 0 deletions src/Event/FailOnTargetViolationSubscriber.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?php

declare(strict_types=1);

namespace Boundwize\Pyrameter\Event;

use Boundwize\Pyrameter\Analysis\TestCollector;
use Boundwize\Pyrameter\Target\TargetEvaluator;
use Closure;
use PHPUnit\Event\Application\Finished;
use PHPUnit\Event\Application\FinishedSubscriber;

use const PHP_EOL;

final readonly class FailOnTargetViolationSubscriber implements FinishedSubscriber
{
private TargetEvaluator $targetEvaluator;

/**
* @param array<string, array{min: float, max: float}> $targets
* @param null|Closure(int): void $exit
*/
public function __construct(
private TestCollector $testCollector,
array $targets,
private bool $failOnViolation = false,
private ?Closure $exit = null,
) {
$this->targetEvaluator = new TargetEvaluator($targets);
}

public function notify(Finished $event): void
{
if (! $this->failOnViolation) {
return;
}

$targetEvaluation = $this->targetEvaluator->evaluate($this->testCollector->summary());

if ($targetEvaluation->allPassed()) {
return;
}

echo PHP_EOL . 'Pyrameter target shape violated.' . PHP_EOL;

if ($event->shellExitCode() !== 0) {
return;
}

$this->exit(1);
}

private function exit(int $status): void
{
if ($this->exit instanceof Closure) {
($this->exit)($status);

return;
}

// @codeCoverageIgnoreStart
exit($status);
// @codeCoverageIgnoreEnd
}
}
25 changes: 0 additions & 25 deletions src/Event/PrintReportSubscriber.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,9 @@
use Boundwize\Pyrameter\Report\PyramidReporter;
use Boundwize\Pyrameter\Report\SuiteShapeResolver;
use Boundwize\Pyrameter\Target\TargetEvaluator;
use Closure;
use PHPUnit\Event\TestRunner\ExecutionFinished;
use PHPUnit\Event\TestRunner\ExecutionFinishedSubscriber;

use const PHP_EOL;

final readonly class PrintReportSubscriber implements ExecutionFinishedSubscriber
{
private TargetEvaluator $targetEvaluator;
Expand All @@ -22,14 +19,11 @@

/**
* @param array<string, array{min: float, max: float}> $targets
* @param null|Closure(int): void $exit
*/
public function __construct(
private TestCollector $testCollector,
array $targets,
private PyramidReporter $pyramidReporter,
private bool $failOnViolation = false,
private ?Closure $exit = null,
) {
$this->targetEvaluator = new TargetEvaluator($targets);
$this->suiteShapeResolver = new SuiteShapeResolver();
Expand All @@ -42,24 +36,5 @@ public function notify(ExecutionFinished $event): void
$suiteShape = $this->suiteShapeResolver->resolve($pyramidSummary, $targetEvaluation);

$this->pyramidReporter->print($pyramidSummary, $targetEvaluation, $suiteShape);

if ($this->failOnViolation && ! $targetEvaluation->allPassed()) {
echo PHP_EOL . 'Pyrameter target shape violated.' . PHP_EOL;

$this->exit(1);
}
}

private function exit(int $status): void
{
if ($this->exit instanceof Closure) {
($this->exit)($status);

return;
}

// @codeCoverageIgnoreStart
exit($status);
// @codeCoverageIgnoreEnd
}
}
6 changes: 6 additions & 0 deletions src/Extension.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use Boundwize\Pyrameter\Config\PyrameterConfigLoader;
use Boundwize\Pyrameter\Detection\TestUsageScanner;
use Boundwize\Pyrameter\Event\CollectTestResultSubscriber;
use Boundwize\Pyrameter\Event\FailOnTargetViolationSubscriber;
use Boundwize\Pyrameter\Event\PrintReportSubscriber;
use Boundwize\Pyrameter\Report\PyramidReporter;
use PHPUnit\Runner\Extension\Extension as PHPUnitExtension;
Expand Down Expand Up @@ -40,6 +41,11 @@ public function bootstrap(
testCollector: $testCollector,
targets: $pyrameterConfig->targetPercentages(),
pyramidReporter: new PyramidReporter(),
));

$facade->registerSubscriber(new FailOnTargetViolationSubscriber(
testCollector: $testCollector,
targets: $pyrameterConfig->targetPercentages(),
failOnViolation: $pyrameterConfig->shouldFailOnViolation(),
));
}
Expand Down
114 changes: 114 additions & 0 deletions tests/Event/FailOnTargetViolationSubscriberTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
<?php

declare(strict_types=1);

namespace Boundwize\Pyrameter\Tests\Event;

use Boundwize\Pyrameter\Analysis\TestCollector;
use Boundwize\Pyrameter\Config\PyrameterConfig;
use Boundwize\Pyrameter\Event\FailOnTargetViolationSubscriber;
use Boundwize\Pyrameter\TestKind;
use Boundwize\Pyrameter\Tests\Fixtures\TelemetryInfoFactory;
use Boundwize\Pyrameter\ValueObject\TestRecord;
use PHPUnit\Event\Application\Finished;
use PHPUnit\Event\Telemetry\Info;
use PHPUnit\Framework\TestCase;

final class FailOnTargetViolationSubscriberTest extends TestCase
{
public function testItDoesNothingWhenFailOnViolationIsDisabled(): void
{
$testCollector = new TestCollector();
$testCollector->add(new TestRecord(self::class, 'testUnit', [], TestKind::Unit));
$testCollector->add(new TestRecord(self::class, 'testIntegration', [], TestKind::Integration));

$exitStatus = null;

$failOnTargetViolationSubscriber = new FailOnTargetViolationSubscriber(
testCollector: $testCollector,
targets: PyrameterConfig::defaults()->targetPercentages(),
failOnViolation: false,
exit: static function (int $status) use (&$exitStatus): void {
$exitStatus = $status;
},
);

$failOnTargetViolationSubscriber->notify(new Finished($this->telemetryInfo(), 0));

$this->assertNull($exitStatus);
}

public function testItDoesNothingWhenTargetsPass(): void
{
$testCollector = new TestCollector();
$testCollector->add(new TestRecord(self::class, 'testUnit', [], TestKind::Unit));

$exitStatus = null;

$failOnTargetViolationSubscriber = new FailOnTargetViolationSubscriber(
testCollector: $testCollector,
targets: PyrameterConfig::defaults()->targetPercentages(),
failOnViolation: true,
exit: static function (int $status) use (&$exitStatus): void {
$exitStatus = $status;
},
);

$failOnTargetViolationSubscriber->notify(new Finished($this->telemetryInfo(), 0));

$this->assertNull($exitStatus);
}

public function testItTerminatesWithFailureWhenTargetsAreViolatedAndPhpunitWouldPass(): void
{
$testCollector = new TestCollector();
$testCollector->add(new TestRecord(self::class, 'testUnit', [], TestKind::Unit));
$testCollector->add(new TestRecord(self::class, 'testIntegration', [], TestKind::Integration));

$exitStatus = null;

$failOnTargetViolationSubscriber = new FailOnTargetViolationSubscriber(
testCollector: $testCollector,
targets: PyrameterConfig::defaults()->targetPercentages(),
failOnViolation: true,
exit: static function (int $status) use (&$exitStatus): void {
$exitStatus = $status;
},
);

$this->expectOutputRegex('/Pyrameter target shape violated\./');

$failOnTargetViolationSubscriber->notify(new Finished($this->telemetryInfo(), 0));

$this->assertSame(1, $exitStatus);
}

public function testItDoesNotOverrideExistingPhpunitFailureExitCode(): void
{
$testCollector = new TestCollector();
$testCollector->add(new TestRecord(self::class, 'testUnit', [], TestKind::Unit));
$testCollector->add(new TestRecord(self::class, 'testIntegration', [], TestKind::Integration));

$exitStatus = null;

$failOnTargetViolationSubscriber = new FailOnTargetViolationSubscriber(
testCollector: $testCollector,
targets: PyrameterConfig::defaults()->targetPercentages(),
failOnViolation: true,
exit: static function (int $status) use (&$exitStatus): void {
$exitStatus = $status;
},
);

$this->expectOutputRegex('/Pyrameter target shape violated\./');

$failOnTargetViolationSubscriber->notify(new Finished($this->telemetryInfo(), 1));

$this->assertNull($exitStatus);
}

private function telemetryInfo(): Info
{
return TelemetryInfoFactory::create();
}
}
12 changes: 2 additions & 10 deletions tests/Event/PrintReportSubscriberTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,29 +33,21 @@ public function testItPrintsTheReportWhenExecutionFinishes(): void
$printReportSubscriber->notify(new ExecutionFinished($this->telemetryInfo()));
}

public function testItTerminatesWithFailureWhenTargetsAreViolatedAndFailOnViolationIsEnabled(): void
public function testItPrintsViolationInReportWhenTargetsAreViolated(): void
{
$testCollector = new TestCollector();
$testCollector->add(new TestRecord(self::class, 'testUnit', [], TestKind::Unit));
$testCollector->add(new TestRecord(self::class, 'testIntegration', [], TestKind::Integration));

$exitStatus = null;

$printReportSubscriber = new PrintReportSubscriber(
testCollector: $testCollector,
targets: PyrameterConfig::defaults()->targetPercentages(),
pyramidReporter: new PyramidReporter(),
failOnViolation: true,
exit: static function (int $status) use (&$exitStatus): void {
$exitStatus = $status;
},
);

$this->expectOutputRegex('/Pyrameter.*Pyrameter target shape violated\./s');
$this->expectOutputRegex('/Pyrameter.*Result: Violated/s');

$printReportSubscriber->notify(new ExecutionFinished($this->telemetryInfo()));

$this->assertSame(1, $exitStatus);
}

private function telemetryInfo(): Info
Expand Down
15 changes: 15 additions & 0 deletions tests/ExtensionSmokeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,21 @@ public function testFailOnViolationChangesPhpunitExitCodeWithoutSubscriberWarnin
$this->assertStringNotContainsString('Exception in third-party event subscriber', $output);
}

public function testFailOnViolationDoesNotHidePhpunitFailureDetails(): void
{
$configuration = __DIR__ . '/Fixtures/SmokeProject/phpunit-failing-test.xml';
[$exitCode, $output] = $this->runPhpUnit($configuration);

$this->assertSame(1, $exitCode, $output);
$this->assertStringContainsString('Pyrameter', $output);
$this->assertStringContainsString('Result: Violated', $output);
$this->assertStringContainsString('Pyrameter target shape violated.', $output);
$this->assertStringContainsString('There was 1 failure', $output);
$this->assertStringContainsString('FailingPdoSmokeFixture::testFails', $output);
$this->assertStringContainsString('Failed asserting', $output);
$this->assertStringNotContainsString('Exception in third-party event subscriber', $output);
}

/**
* @return array{int, string}
*/
Expand Down
16 changes: 16 additions & 0 deletions tests/Fixtures/SmokeProject/FailingPdoSmokeFixture.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

declare(strict_types=1);

namespace Boundwize\Pyrameter\Tests\Fixtures\SmokeProject;

use PDO;
use PHPUnit\Framework\TestCase;

final class FailingPdoSmokeFixture extends TestCase
{
public function testFails(): void
{
self::assertSame('expected', PDO::class);
}
}
15 changes: 15 additions & 0 deletions tests/Fixtures/SmokeProject/phpunit-failing-test.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="../../../vendor/autoload.php"
colors="false"
cacheDirectory="../../../.phpunit-smoke-cache">
<extensions>
<bootstrap class="Boundwize\Pyrameter\Extension">
<parameter name="config" value="tests/Fixtures/SmokeProject/pyrameter-fail.php"/>
</bootstrap>
</extensions>
<testsuites>
<testsuite name="Smoke">
<file>FailingPdoSmokeFixture.php</file>
</testsuite>
</testsuites>
</phpunit>
Loading