Skip to content

Commit 0b372dd

Browse files
Merge pull request #6 from robiningelbrecht/add-exit-on-low-coverage-per-rule
Add exit on low coverage per rule
2 parents ede686d + 32a7f3e commit 0b372dd

24 files changed

+394
-433
lines changed

README.md

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -67,17 +67,33 @@ For example:
6767
use RobinIngelbrecht\PHPUnitCoverageTools\MinCoverage\MinCoverageRules;
6868

6969
return [
70-
MinCoverageRules::TOTAL => 20,
71-
'RobinIngelbrecht\PHPUnitCoverageTools\*' => 80,
72-
'RobinIngelbrecht\PHPUnitCoverageTools\Subscriber\Application\ApplicationFinishedSubscriber' => 100,
73-
'RobinIngelbrecht\PHPUnitCoverageTools\*CommandHandler' => 100,
70+
new MinCoverageRule(
71+
pattern: MinCoverageRule::TOTAL,
72+
minCoverage: 20,
73+
exitOnLowCoverage: true
74+
),
75+
new MinCoverageRule(
76+
pattern: 'RobinIngelbrecht\PHPUnitCoverageTools\*',
77+
minCoverage: 80,
78+
exitOnLowCoverage: false
79+
),
80+
new MinCoverageRule(
81+
pattern: 'RobinIngelbrecht\PHPUnitCoverageTools\Subscriber\Application\ApplicationFinishedSubscriber',
82+
minCoverage: 100,
83+
exitOnLowCoverage: true
84+
),
85+
new MinCoverageRule(
86+
pattern: 'RobinIngelbrecht\PHPUnitCoverageTools\*CommandHandler',
87+
minCoverage: 100,
88+
exitOnLowCoverage: true
89+
),
7490
];
7591
```
7692

7793
This example will enforce:
7894

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

clover.xml

Lines changed: 0 additions & 306 deletions
This file was deleted.

src/ConsoleOutput.php

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -45,28 +45,47 @@ public function print(array $results, ResultStatus $finalStatus): void
4545
$tableStyle = new TableStyle();
4646
$tableStyle
4747
->setHeaderTitleFormat('<fg=black;bg=yellow;options=bold> %s </>')
48-
->setCellHeaderFormat('<bold>%s</bold>');
48+
->setCellHeaderFormat('<bold>%s</bold>')
49+
->setPadType(STR_PAD_BOTH);
4950

5051
$table = new Table($this->output);
5152
$table
5253
->setStyle($tableStyle)
5354
->setHeaderTitle('Code coverage results')
54-
->setHeaders(['Pattern', 'Expected', 'Actual', ''])
55+
->setHeaders(['Pattern', 'Expected', 'Actual', '', 'Exit on fail?'])
56+
->setColumnMaxWidth(1, 10)
57+
->setColumnMaxWidth(2, 8)
58+
->setColumnMaxWidth(4, 11)
5559
->setRows([
5660
...array_map(fn (MinCoverageResult $result) => [
57-
$result->getPattern(),
61+
new TableCell(
62+
$result->getPattern(),
63+
[
64+
'style' => new TableCellStyle([
65+
'align' => 'left',
66+
]),
67+
]
68+
),
5869
$result->getExpectedMinCoverage().'%',
5970
sprintf('<%s>%s%%</%s>', $result->getStatus()->value, $result->getActualMinCoverage(), $result->getStatus()->value),
60-
$result->getNumberOfTrackedLines() > 0 ?
61-
sprintf('<bold>%s</bold> of %s lines covered', $result->getNumberOfCoveredLines(), $result->getNumberOfTrackedLines()) :
62-
'No lines to track...?',
71+
new TableCell(
72+
$result->getNumberOfTrackedLines() > 0 ?
73+
sprintf('<bold>%s</bold> of %s lines covered', $result->getNumberOfCoveredLines(), $result->getNumberOfTrackedLines()) :
74+
'No lines to track...?',
75+
[
76+
'style' => new TableCellStyle([
77+
'align' => 'left',
78+
]),
79+
]
80+
),
81+
$result->exitOnLowCoverage() ? 'Yes' : 'No',
6382
], $results),
6483
new TableSeparator(),
6584
[
6685
new TableCell(
6786
$finalStatus->getMessage(),
6887
[
69-
'colspan' => 4,
88+
'colspan' => 5,
7089
'style' => new TableCellStyle([
7190
'align' => 'center',
7291
'cellFormat' => '<'.$finalStatus->value.'>%s</'.$finalStatus->value.'>',

src/Exitter.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44

55
class Exitter
66
{
7-
public function exit(int $code): void
7+
public function exit(): void
88
{
9-
exit($code);
9+
exit(1);
1010
}
1111
}

src/MinCoverage/MinCoverageResult.php

Lines changed: 30 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ private function __construct(
1010
private readonly float $actualMinCoverage,
1111
private readonly int $numberOfTrackedLines,
1212
private readonly int $numberOfCoveredLines,
13+
private readonly bool $exitOnLowCoverage
1314
) {
1415
}
1516

@@ -47,19 +48,26 @@ public function getNumberOfCoveredLines(): int
4748
return $this->numberOfCoveredLines;
4849
}
4950

51+
public function exitOnLowCoverage(): bool
52+
{
53+
return $this->exitOnLowCoverage;
54+
}
55+
5056
public static function fromPatternAndNumbers(
5157
string $pattern,
5258
int $expectedMinCoverage,
5359
float $actualMinCoverage,
5460
int $numberOfTrackedLines,
5561
int $numberOfCoveredLines,
62+
bool $exitOnLowCoverage
5663
): self {
5764
return new self(
58-
$pattern,
59-
$expectedMinCoverage,
60-
$actualMinCoverage,
61-
$numberOfTrackedLines,
62-
$numberOfCoveredLines,
65+
pattern: $pattern,
66+
expectedMinCoverage: $expectedMinCoverage,
67+
actualMinCoverage: $actualMinCoverage,
68+
numberOfTrackedLines: $numberOfTrackedLines,
69+
numberOfCoveredLines: $numberOfCoveredLines,
70+
exitOnLowCoverage: $exitOnLowCoverage
6371
);
6472
}
6573

@@ -74,14 +82,17 @@ public static function mapFromRulesAndMetrics(
7482
CoverageMetric $metricTotal = null,
7583
): array {
7684
$results = [];
77-
foreach ($minCoverageRules->getRules() as $pattern => $minCoverage) {
78-
if (MinCoverageRules::TOTAL === $pattern && $metricTotal) {
85+
foreach ($minCoverageRules->getRules() as $minCoverageRule) {
86+
$pattern = $minCoverageRule->getPattern();
87+
$minCoverage = $minCoverageRule->getMinCoverage();
88+
if (MinCoverageRule::TOTAL === $minCoverageRule->getPattern() && $metricTotal) {
7989
$results[] = MinCoverageResult::fromPatternAndNumbers(
80-
$pattern,
81-
$minCoverage,
82-
$metricTotal->getTotalPercentageCoverage(),
83-
$metricTotal->getNumberOfTrackedLines(),
84-
$metricTotal->getNumberOfCoveredLines()
90+
pattern: $pattern,
91+
expectedMinCoverage: $minCoverage,
92+
actualMinCoverage: $metricTotal->getTotalPercentageCoverage(),
93+
numberOfTrackedLines: $metricTotal->getNumberOfTrackedLines(),
94+
numberOfCoveredLines: $metricTotal->getNumberOfCoveredLines(),
95+
exitOnLowCoverage: $minCoverageRule->exitOnLowCoverage()
8596
);
8697
continue;
8798
}
@@ -97,16 +108,17 @@ public static function mapFromRulesAndMetrics(
97108
}
98109

99110
$results[] = MinCoverageResult::fromPatternAndNumbers(
100-
$pattern,
101-
$minCoverage,
102-
round($coveragePercentage, 2),
103-
$totalTrackedLines,
104-
$totalCoveredLines
111+
pattern: $pattern,
112+
expectedMinCoverage: $minCoverage,
113+
actualMinCoverage: round($coveragePercentage, 2),
114+
numberOfTrackedLines: $totalTrackedLines,
115+
numberOfCoveredLines: $totalCoveredLines,
116+
exitOnLowCoverage: $minCoverageRule->exitOnLowCoverage()
105117
);
106118
}
107119

108120
uasort($results, function (MinCoverageResult $a, MinCoverageResult $b) {
109-
if (MinCoverageRules::TOTAL === $a->getPattern()) {
121+
if (MinCoverageRule::TOTAL === $a->getPattern()) {
110122
return 1;
111123
}
112124
if ($a->getStatus() === $b->getStatus()) {
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
namespace RobinIngelbrecht\PHPUnitCoverageTools\MinCoverage;
4+
5+
final class MinCoverageRule
6+
{
7+
public const TOTAL = 'Total';
8+
9+
public function __construct(
10+
private readonly string $pattern,
11+
private readonly int $minCoverage,
12+
private readonly bool $exitOnLowCoverage
13+
) {
14+
if ($this->minCoverage < 0 || $this->minCoverage > 100) {
15+
throw new \RuntimeException(sprintf('MinCoverage has to be value between 0 and 100. %s given', $this->minCoverage));
16+
}
17+
}
18+
19+
public function getPattern(): string
20+
{
21+
return $this->pattern;
22+
}
23+
24+
public function getMinCoverage(): int
25+
{
26+
return $this->minCoverage;
27+
}
28+
29+
public function exitOnLowCoverage(): bool
30+
{
31+
return $this->exitOnLowCoverage;
32+
}
33+
34+
public function isTotalRule(): bool
35+
{
36+
return MinCoverageRule::TOTAL === $this->getPattern();
37+
}
38+
}

src/MinCoverage/MinCoverageRules.php

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,17 @@
66

77
class MinCoverageRules
88
{
9+
/** @deprecated Use MinCoverageRule::TOTAL */
910
public const TOTAL = 'Total';
1011

1112
private function __construct(
12-
/** @var array<string, int> */
13+
/** @var \RobinIngelbrecht\PHPUnitCoverageTools\MinCoverage\MinCoverageRule[] */
1314
private readonly array $rules
1415
) {
1516
}
1617

1718
/**
18-
* @return array<string, int>
19+
* @return \RobinIngelbrecht\PHPUnitCoverageTools\MinCoverage\MinCoverageRule[]
1920
*/
2021
public function getRules(): array
2122
{
@@ -24,28 +25,34 @@ public function getRules(): array
2425

2526
public function hasTotalRule(): bool
2627
{
27-
return array_key_exists(self::TOTAL, $this->rules);
28+
foreach ($this->rules as $rule) {
29+
if ($rule->isTotalRule()) {
30+
return true;
31+
}
32+
}
33+
34+
return false;
2835
}
2936

3037
public function hasOtherRulesThanTotalRule(): bool
3138
{
32-
foreach ($this->rules as $pattern => $minCoverage) {
33-
if (self::TOTAL !== $pattern) {
39+
foreach ($this->rules as $rule) {
40+
if (!$rule->isTotalRule()) {
3441
return true;
3542
}
3643
}
3744

3845
return false;
3946
}
4047

41-
public static function fromInt(int $minCoverage): self
48+
public static function fromInt(int $minCoverage, bool $exitOnLowCoverage): self
4249
{
43-
if ($minCoverage < 0 || $minCoverage > 100) {
44-
throw new \RuntimeException(sprintf('MinCoverage has to be value between 0 and 100. %s given', $minCoverage));
45-
}
46-
4750
return new self(
48-
[self::TOTAL => $minCoverage],
51+
[new MinCoverageRule(
52+
pattern: MinCoverageRule::TOTAL,
53+
minCoverage: $minCoverage,
54+
exitOnLowCoverage: $exitOnLowCoverage
55+
)],
4956
);
5057
}
5158

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

6269
$rules = require $absolutePathToConfigFile;
63-
foreach ($rules as $minCoverage) {
64-
if ($minCoverage < 0 || $minCoverage > 100) {
65-
throw new \RuntimeException(sprintf('MinCoverage has to be value between 0 and 100. %s given', $minCoverage));
70+
foreach ($rules as $minCoverageRule) {
71+
if (!$minCoverageRule instanceof MinCoverageRule) {
72+
throw new \RuntimeException('Make sure all coverage rules are of instance '.MinCoverageRule::class);
6673
}
6774
}
75+
$patterns = array_map(fn (MinCoverageRule $minCoverageRule) => $minCoverageRule->getPattern(), $rules);
76+
if (count(array_unique($patterns)) !== count($patterns)) {
77+
throw new \RuntimeException('Make sure all coverage rule patterns are unique');
78+
}
6879

6980
return new self($rules);
7081
}

src/Subscriber/Application/ApplicationFinishedSubscriber.php

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use RobinIngelbrecht\PHPUnitCoverageTools\Exitter;
1212
use RobinIngelbrecht\PHPUnitCoverageTools\MinCoverage\CoverageMetric;
1313
use RobinIngelbrecht\PHPUnitCoverageTools\MinCoverage\MinCoverageResult;
14+
use RobinIngelbrecht\PHPUnitCoverageTools\MinCoverage\MinCoverageRule;
1415
use RobinIngelbrecht\PHPUnitCoverageTools\MinCoverage\MinCoverageRules;
1516
use RobinIngelbrecht\PHPUnitCoverageTools\MinCoverage\ResultStatus;
1617
use Symfony\Component\Console\Helper\FormatterHelper;
@@ -20,7 +21,6 @@ final class ApplicationFinishedSubscriber extends FormatterHelper implements Fin
2021
public function __construct(
2122
private readonly string $relativePathToCloverXml,
2223
private readonly MinCoverageRules $minCoverageRules,
23-
private readonly bool $exitOnLowCoverage,
2424
private readonly bool $cleanUpCloverXml,
2525
private readonly Exitter $exitter,
2626
private readonly ConsoleOutput $consoleOutput,
@@ -48,7 +48,7 @@ public function notify(Finished $event): void
4848
if ($this->minCoverageRules->hasTotalRule() && \XMLReader::ELEMENT == $reader->nodeType && 'metrics' == $reader->name && 2 === $reader->depth) {
4949
/** @var \SimpleXMLElement $node */
5050
$node = simplexml_load_string($reader->readOuterXml());
51-
$metricTotal = CoverageMetric::fromCloverXmlNode($node, MinCoverageRules::TOTAL);
51+
$metricTotal = CoverageMetric::fromCloverXmlNode($node, MinCoverageRule::TOTAL);
5252
continue;
5353
}
5454
if ($this->minCoverageRules->hasOtherRulesThanTotalRule() && \XMLReader::ELEMENT == $reader->nodeType && 'class' == $reader->name && 3 === $reader->depth) {
@@ -79,8 +79,13 @@ public function notify(Finished $event): void
7979

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

82-
if ($this->exitOnLowCoverage && ResultStatus::FAILED === $finalStatus) {
83-
$this->exitter->exit(1);
82+
$needsExit = !empty(array_filter(
83+
$results,
84+
fn (MinCoverageResult $minCoverageResult) => $minCoverageResult->exitOnLowCoverage())
85+
);
86+
if (ResultStatus::FAILED === $finalStatus
87+
&& $needsExit) {
88+
$this->exitter->exit();
8489
}
8590
}
8691

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

105110
try {
106111
if (preg_match('/--min-coverage=(?<minCoverage>[\d]+)/', $arg, $matches)) {
107-
$rules = MinCoverageRules::fromInt((int) $matches['minCoverage']);
112+
$rules = MinCoverageRules::fromInt(
113+
minCoverage: (int) $matches['minCoverage'],
114+
exitOnLowCoverage: $parameters->has('exitOnLowCoverage') && (int) $parameters->get('exitOnLowCoverage')
115+
);
108116
break;
109117
}
110118

@@ -128,7 +136,6 @@ public static function fromConfigurationAndParameters(
128136
return new self(
129137
relativePathToCloverXml: $configuration->coverageClover(),
130138
minCoverageRules: $rules,
131-
exitOnLowCoverage: $parameters->has('exitOnLowCoverage') && (int) $parameters->get('exitOnLowCoverage'),
132139
cleanUpCloverXml: $cleanUpCloverXml,
133140
exitter: new Exitter(),
134141
consoleOutput: new ConsoleOutput(new \Symfony\Component\Console\Output\ConsoleOutput()),

tests/SpyOutput.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22

33
namespace Tests;
44

5-
use Symfony\Component\Console\Output\NullOutput;
5+
use Symfony\Component\Console\Output\BufferedOutput;
66

7-
class SpyOutput extends NullOutput implements \Stringable
7+
class SpyOutput extends BufferedOutput implements \Stringable
88
{
99
private array $messages = [];
1010

0 commit comments

Comments
 (0)