Skip to content

Commit

Permalink
Merge pull request #6 from robiningelbrecht/add-exit-on-low-coverage-…
Browse files Browse the repository at this point in the history
…per-rule

Add exit on low coverage per rule
  • Loading branch information
robiningelbrecht committed Sep 18, 2023
2 parents ede686d + 32a7f3e commit 0b372dd
Show file tree
Hide file tree
Showing 24 changed files with 394 additions and 433 deletions.
26 changes: 21 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,17 +67,33 @@ For example:
use RobinIngelbrecht\PHPUnitCoverageTools\MinCoverage\MinCoverageRules;

return [
MinCoverageRules::TOTAL => 20,
'RobinIngelbrecht\PHPUnitCoverageTools\*' => 80,
'RobinIngelbrecht\PHPUnitCoverageTools\Subscriber\Application\ApplicationFinishedSubscriber' => 100,
'RobinIngelbrecht\PHPUnitCoverageTools\*CommandHandler' => 100,
new MinCoverageRule(
pattern: MinCoverageRule::TOTAL,
minCoverage: 20,
exitOnLowCoverage: true
),
new MinCoverageRule(
pattern: 'RobinIngelbrecht\PHPUnitCoverageTools\*',
minCoverage: 80,
exitOnLowCoverage: false
),
new MinCoverageRule(
pattern: 'RobinIngelbrecht\PHPUnitCoverageTools\Subscriber\Application\ApplicationFinishedSubscriber',
minCoverage: 100,
exitOnLowCoverage: true
),
new MinCoverageRule(
pattern: 'RobinIngelbrecht\PHPUnitCoverageTools\*CommandHandler',
minCoverage: 100,
exitOnLowCoverage: true
),
];
```

This example will enforce:

- A minimum total coverage of *20%*
- A minimum coverage of *80%* for all classes in namespace `RobinIngelbrecht\PHPUnitCoverageTools`
- A minimum coverage of *80%* for all classes in namespace `RobinIngelbrecht\PHPUnitCoverageTools`, but will NOT `exit = 1` if it fails
- *100%* code coverage for the class `ApplicationFinishedSubscriber`
- *100%* code coverage for the classes ending with `CommandHandler`

Expand Down
306 changes: 0 additions & 306 deletions clover.xml

This file was deleted.

33 changes: 26 additions & 7 deletions src/ConsoleOutput.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,28 +45,47 @@ public function print(array $results, ResultStatus $finalStatus): void
$tableStyle = new TableStyle();
$tableStyle
->setHeaderTitleFormat('<fg=black;bg=yellow;options=bold> %s </>')
->setCellHeaderFormat('<bold>%s</bold>');
->setCellHeaderFormat('<bold>%s</bold>')
->setPadType(STR_PAD_BOTH);

$table = new Table($this->output);
$table
->setStyle($tableStyle)
->setHeaderTitle('Code coverage results')
->setHeaders(['Pattern', 'Expected', 'Actual', ''])
->setHeaders(['Pattern', 'Expected', 'Actual', '', 'Exit on fail?'])
->setColumnMaxWidth(1, 10)
->setColumnMaxWidth(2, 8)
->setColumnMaxWidth(4, 11)
->setRows([
...array_map(fn (MinCoverageResult $result) => [
$result->getPattern(),
new TableCell(
$result->getPattern(),
[
'style' => new TableCellStyle([
'align' => 'left',
]),
]
),
$result->getExpectedMinCoverage().'%',
sprintf('<%s>%s%%</%s>', $result->getStatus()->value, $result->getActualMinCoverage(), $result->getStatus()->value),
$result->getNumberOfTrackedLines() > 0 ?
sprintf('<bold>%s</bold> of %s lines covered', $result->getNumberOfCoveredLines(), $result->getNumberOfTrackedLines()) :
'No lines to track...?',
new TableCell(
$result->getNumberOfTrackedLines() > 0 ?
sprintf('<bold>%s</bold> of %s lines covered', $result->getNumberOfCoveredLines(), $result->getNumberOfTrackedLines()) :
'No lines to track...?',
[
'style' => new TableCellStyle([
'align' => 'left',
]),
]
),
$result->exitOnLowCoverage() ? 'Yes' : 'No',
], $results),
new TableSeparator(),
[
new TableCell(
$finalStatus->getMessage(),
[
'colspan' => 4,
'colspan' => 5,
'style' => new TableCellStyle([
'align' => 'center',
'cellFormat' => '<'.$finalStatus->value.'>%s</'.$finalStatus->value.'>',
Expand Down
4 changes: 2 additions & 2 deletions src/Exitter.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@

class Exitter
{
public function exit(int $code): void
public function exit(): void
{
exit($code);
exit(1);
}
}
48 changes: 30 additions & 18 deletions src/MinCoverage/MinCoverageResult.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ private function __construct(
private readonly float $actualMinCoverage,
private readonly int $numberOfTrackedLines,
private readonly int $numberOfCoveredLines,
private readonly bool $exitOnLowCoverage
) {
}

Expand Down Expand Up @@ -47,19 +48,26 @@ public function getNumberOfCoveredLines(): int
return $this->numberOfCoveredLines;
}

public function exitOnLowCoverage(): bool
{
return $this->exitOnLowCoverage;
}

public static function fromPatternAndNumbers(
string $pattern,
int $expectedMinCoverage,
float $actualMinCoverage,
int $numberOfTrackedLines,
int $numberOfCoveredLines,
bool $exitOnLowCoverage
): self {
return new self(
$pattern,
$expectedMinCoverage,
$actualMinCoverage,
$numberOfTrackedLines,
$numberOfCoveredLines,
pattern: $pattern,
expectedMinCoverage: $expectedMinCoverage,
actualMinCoverage: $actualMinCoverage,
numberOfTrackedLines: $numberOfTrackedLines,
numberOfCoveredLines: $numberOfCoveredLines,
exitOnLowCoverage: $exitOnLowCoverage
);
}

Expand All @@ -74,14 +82,17 @@ public static function mapFromRulesAndMetrics(
CoverageMetric $metricTotal = null,
): array {
$results = [];
foreach ($minCoverageRules->getRules() as $pattern => $minCoverage) {
if (MinCoverageRules::TOTAL === $pattern && $metricTotal) {
foreach ($minCoverageRules->getRules() as $minCoverageRule) {
$pattern = $minCoverageRule->getPattern();
$minCoverage = $minCoverageRule->getMinCoverage();
if (MinCoverageRule::TOTAL === $minCoverageRule->getPattern() && $metricTotal) {
$results[] = MinCoverageResult::fromPatternAndNumbers(
$pattern,
$minCoverage,
$metricTotal->getTotalPercentageCoverage(),
$metricTotal->getNumberOfTrackedLines(),
$metricTotal->getNumberOfCoveredLines()
pattern: $pattern,
expectedMinCoverage: $minCoverage,
actualMinCoverage: $metricTotal->getTotalPercentageCoverage(),
numberOfTrackedLines: $metricTotal->getNumberOfTrackedLines(),
numberOfCoveredLines: $metricTotal->getNumberOfCoveredLines(),
exitOnLowCoverage: $minCoverageRule->exitOnLowCoverage()
);
continue;
}
Expand All @@ -97,16 +108,17 @@ public static function mapFromRulesAndMetrics(
}

$results[] = MinCoverageResult::fromPatternAndNumbers(
$pattern,
$minCoverage,
round($coveragePercentage, 2),
$totalTrackedLines,
$totalCoveredLines
pattern: $pattern,
expectedMinCoverage: $minCoverage,
actualMinCoverage: round($coveragePercentage, 2),
numberOfTrackedLines: $totalTrackedLines,
numberOfCoveredLines: $totalCoveredLines,
exitOnLowCoverage: $minCoverageRule->exitOnLowCoverage()
);
}

uasort($results, function (MinCoverageResult $a, MinCoverageResult $b) {
if (MinCoverageRules::TOTAL === $a->getPattern()) {
if (MinCoverageRule::TOTAL === $a->getPattern()) {
return 1;
}
if ($a->getStatus() === $b->getStatus()) {
Expand Down
38 changes: 38 additions & 0 deletions src/MinCoverage/MinCoverageRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

namespace RobinIngelbrecht\PHPUnitCoverageTools\MinCoverage;

final class MinCoverageRule
{
public const TOTAL = 'Total';

public function __construct(
private readonly string $pattern,
private readonly int $minCoverage,
private readonly bool $exitOnLowCoverage
) {
if ($this->minCoverage < 0 || $this->minCoverage > 100) {
throw new \RuntimeException(sprintf('MinCoverage has to be value between 0 and 100. %s given', $this->minCoverage));
}
}

public function getPattern(): string
{
return $this->pattern;
}

public function getMinCoverage(): int
{
return $this->minCoverage;
}

public function exitOnLowCoverage(): bool
{
return $this->exitOnLowCoverage;
}

public function isTotalRule(): bool
{
return MinCoverageRule::TOTAL === $this->getPattern();
}
}
39 changes: 25 additions & 14 deletions src/MinCoverage/MinCoverageRules.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,17 @@

class MinCoverageRules
{
/** @deprecated Use MinCoverageRule::TOTAL */
public const TOTAL = 'Total';

private function __construct(
/** @var array<string, int> */
/** @var \RobinIngelbrecht\PHPUnitCoverageTools\MinCoverage\MinCoverageRule[] */
private readonly array $rules
) {
}

/**
* @return array<string, int>
* @return \RobinIngelbrecht\PHPUnitCoverageTools\MinCoverage\MinCoverageRule[]
*/
public function getRules(): array
{
Expand All @@ -24,28 +25,34 @@ public function getRules(): array

public function hasTotalRule(): bool
{
return array_key_exists(self::TOTAL, $this->rules);
foreach ($this->rules as $rule) {
if ($rule->isTotalRule()) {
return true;
}
}

return false;
}

public function hasOtherRulesThanTotalRule(): bool
{
foreach ($this->rules as $pattern => $minCoverage) {
if (self::TOTAL !== $pattern) {
foreach ($this->rules as $rule) {
if (!$rule->isTotalRule()) {
return true;
}
}

return false;
}

public static function fromInt(int $minCoverage): self
public static function fromInt(int $minCoverage, bool $exitOnLowCoverage): self
{
if ($minCoverage < 0 || $minCoverage > 100) {
throw new \RuntimeException(sprintf('MinCoverage has to be value between 0 and 100. %s given', $minCoverage));
}

return new self(
[self::TOTAL => $minCoverage],
[new MinCoverageRule(
pattern: MinCoverageRule::TOTAL,
minCoverage: $minCoverage,
exitOnLowCoverage: $exitOnLowCoverage
)],
);
}

Expand All @@ -60,11 +67,15 @@ public static function fromConfigFile(string $filePathToConfigFile): self
}

$rules = require $absolutePathToConfigFile;
foreach ($rules as $minCoverage) {
if ($minCoverage < 0 || $minCoverage > 100) {
throw new \RuntimeException(sprintf('MinCoverage has to be value between 0 and 100. %s given', $minCoverage));
foreach ($rules as $minCoverageRule) {
if (!$minCoverageRule instanceof MinCoverageRule) {
throw new \RuntimeException('Make sure all coverage rules are of instance '.MinCoverageRule::class);
}
}
$patterns = array_map(fn (MinCoverageRule $minCoverageRule) => $minCoverageRule->getPattern(), $rules);
if (count(array_unique($patterns)) !== count($patterns)) {
throw new \RuntimeException('Make sure all coverage rule patterns are unique');
}

return new self($rules);
}
Expand Down
19 changes: 13 additions & 6 deletions src/Subscriber/Application/ApplicationFinishedSubscriber.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use RobinIngelbrecht\PHPUnitCoverageTools\Exitter;
use RobinIngelbrecht\PHPUnitCoverageTools\MinCoverage\CoverageMetric;
use RobinIngelbrecht\PHPUnitCoverageTools\MinCoverage\MinCoverageResult;
use RobinIngelbrecht\PHPUnitCoverageTools\MinCoverage\MinCoverageRule;
use RobinIngelbrecht\PHPUnitCoverageTools\MinCoverage\MinCoverageRules;
use RobinIngelbrecht\PHPUnitCoverageTools\MinCoverage\ResultStatus;
use Symfony\Component\Console\Helper\FormatterHelper;
Expand All @@ -20,7 +21,6 @@ final class ApplicationFinishedSubscriber extends FormatterHelper implements Fin
public function __construct(
private readonly string $relativePathToCloverXml,
private readonly MinCoverageRules $minCoverageRules,
private readonly bool $exitOnLowCoverage,
private readonly bool $cleanUpCloverXml,
private readonly Exitter $exitter,
private readonly ConsoleOutput $consoleOutput,
Expand Down Expand Up @@ -48,7 +48,7 @@ public function notify(Finished $event): void
if ($this->minCoverageRules->hasTotalRule() && \XMLReader::ELEMENT == $reader->nodeType && 'metrics' == $reader->name && 2 === $reader->depth) {
/** @var \SimpleXMLElement $node */
$node = simplexml_load_string($reader->readOuterXml());
$metricTotal = CoverageMetric::fromCloverXmlNode($node, MinCoverageRules::TOTAL);
$metricTotal = CoverageMetric::fromCloverXmlNode($node, MinCoverageRule::TOTAL);
continue;
}
if ($this->minCoverageRules->hasOtherRulesThanTotalRule() && \XMLReader::ELEMENT == $reader->nodeType && 'class' == $reader->name && 3 === $reader->depth) {
Expand Down Expand Up @@ -79,8 +79,13 @@ public function notify(Finished $event): void

$this->consoleOutput->print($results, $finalStatus);

if ($this->exitOnLowCoverage && ResultStatus::FAILED === $finalStatus) {
$this->exitter->exit(1);
$needsExit = !empty(array_filter(
$results,
fn (MinCoverageResult $minCoverageResult) => $minCoverageResult->exitOnLowCoverage())
);
if (ResultStatus::FAILED === $finalStatus
&& $needsExit) {
$this->exitter->exit();
}
}

Expand All @@ -104,7 +109,10 @@ public static function fromConfigurationAndParameters(

try {
if (preg_match('/--min-coverage=(?<minCoverage>[\d]+)/', $arg, $matches)) {
$rules = MinCoverageRules::fromInt((int) $matches['minCoverage']);
$rules = MinCoverageRules::fromInt(
minCoverage: (int) $matches['minCoverage'],
exitOnLowCoverage: $parameters->has('exitOnLowCoverage') && (int) $parameters->get('exitOnLowCoverage')
);
break;
}

Expand All @@ -128,7 +136,6 @@ public static function fromConfigurationAndParameters(
return new self(
relativePathToCloverXml: $configuration->coverageClover(),
minCoverageRules: $rules,
exitOnLowCoverage: $parameters->has('exitOnLowCoverage') && (int) $parameters->get('exitOnLowCoverage'),
cleanUpCloverXml: $cleanUpCloverXml,
exitter: new Exitter(),
consoleOutput: new ConsoleOutput(new \Symfony\Component\Console\Output\ConsoleOutput()),
Expand Down
4 changes: 2 additions & 2 deletions tests/SpyOutput.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

namespace Tests;

use Symfony\Component\Console\Output\NullOutput;
use Symfony\Component\Console\Output\BufferedOutput;

class SpyOutput extends NullOutput implements \Stringable
class SpyOutput extends BufferedOutput implements \Stringable
{
private array $messages = [];

Expand Down
Loading

0 comments on commit 0b372dd

Please sign in to comment.