├── phpstan.baseline-8.2.neon
├── .gitignore
├── tests
├── Subscriber
│ └── Application
│ │ ├── min-coverage-rules-empty.php
│ │ ├── min-coverage-invalid-rule-instances.php
│ │ ├── min-coverage-rules-no-tracked-lines.php
│ │ ├── min-coverage-rules-total-only.php
│ │ ├── min-coverage-rules-without-total.php
│ │ ├── min-coverage-rules-with-warning.php
│ │ ├── min-coverage-rules-invalid.php
│ │ ├── min-coverage-rules-success.php
│ │ ├── min-coverage-rules-with-duplicates.php
│ │ ├── min-coverage-rules-no-exit.php
│ │ ├── min-coverage-rules-with-failed-rule.php
│ │ └── ApplicationFinishedSubscriberTest.php
├── clover-invalid.xml
├── clover-test-divide-by-zero.xml
├── PausedTimer.php
├── clover-with-no-tracked-lines.xml
├── SpyOutput.php
├── FixedResourceUsageFormatter.php
└── clover.xml
├── readme
├── fail-example.png
├── success-example.png
└── warning-example.png
├── phpstan.neon
├── docker
└── php-cli
│ ├── xdebug.ini
│ └── Dockerfile
├── src
├── Exitter.php
├── Timer
│ ├── Timer.php
│ ├── ResourceUsageFormatter.php
│ ├── SystemTimer.php
│ └── SystemResourceUsageFormatter.php
├── PhpUnitExtension.php
├── MinCoverage
│ ├── MinCoverageRule.php
│ ├── ResultStatus.php
│ ├── MinCoverageRules.php
│ ├── CoverageMetric.php
│ └── MinCoverageResult.php
├── ConsoleOutput.php
└── Subscriber
│ └── Application
│ └── ApplicationFinishedSubscriber.php
├── docker-compose.yml
├── .php-cs-fixer.dist.php
├── phpstan.ignore-by-php-version.neon.php
├── phpunit.xml.dist
├── Makefile
├── LICENSE
├── composer.json
├── .github
└── workflows
│ └── ci.yml
└── README.md
/phpstan.baseline-8.2.neon:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | vendor
2 | .phpunit.cache
3 | .php-cs-fixer.cache
--------------------------------------------------------------------------------
/tests/Subscriber/Application/min-coverage-rules-empty.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/docker/php-cli/xdebug.ini:
--------------------------------------------------------------------------------
1 | xdebug.mode=coverage,debug
2 | xdebug.start_with_request=yes
3 | xdebug.discover_client_host=0
4 | xdebug.client_host=host.docker.internal
5 | xdebug.idekey=PHPSTORM
6 |
--------------------------------------------------------------------------------
/docker/php-cli/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM php:8.4-cli
2 |
3 | RUN apt-get update && apt-get install -y zip unzip git curl
4 | RUN docker-php-ext-install mysqli pdo pdo_mysql
5 |
6 | COPY --from=composer:latest /usr/bin/composer /usr/local/bin/composer
--------------------------------------------------------------------------------
/src/Exitter.php:
--------------------------------------------------------------------------------
1 | 100,
8 | ];
9 |
--------------------------------------------------------------------------------
/src/Timer/Timer.php:
--------------------------------------------------------------------------------
1 | in(__DIR__)
5 | ->exclude('var')
6 | ->exclude('vendor')
7 | ->exclude('docker')
8 | ->exclude('bin');
9 |
10 | return (new PhpCsFixer\Config)
11 | ->setRules([
12 | '@Symfony' => true
13 | ])
14 | ->setFinder($finder);
--------------------------------------------------------------------------------
/phpstan.ignore-by-php-version.neon.php:
--------------------------------------------------------------------------------
1 | = 80200) {
7 | $includes[] = __DIR__.'/phpstan.baseline-8.2.neon';
8 | }
9 |
10 | $config = [];
11 | $config['includes'] = $includes;
12 | $config['parameters']['phpVersion'] = PHP_VERSION_ID;
13 |
14 | return $config;
15 |
--------------------------------------------------------------------------------
/src/Timer/ResourceUsageFormatter.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/tests/Subscriber/Application/min-coverage-rules-with-warning.php:
--------------------------------------------------------------------------------
1 | duration;
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
13 | tests
14 |
15 |
16 |
17 |
18 | src
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/src/Timer/SystemTimer.php:
--------------------------------------------------------------------------------
1 | timer->start();
26 | }
27 |
28 | /**
29 | * @codeCoverageIgnore
30 | */
31 | public function stop(): Duration
32 | {
33 | return $this->timer->stop();
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/tests/clover-with-no-tracked-lines.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/tests/Subscriber/Application/min-coverage-rules-no-exit.php:
--------------------------------------------------------------------------------
1 | messages = [...$this->messages, ...$messages];
17 | }
18 |
19 | public function write($messages, bool $newline = false, int $options = self::OUTPUT_NORMAL): void
20 | {
21 | if (!is_iterable($messages)) {
22 | $messages = [$messages];
23 | }
24 | $this->messages = [...$this->messages, ...$messages];
25 | }
26 |
27 | public function __toString(): string
28 | {
29 | return implode(PHP_EOL, $this->messages);
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/tests/FixedResourceUsageFormatter.php:
--------------------------------------------------------------------------------
1 | asString(),
25 | number_format($this->usageInMb, 2, '.', ''),
26 | );
27 | }
28 |
29 | public function resourceUsageSinceStartOfRequest(): string
30 | {
31 | return sprintf(
32 | 'Time: 00:00.350, Memory: %s MB',
33 | number_format($this->usageInMb, 2, '.', ''),
34 | );
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/PhpUnitExtension.php:
--------------------------------------------------------------------------------
1 | registerSubscribers(
30 | $subscriber,
31 | );
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/Timer/SystemResourceUsageFormatter.php:
--------------------------------------------------------------------------------
1 | resourceUsageFormatter->resourceUsage($duration);
26 | }
27 |
28 | /**
29 | * @codeCoverageIgnore
30 | */
31 | public function resourceUsageSinceStartOfRequest(): string
32 | {
33 | return $this->resourceUsageFormatter->resourceUsageSinceStartOfRequest();
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/MinCoverage/MinCoverageRule.php:
--------------------------------------------------------------------------------
1 | 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 | }
39 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Robin Ingelbrecht
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "robiningelbrecht/phpunit-coverage-tools",
3 | "description": "PHPUnit coverage tools",
4 | "keywords": [
5 | "Testing",
6 | "PHP",
7 | "Code coverage",
8 | "phpunit"
9 | ],
10 | "type": "library",
11 | "license": "MIT",
12 | "authors": [
13 | {
14 | "name": "Robin Ingelbrecht",
15 | "email": "ingelbrecht_robin@hotmail.com"
16 | }
17 | ],
18 | "require": {
19 | "php": "^8.1",
20 | "ext-simplexml": "*",
21 | "ext-xmlreader": "*",
22 | "phpunit/phpunit": "^10.3||^11.0||^12.0",
23 | "symfony/console": "^5.4||^6.2||^7.0||^8.0"
24 | },
25 | "autoload": {
26 | "psr-4": {
27 | "RobinIngelbrecht\\PHPUnitCoverageTools\\": "src/"
28 | }
29 | },
30 | "autoload-dev": {
31 | "psr-4": {
32 | "Tests\\": "tests/"
33 | }
34 | },
35 | "require-dev": {
36 | "friendsofphp/php-cs-fixer": "^3.9",
37 | "phpstan/phpstan": "^2",
38 | "spatie/phpunit-snapshot-assertions": "^5.0"
39 | },
40 | "config": {
41 | "sort-packages": true
42 | },
43 | "scripts": {
44 | "lint:fix": " ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php",
45 | "phpunit:test": "vendor/bin/phpunit --configuration=tests/phpunit.test.xml --no-output"
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/MinCoverage/ResultStatus.php:
--------------------------------------------------------------------------------
1 | self::SUCCESS ,
15 | 2 => self::WARNING,
16 | 3 => self::FAILED,
17 | default => throw new \InvalidArgumentException('Invalid weight '.$weight),
18 | };
19 | }
20 |
21 | public function getWeight(): int
22 | {
23 | return match ($this) {
24 | self::SUCCESS => 1,
25 | self::WARNING => 2,
26 | self::FAILED => 3,
27 | };
28 | }
29 |
30 | public function getMessage(): string
31 | {
32 | return match ($this) {
33 | self::SUCCESS => 'All minimum code coverage rules passed, give yourself a pat on the back!',
34 | self::WARNING => 'There was at least one pattern that did not match any covered classes. Please consider removing them.',
35 | self::FAILED => 'Not all minimum code coverage rules passed, please try again... :)',
36 | };
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 | on:
3 | pull_request:
4 | workflow_dispatch:
5 | jobs:
6 | test-suite:
7 | name: PHPStan, PHPcs & Testsuite
8 | runs-on: ubuntu-latest
9 | strategy:
10 | matrix:
11 | php-versions: [ '8.1', '8.2', '8.3', '8.4', '8.5' ]
12 |
13 | steps:
14 | # https://github.com/marketplace/actions/setup-php-action
15 | - name: Setup PHP ${{ matrix.php-versions }} with Xdebug 3.x
16 | uses: shivammathur/setup-php@v2
17 | with:
18 | php-version: ${{ matrix.php-versions }}
19 | coverage: xdebug
20 |
21 | # https://github.com/marketplace/actions/checkout
22 | - name: Checkout code
23 | uses: actions/checkout@v3
24 |
25 | - name: Remove composer.lock
26 | run: |
27 | rm composer.lock
28 |
29 | - name: Install dependencies
30 | run: composer install --prefer-dist
31 |
32 | - name: Run PHPStan
33 | run: vendor/bin/phpstan analyse
34 |
35 | #- name: Run PHPcs fixer dry-run
36 | # run: vendor/bin/php-cs-fixer fix --dry-run --stop-on-violation --config=.php-cs-fixer.dist.php
37 |
38 | - name: Run test suite
39 | run: vendor/bin/phpunit --fail-on-incomplete --log-junit junit.xml --coverage-clover clover.xml
40 |
41 | # https://github.com/marketplace/actions/codecov
42 | - name: Send test coverage to codecov.io
43 | uses: codecov/codecov-action@v5
44 | with:
45 | files: clover.xml,!tests/clover.xml,!tests/clover-invalid.xml,!tests/clover-test-divide-by-zero.xml
46 | fail_ci_if_error: true # optional (default = false)
47 | verbose: true # optional (default = false)
48 | token: ${{ secrets.CODECOV_TOKEN }}
49 |
--------------------------------------------------------------------------------
/src/MinCoverage/MinCoverageRules.php:
--------------------------------------------------------------------------------
1 | rules;
24 | }
25 |
26 | public function hasTotalRule(): bool
27 | {
28 | foreach ($this->rules as $rule) {
29 | if ($rule->isTotalRule()) {
30 | return true;
31 | }
32 | }
33 |
34 | return false;
35 | }
36 |
37 | public function hasOtherRulesThanTotalRule(): bool
38 | {
39 | foreach ($this->rules as $rule) {
40 | if (!$rule->isTotalRule()) {
41 | return true;
42 | }
43 | }
44 |
45 | return false;
46 | }
47 |
48 | public static function fromInt(int $minCoverage, bool $exitOnLowCoverage): self
49 | {
50 | return new self(
51 | [new MinCoverageRule(
52 | pattern: MinCoverageRule::TOTAL,
53 | minCoverage: $minCoverage,
54 | exitOnLowCoverage: $exitOnLowCoverage
55 | )],
56 | );
57 | }
58 |
59 | public static function fromConfigFile(string $filePathToConfigFile): self
60 | {
61 | /** @var string $reflectionFileName */
62 | $reflectionFileName = (new \ReflectionClass(ClassLoader::class))->getFileName();
63 | $absolutePathToConfigFile = dirname($reflectionFileName, 3).'/'.$filePathToConfigFile;
64 |
65 | if (!file_exists($absolutePathToConfigFile)) {
66 | throw new \RuntimeException(sprintf('Config file %s not found', $absolutePathToConfigFile));
67 | }
68 |
69 | $rules = require $absolutePathToConfigFile;
70 | foreach ($rules as $minCoverageRule) {
71 | if (!$minCoverageRule instanceof MinCoverageRule) {
72 | throw new \RuntimeException('Make sure all coverage rules are of instance '.MinCoverageRule::class);
73 | }
74 | }
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 | }
79 |
80 | return new self($rules);
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/src/MinCoverage/CoverageMetric.php:
--------------------------------------------------------------------------------
1 | forClass;
23 | }
24 |
25 | public function getNumberOfMethods(): int
26 | {
27 | return $this->numberOfMethods;
28 | }
29 |
30 | public function getNumberOfCoveredMethods(): int
31 | {
32 | return $this->numberOfCoveredMethods;
33 | }
34 |
35 | public function getNumberOfStatements(): int
36 | {
37 | return $this->numberOfStatements;
38 | }
39 |
40 | public function getNumberOfCoveredStatements(): int
41 | {
42 | return $this->numberOfCoveredStatements;
43 | }
44 |
45 | public function getNumberOfConditionals(): int
46 | {
47 | return $this->numberOfConditionals;
48 | }
49 |
50 | public function getNumberOfCoveredConditionals(): int
51 | {
52 | return $this->numberOfCoveredConditionals;
53 | }
54 |
55 | public function getNumberOfTrackedLines(): int
56 | {
57 | return $this->numberOfTrackedLines;
58 | }
59 |
60 | public function getNumberOfCoveredLines(): int
61 | {
62 | return $this->numberOfCoveredLines;
63 | }
64 |
65 | public function getTotalPercentageCoverage(): float
66 | {
67 | // https://confluence.atlassian.com/clover/how-are-the-clover-coverage-percentages-calculated-79986990.html
68 | // TPC = (coveredconditionals + coveredstatements + coveredmethods) / (conditionals + statements + methods)
69 | $divideBy = $this->getNumberOfConditionals() + $this->getNumberOfStatements() + $this->getNumberOfMethods();
70 | if (0 === $divideBy) {
71 | return 0.00;
72 | }
73 |
74 | return round((($this->getNumberOfCoveredConditionals() + $this->getNumberOfCoveredStatements() + $this->getNumberOfCoveredMethods()) /
75 | $divideBy) * 100, 2);
76 | }
77 |
78 | public static function fromCloverXmlNode(\SimpleXMLElement $node, string $forClass): self
79 | {
80 | /** @var \SimpleXMLElement $attributes */
81 | $attributes = $node->attributes();
82 |
83 | return new self(
84 | forClass: $forClass,
85 | numberOfMethods: (int) $attributes['methods'],
86 | numberOfCoveredMethods: (int) $attributes['coveredmethods'],
87 | numberOfStatements: (int) $attributes['statements'],
88 | numberOfCoveredStatements: (int) $attributes['coveredstatements'],
89 | numberOfConditionals: (int) $attributes['conditionals'],
90 | numberOfCoveredConditionals: (int) $attributes['coveredconditionals'],
91 | numberOfTrackedLines: (int) $attributes['elements'],
92 | numberOfCoveredLines: (int) $attributes['coveredelements'],
93 | );
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/src/MinCoverage/MinCoverageResult.php:
--------------------------------------------------------------------------------
1 | pattern;
20 | }
21 |
22 | public function getStatus(): ResultStatus
23 | {
24 | if (0 === $this->getNumberOfTrackedLines()) {
25 | return ResultStatus::WARNING;
26 | }
27 |
28 | return $this->expectedMinCoverage <= $this->actualMinCoverage ? ResultStatus::SUCCESS : ResultStatus::FAILED;
29 | }
30 |
31 | public function getExpectedMinCoverage(): int
32 | {
33 | return $this->expectedMinCoverage;
34 | }
35 |
36 | public function getActualMinCoverage(): float
37 | {
38 | return $this->actualMinCoverage;
39 | }
40 |
41 | public function getNumberOfTrackedLines(): int
42 | {
43 | return $this->numberOfTrackedLines;
44 | }
45 |
46 | public function getNumberOfCoveredLines(): int
47 | {
48 | return $this->numberOfCoveredLines;
49 | }
50 |
51 | public function exitOnLowCoverage(): bool
52 | {
53 | return $this->exitOnLowCoverage;
54 | }
55 |
56 | /**
57 | * @param CoverageMetric[] $metrics
58 | *
59 | * @return MinCoverageResult[]
60 | */
61 | public static function mapFromRulesAndMetrics(
62 | MinCoverageRules $minCoverageRules,
63 | array $metrics,
64 | ?CoverageMetric $metricTotal = null,
65 | ): array {
66 | $results = [];
67 | foreach ($minCoverageRules->getRules() as $minCoverageRule) {
68 | $pattern = $minCoverageRule->getPattern();
69 | $minCoverage = $minCoverageRule->getMinCoverage();
70 | if (MinCoverageRule::TOTAL === $minCoverageRule->getPattern() && $metricTotal) {
71 | $results[] = new MinCoverageResult(
72 | pattern: $pattern,
73 | expectedMinCoverage: $minCoverage,
74 | actualMinCoverage: $metricTotal->getTotalPercentageCoverage(),
75 | numberOfTrackedLines: $metricTotal->getNumberOfTrackedLines(),
76 | numberOfCoveredLines: $metricTotal->getNumberOfCoveredLines(),
77 | exitOnLowCoverage: $minCoverageRule->exitOnLowCoverage()
78 | );
79 | continue;
80 | }
81 |
82 | $metricsForPattern = array_filter($metrics, fn (CoverageMetric $metric) => fnmatch($pattern, $metric->getForClass(), FNM_NOESCAPE));
83 | $totalTrackedLines = array_sum(array_map(fn (CoverageMetric $metric) => $metric->getNumberOfTrackedLines(), $metricsForPattern));
84 | $totalCoveredLines = array_sum(array_map(fn (CoverageMetric $metric) => $metric->getNumberOfCoveredLines(), $metricsForPattern));
85 |
86 | $coveragePercentage = 0;
87 | foreach ($metricsForPattern as $metric) {
88 | if (0 === $totalTrackedLines) {
89 | continue;
90 | }
91 | $weight = $metric->getNumberOfTrackedLines() / $totalTrackedLines;
92 | $coveragePercentage += ($metric->getTotalPercentageCoverage() * $weight);
93 | }
94 |
95 | $results[] = new MinCoverageResult(
96 | pattern: $pattern,
97 | expectedMinCoverage: $minCoverage,
98 | actualMinCoverage: round($coveragePercentage, 2),
99 | numberOfTrackedLines: $totalTrackedLines,
100 | numberOfCoveredLines: $totalCoveredLines,
101 | exitOnLowCoverage: $minCoverageRule->exitOnLowCoverage()
102 | );
103 | }
104 |
105 | uasort($results, function (MinCoverageResult $a, MinCoverageResult $b) {
106 | if (MinCoverageRule::TOTAL === $a->getPattern()) {
107 | return 1;
108 | }
109 | if ($a->getStatus() === $b->getStatus()) {
110 | return 0;
111 | }
112 |
113 | return ($a->getStatus()->getWeight() < $b->getStatus()->getWeight()) ? 1 : -1;
114 | });
115 |
116 | return $results;
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
PHPUnit Coverage tools
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | ---
14 |
15 | This extension allows you to enforce minimum code coverage by using the clover xml report from PHPUnit.
16 | Based on the given threshold the testsuite will exit ok if the coverage is higher than the threshold
17 | or exit with code 1 if the coverage is lower than the threshold.
18 | This can be used in your continuous deployment environment or can be added to a pre-commit hook.
19 |
20 | ## Installation
21 |
22 | ```bash
23 | > composer require robiningelbrecht/phpunit-coverage-tools --dev
24 | ```
25 |
26 | ## Configuration
27 |
28 | Navigate to your `phpunit.xml.dist` file and add following config to set default options:
29 |
30 | ```xml
31 |
32 |
33 |
34 |
35 |
36 |
37 | ```
38 | ## Usage
39 |
40 | Just run your testsuite like you normally would, but add following arguments:
41 |
42 | ### --min-coverage=`[INTEGER]`
43 |
44 | ```bash
45 | > vendor/bin/phpunit --coverage-clover=path/to/clover.xml -d --min-coverage=100
46 | ```
47 |
48 | When assigning an integer between 0 - 100, you enforce a minimum code coverage
49 | for all your classes. In other words, the total coverage of your project has to be
50 | higher than this threshold.
51 |
52 | ### --min-coverage=`[path/to/min-coverage-rules.php]`
53 |
54 | ```bash
55 | > vendor/bin/phpunit --coverage-clover=path/to/clover.xml -d --min-coverage="path/to/min-coverage-rules.php"
56 | ```
57 |
58 | When referencing a PHP config file, you can configure more complex rules.
59 | This allows you to be stricter for critical parts of your application and less strict
60 | for parts of your app that are not that critical.
61 |
62 | For example:
63 |
64 | ```php
65 | output->setDecorated(true);
25 | $this->output->getFormatter()->setStyle(
26 | 'success',
27 | new OutputFormatterStyle('green', null, ['bold'])
28 | );
29 | $this->output->getFormatter()->setStyle(
30 | 'failed',
31 | new OutputFormatterStyle('red', null, ['bold'])
32 | );
33 | $this->output->getFormatter()->setStyle(
34 | 'warning',
35 | new OutputFormatterStyle('yellow', null, ['bold'])
36 | );
37 | $this->output->getFormatter()->setStyle(
38 | 'bold',
39 | new OutputFormatterStyle(null, null, ['bold'])
40 | );
41 | }
42 |
43 | public static function create(): self
44 | {
45 | return new self(
46 | output: new \Symfony\Component\Console\Output\ConsoleOutput(),
47 | resourceUsageFormatter: SystemResourceUsageFormatter::create()
48 | );
49 | }
50 |
51 | /**
52 | * @param MinCoverageResult[] $results
53 | */
54 | public function print(array $results, Duration $duration): void
55 | {
56 | /** @var non-empty-array $statusWeights */
57 | $statusWeights = array_map(fn (MinCoverageResult $result) => $result->getStatus()->getWeight(), $results);
58 | $finalStatus = ResultStatus::fromWeight(max($statusWeights));
59 |
60 | $this->output->writeln('');
61 | $tableStyle = new TableStyle();
62 | $tableStyle
63 | ->setHeaderTitleFormat(' %s >')
64 | ->setCellHeaderFormat('%s')
65 | ->setPadType(STR_PAD_BOTH);
66 |
67 | $table = new Table($this->output);
68 | $table
69 | ->setStyle($tableStyle)
70 | ->setHeaderTitle('Code coverage results')
71 | ->setHeaders(['Pattern', 'Expected', 'Actual', '', 'Exit on fail?'])
72 | ->setColumnMaxWidth(1, 10)
73 | ->setColumnMaxWidth(2, 8)
74 | ->setColumnMaxWidth(4, 11)
75 | ->setRows([
76 | ...array_map(fn (MinCoverageResult $result) => [
77 | new TableCell(
78 | $result->getPattern(),
79 | [
80 | 'style' => new TableCellStyle([
81 | 'align' => 'left',
82 | ]),
83 | ]
84 | ),
85 | $result->getExpectedMinCoverage().'%',
86 | sprintf('<%s>%s%%%s>', $result->getStatus()->value, $result->getActualMinCoverage(), $result->getStatus()->value),
87 | new TableCell(
88 | $result->getNumberOfTrackedLines() > 0 ?
89 | sprintf('%s of %s lines covered', $result->getNumberOfCoveredLines(), $result->getNumberOfTrackedLines()) :
90 | 'No lines to track...?',
91 | [
92 | 'style' => new TableCellStyle([
93 | 'align' => 'left',
94 | ]),
95 | ]
96 | ),
97 | $result->exitOnLowCoverage() ? 'Yes' : 'No',
98 | ], $results),
99 | new TableSeparator(),
100 | [
101 | new TableCell(
102 | $finalStatus->getMessage(),
103 | [
104 | 'colspan' => 5,
105 | 'style' => new TableCellStyle([
106 | 'align' => 'center',
107 | 'cellFormat' => '<'.$finalStatus->value.'>%s'.$finalStatus->value.'>',
108 | ]
109 | ),
110 | ]
111 | ),
112 | ],
113 | ]);
114 | $table->render();
115 | $this->output->writeln($this->resourceUsageFormatter->resourceUsage($duration));
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/src/Subscriber/Application/ApplicationFinishedSubscriber.php:
--------------------------------------------------------------------------------
1 | timer->start();
36 | /** @var string $reflectionFileName */
37 | $reflectionFileName = (new \ReflectionClass(ClassLoader::class))->getFileName();
38 |
39 | $absolutePathToCloverXml = $this->pathToCloverXml;
40 | if (!str_starts_with($this->pathToCloverXml, '/')) {
41 | // User is probably using relative path to clover.xml
42 | $absolutePathToCloverXml = dirname($reflectionFileName, 3).'/'.$this->pathToCloverXml;
43 | }
44 |
45 | if (!file_exists($absolutePathToCloverXml)) {
46 | throw new \RuntimeException('Clover XML file not found at: '.$absolutePathToCloverXml);
47 | }
48 |
49 | /** @var CoverageMetric[] $metrics */
50 | $metrics = [];
51 | $metricTotal = null;
52 |
53 | // @TODO: Move this to static function in CoverageMetric?
54 | $reader = new \XMLReader();
55 | $reader->open($absolutePathToCloverXml);
56 | while ($reader->read()) {
57 | if ($this->minCoverageRules->hasTotalRule() && \XMLReader::ELEMENT == $reader->nodeType && 'metrics' == $reader->name && 2 === $reader->depth) {
58 | /** @var \SimpleXMLElement $node */
59 | $node = simplexml_load_string($reader->readOuterXml());
60 | $metricTotal = CoverageMetric::fromCloverXmlNode(
61 | node: $node,
62 | forClass: MinCoverageRule::TOTAL
63 | );
64 | continue;
65 | }
66 | if ($this->minCoverageRules->hasOtherRulesThanTotalRule()
67 | && \XMLReader::ELEMENT == $reader->nodeType && 'class' == $reader->name
68 | && (3 === $reader->depth || 4 === $reader->depth)) {
69 | /** @var \SimpleXMLElement $node */
70 | $node = simplexml_load_string($reader->readInnerXml());
71 | /** @var string $className */
72 | $className = $reader->getAttribute('name');
73 | $metrics[] = CoverageMetric::fromCloverXmlNode(
74 | node: $node,
75 | forClass: $className
76 | );
77 | }
78 | }
79 | $reader->close();
80 |
81 | if ($this->cleanUpCloverXml) {
82 | unlink($absolutePathToCloverXml);
83 | }
84 |
85 | if (!$metrics && !$metricTotal) {
86 | throw new \RuntimeException('Could not determine coverage metrics');
87 | }
88 |
89 | $results = MinCoverageResult::mapFromRulesAndMetrics(
90 | minCoverageRules: $this->minCoverageRules,
91 | metrics: $metrics,
92 | metricTotal: $metricTotal,
93 | );
94 |
95 | $this->consoleOutput->print(
96 | results: $results,
97 | duration: $this->timer->stop()
98 | );
99 |
100 | $needsExit = !empty(array_filter(
101 | $results,
102 | fn (MinCoverageResult $minCoverageResult) => $minCoverageResult->exitOnLowCoverage() && ResultStatus::FAILED === $minCoverageResult->getStatus())
103 | );
104 | if ($needsExit) {
105 | $this->exitter->exit();
106 | }
107 | }
108 |
109 | /**
110 | * @param string[] $args
111 | */
112 | public static function fromConfigurationAndParameters(
113 | Configuration $configuration,
114 | ParameterCollection $parameters,
115 | array $args,
116 | ): ?self {
117 | if (!$configuration->hasCoverageClover()) {
118 | return null;
119 | }
120 |
121 | $rules = null;
122 | foreach ($args as $arg) {
123 | if (!str_starts_with($arg, '--min-coverage=')) {
124 | continue;
125 | }
126 |
127 | try {
128 | if (preg_match('/--min-coverage=(?[\d]+)/', $arg, $matches)) {
129 | $rules = MinCoverageRules::fromInt(
130 | minCoverage: (int) $matches['minCoverage'],
131 | exitOnLowCoverage: $parameters->has('exitOnLowCoverage') && (int) $parameters->get('exitOnLowCoverage')
132 | );
133 | break;
134 | }
135 |
136 | if (preg_match('/--min-coverage=(?[\S]+)/', $arg, $matches)) {
137 | $rules = MinCoverageRules::fromConfigFile(trim($matches['minCoverage'], '"'));
138 | break;
139 | }
140 | } catch (\RuntimeException) {
141 | return null;
142 | }
143 | }
144 |
145 | if (empty($rules) || empty($rules->getRules())) {
146 | return null;
147 | }
148 |
149 | if (!$cleanUpCloverXml = in_array('--clean-up-clover-xml', $args, true)) {
150 | $cleanUpCloverXml = $parameters->has('cleanUpCloverXml') && (int) $parameters->get('cleanUpCloverXml');
151 | }
152 |
153 | return new self(
154 | pathToCloverXml: $configuration->coverageClover(),
155 | minCoverageRules: $rules,
156 | cleanUpCloverXml: $cleanUpCloverXml,
157 | exitter: new Exitter(),
158 | consoleOutput: ConsoleOutput::create(),
159 | timer: SystemTimer::create(),
160 | );
161 | }
162 | }
163 |
--------------------------------------------------------------------------------
/tests/clover.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
--------------------------------------------------------------------------------
/tests/Subscriber/Application/ApplicationFinishedSubscriberTest.php:
--------------------------------------------------------------------------------
1 | exitter = $this->createMock(Exitter::class);
42 | $this->output = new SpyOutput();
43 | $this->timer = PausedTimer::withDuration(\SebastianBergmann\Timer\Duration::fromMicroseconds(350123));
44 | $this->resourceUsageFormatter = FixedResourceUsageFormatter::withUsageInMb(12.00);
45 | }
46 |
47 | public function testNotifyWithAtLeastOneFailedRule(): void
48 | {
49 | $this->exitter
50 | ->expects($this->once())
51 | ->method('exit');
52 |
53 | $subscriber = new ApplicationFinishedSubscriber(
54 | pathToCloverXml: 'tests/clover.xml',
55 | minCoverageRules: MinCoverageRules::fromConfigFile('tests/Subscriber/Application/min-coverage-rules-with-failed-rule.php'),
56 | cleanUpCloverXml: false,
57 | exitter: $this->exitter,
58 | consoleOutput: new ConsoleOutput($this->output, $this->resourceUsageFormatter),
59 | timer: $this->timer,
60 | );
61 |
62 | $subscriber->notify(event: new Finished(
63 | new Info(
64 | current: new Snapshot(
65 | time: HRTime::fromSecondsAndNanoseconds(1, 0),
66 | memoryUsage: MemoryUsage::fromBytes(100),
67 | peakMemoryUsage: MemoryUsage::fromBytes(100),
68 | garbageCollectorStatus: new GarbageCollectorStatus(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
69 | ),
70 | durationSinceStart: Duration::fromSecondsAndNanoseconds(1, 0),
71 | memorySinceStart: MemoryUsage::fromBytes(100),
72 | durationSincePrevious: Duration::fromSecondsAndNanoseconds(1, 0),
73 | memorySincePrevious: MemoryUsage::fromBytes(100),
74 | ),
75 | 0
76 | ));
77 |
78 | $this->assertStringContainsString('Not all minimum code coverage rules', $this->output);
79 | $this->assertStringContainsString('passed, please try again... :)', $this->output);
80 | }
81 |
82 | public function testNotifyWithAWarning(): void
83 | {
84 | $this->exitter
85 | ->expects($this->never())
86 | ->method('exit');
87 |
88 | $subscriber = new ApplicationFinishedSubscriber(
89 | pathToCloverXml: 'tests/clover.xml',
90 | minCoverageRules: MinCoverageRules::fromConfigFile('tests/Subscriber/Application/min-coverage-rules-with-warning.php'),
91 | cleanUpCloverXml: false,
92 | exitter: $this->exitter,
93 | consoleOutput: new ConsoleOutput($this->output, $this->resourceUsageFormatter),
94 | timer: $this->timer,
95 | );
96 |
97 | $subscriber->notify(event: new Finished(
98 | new Info(
99 | current: new Snapshot(
100 | time: HRTime::fromSecondsAndNanoseconds(1, 0),
101 | memoryUsage: MemoryUsage::fromBytes(100),
102 | peakMemoryUsage: MemoryUsage::fromBytes(100),
103 | garbageCollectorStatus: new GarbageCollectorStatus(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
104 | ),
105 | durationSinceStart: Duration::fromSecondsAndNanoseconds(1, 0),
106 | memorySinceStart: MemoryUsage::fromBytes(100),
107 | durationSincePrevious: Duration::fromSecondsAndNanoseconds(1, 0),
108 | memorySincePrevious: MemoryUsage::fromBytes(100),
109 | ),
110 | 0
111 | ));
112 |
113 | $this->assertStringContainsString('There was at least one pattern that', $this->output);
114 | $this->assertStringContainsString('did not match any covered classes.', $this->output);
115 | $this->assertStringContainsString('Please consider removing them', $this->output);
116 | }
117 |
118 | public function testNotifyWhenCoverageIsOk(): void
119 | {
120 | $this->exitter
121 | ->expects($this->never())
122 | ->method('exit');
123 |
124 | $subscriber = new ApplicationFinishedSubscriber(
125 | pathToCloverXml: 'tests/clover.xml',
126 | minCoverageRules: MinCoverageRules::fromConfigFile('tests/Subscriber/Application/min-coverage-rules-success.php'),
127 | cleanUpCloverXml: false,
128 | exitter: $this->exitter,
129 | consoleOutput: new ConsoleOutput($this->output, $this->resourceUsageFormatter),
130 | timer: $this->timer,
131 | );
132 |
133 | $subscriber->notify(event: new Finished(
134 | new Info(
135 | current: new Snapshot(
136 | time: HRTime::fromSecondsAndNanoseconds(1, 0),
137 | memoryUsage: MemoryUsage::fromBytes(100),
138 | peakMemoryUsage: MemoryUsage::fromBytes(100),
139 | garbageCollectorStatus: new GarbageCollectorStatus(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
140 | ),
141 | durationSinceStart: Duration::fromSecondsAndNanoseconds(1, 0),
142 | memorySinceStart: MemoryUsage::fromBytes(100),
143 | durationSincePrevious: Duration::fromSecondsAndNanoseconds(1, 0),
144 | memorySincePrevious: MemoryUsage::fromBytes(100),
145 | ),
146 | 0
147 | ));
148 |
149 | $this->assertStringContainsString('All minimum code coverage rules', $this->output);
150 | $this->assertStringContainsString('passed, give yourself a pat on the', $this->output);
151 | }
152 |
153 | public function testNotifyWithOnlyTotal(): void
154 | {
155 | $this->exitter
156 | ->expects($this->never())
157 | ->method('exit');
158 |
159 | $subscriber = new ApplicationFinishedSubscriber(
160 | pathToCloverXml: 'tests/clover.xml',
161 | minCoverageRules: MinCoverageRules::fromConfigFile('tests/Subscriber/Application/min-coverage-rules-total-only.php'),
162 | cleanUpCloverXml: false,
163 | exitter: $this->exitter,
164 | consoleOutput: new ConsoleOutput($this->output, $this->resourceUsageFormatter),
165 | timer: $this->timer,
166 | );
167 |
168 | $subscriber->notify(event: new Finished(
169 | new Info(
170 | current: new Snapshot(
171 | time: HRTime::fromSecondsAndNanoseconds(1, 0),
172 | memoryUsage: MemoryUsage::fromBytes(100),
173 | peakMemoryUsage: MemoryUsage::fromBytes(100),
174 | garbageCollectorStatus: new GarbageCollectorStatus(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
175 | ),
176 | durationSinceStart: Duration::fromSecondsAndNanoseconds(1, 0),
177 | memorySinceStart: MemoryUsage::fromBytes(100),
178 | durationSincePrevious: Duration::fromSecondsAndNanoseconds(1, 0),
179 | memorySincePrevious: MemoryUsage::fromBytes(100),
180 | ),
181 | 0
182 | ));
183 |
184 | $this->assertStringContainsString('Total', $this->output);
185 | $this->assertStringContainsString('All minimum code coverage rules', $this->output);
186 | $this->assertStringContainsString('passed, give yourself a pat on the', $this->output);
187 | }
188 |
189 | public function testNotifyWithoutTotal(): void
190 | {
191 | $this->exitter
192 | ->expects($this->never())
193 | ->method('exit');
194 |
195 | $subscriber = new ApplicationFinishedSubscriber(
196 | pathToCloverXml: 'tests/clover.xml',
197 | minCoverageRules: MinCoverageRules::fromConfigFile('tests/Subscriber/Application/min-coverage-rules-without-total.php'),
198 | cleanUpCloverXml: false,
199 | exitter: $this->exitter,
200 | consoleOutput: new ConsoleOutput($this->output, $this->resourceUsageFormatter),
201 | timer: $this->timer,
202 | );
203 |
204 | $subscriber->notify(event: new Finished(
205 | new Info(
206 | current: new Snapshot(
207 | time: HRTime::fromSecondsAndNanoseconds(1, 0),
208 | memoryUsage: MemoryUsage::fromBytes(100),
209 | peakMemoryUsage: MemoryUsage::fromBytes(100),
210 | garbageCollectorStatus: new GarbageCollectorStatus(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
211 | ),
212 | durationSinceStart: Duration::fromSecondsAndNanoseconds(1, 0),
213 | memorySinceStart: MemoryUsage::fromBytes(100),
214 | durationSincePrevious: Duration::fromSecondsAndNanoseconds(1, 0),
215 | memorySincePrevious: MemoryUsage::fromBytes(100),
216 | ),
217 | 0
218 | ));
219 |
220 | $this->assertStringNotContainsString('Total', $this->output);
221 | $this->assertStringContainsString('All minimum code coverage rules', $this->output);
222 | $this->assertStringContainsString('passed, give yourself a pat on the', $this->output);
223 | }
224 |
225 | public function testNotifyWithRulesThatDoNotExit(): void
226 | {
227 | $this->exitter
228 | ->expects($this->never())
229 | ->method('exit');
230 |
231 | $subscriber = new ApplicationFinishedSubscriber(
232 | pathToCloverXml: 'tests/clover.xml',
233 | minCoverageRules: MinCoverageRules::fromConfigFile('tests/Subscriber/Application/min-coverage-rules-no-exit.php'),
234 | cleanUpCloverXml: false,
235 | exitter: $this->exitter,
236 | consoleOutput: new ConsoleOutput($this->output, $this->resourceUsageFormatter),
237 | timer: $this->timer,
238 | );
239 |
240 | $subscriber->notify(event: new Finished(
241 | new Info(
242 | current: new Snapshot(
243 | time: HRTime::fromSecondsAndNanoseconds(1, 0),
244 | memoryUsage: MemoryUsage::fromBytes(100),
245 | peakMemoryUsage: MemoryUsage::fromBytes(100),
246 | garbageCollectorStatus: new GarbageCollectorStatus(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
247 | ),
248 | durationSinceStart: Duration::fromSecondsAndNanoseconds(1, 0),
249 | memorySinceStart: MemoryUsage::fromBytes(100),
250 | durationSincePrevious: Duration::fromSecondsAndNanoseconds(1, 0),
251 | memorySincePrevious: MemoryUsage::fromBytes(100),
252 | ),
253 | 0
254 | ));
255 |
256 | $this->assertStringContainsString('No', $this->output);
257 | $this->assertStringContainsString('Not all minimum code coverage rules', $this->output);
258 | $this->assertStringContainsString('passed, please try again... :)', $this->output);
259 | }
260 |
261 | public function testDivideByZero(): void
262 | {
263 | $this->exitter
264 | ->expects($this->never())
265 | ->method('exit');
266 |
267 | $subscriber = new ApplicationFinishedSubscriber(
268 | pathToCloverXml: 'tests/clover-test-divide-by-zero.xml',
269 | minCoverageRules: MinCoverageRules::fromInt(100, true),
270 | cleanUpCloverXml: false,
271 | exitter: $this->exitter,
272 | consoleOutput: new ConsoleOutput($this->output, $this->resourceUsageFormatter),
273 | timer: $this->timer,
274 | );
275 |
276 | $subscriber->notify(event: new Finished(
277 | new Info(
278 | current: new Snapshot(
279 | time: HRTime::fromSecondsAndNanoseconds(1, 0),
280 | memoryUsage: MemoryUsage::fromBytes(100),
281 | peakMemoryUsage: MemoryUsage::fromBytes(100),
282 | garbageCollectorStatus: new GarbageCollectorStatus(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
283 | ),
284 | durationSinceStart: Duration::fromSecondsAndNanoseconds(1, 0),
285 | memorySinceStart: MemoryUsage::fromBytes(100),
286 | durationSincePrevious: Duration::fromSecondsAndNanoseconds(1, 0),
287 | memorySincePrevious: MemoryUsage::fromBytes(100),
288 | ),
289 | 0
290 | ));
291 |
292 | $this->assertStringContainsString('There was at least one pattern that', $this->output);
293 | $this->assertStringContainsString('did not match any covered classes.', $this->output);
294 | $this->assertStringContainsString('Please consider removing them', $this->output);
295 | }
296 |
297 | public function testNotifyWhenNoTrackedLines(): void
298 | {
299 | $this->exitter
300 | ->expects($this->never())
301 | ->method('exit');
302 |
303 | $subscriber = new ApplicationFinishedSubscriber(
304 | pathToCloverXml: 'tests/clover-with-no-tracked-lines.xml',
305 | minCoverageRules: MinCoverageRules::fromConfigFile('tests/Subscriber/Application/min-coverage-rules-no-tracked-lines.php'),
306 | cleanUpCloverXml: false,
307 | exitter: $this->exitter,
308 | consoleOutput: new ConsoleOutput($this->output, $this->resourceUsageFormatter),
309 | timer: $this->timer,
310 | );
311 |
312 | $subscriber->notify(event: new Finished(
313 | new Info(
314 | current: new Snapshot(
315 | time: HRTime::fromSecondsAndNanoseconds(1, 0),
316 | memoryUsage: MemoryUsage::fromBytes(100),
317 | peakMemoryUsage: MemoryUsage::fromBytes(100),
318 | garbageCollectorStatus: new GarbageCollectorStatus(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
319 | ),
320 | durationSinceStart: Duration::fromSecondsAndNanoseconds(1, 0),
321 | memorySinceStart: MemoryUsage::fromBytes(100),
322 | durationSincePrevious: Duration::fromSecondsAndNanoseconds(1, 0),
323 | memorySincePrevious: MemoryUsage::fromBytes(100),
324 | ),
325 | 0
326 | ));
327 |
328 | $this->assertStringContainsString('There was at least one pattern that', $this->output);
329 | $this->assertStringContainsString('did not match any covered classes.', $this->output);
330 | $this->assertStringContainsString('Please consider removing them', $this->output);
331 | }
332 |
333 | public function testNotifyWithNonExistingCloverFile(): void
334 | {
335 | $this->exitter
336 | ->expects($this->never())
337 | ->method('exit');
338 |
339 | $subscriber = new ApplicationFinishedSubscriber(
340 | pathToCloverXml: 'tests/clover-wrong.xml',
341 | minCoverageRules: MinCoverageRules::fromConfigFile('tests/Subscriber/Application/min-coverage-rules-success.php'),
342 | cleanUpCloverXml: false,
343 | exitter: $this->exitter,
344 | consoleOutput: new ConsoleOutput($this->output, $this->resourceUsageFormatter),
345 | timer: $this->timer,
346 | );
347 |
348 | $this->expectExceptionMessage('Clover XML file not found at:');
349 | $subscriber->notify(event: new Finished(
350 | new Info(
351 | current: new Snapshot(
352 | time: HRTime::fromSecondsAndNanoseconds(1, 0),
353 | memoryUsage: MemoryUsage::fromBytes(100),
354 | peakMemoryUsage: MemoryUsage::fromBytes(100),
355 | garbageCollectorStatus: new GarbageCollectorStatus(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
356 | ),
357 | durationSinceStart: Duration::fromSecondsAndNanoseconds(1, 0),
358 | memorySinceStart: MemoryUsage::fromBytes(100),
359 | durationSincePrevious: Duration::fromSecondsAndNanoseconds(1, 0),
360 | memorySincePrevious: MemoryUsage::fromBytes(100),
361 | ),
362 | 0
363 | ));
364 | }
365 |
366 | public function testNotifyWithInvalidCloverFile(): void
367 | {
368 | $this->exitter
369 | ->expects($this->never())
370 | ->method('exit');
371 |
372 | $subscriber = new ApplicationFinishedSubscriber(
373 | pathToCloverXml: 'tests/clover-invalid.xml',
374 | minCoverageRules: MinCoverageRules::fromConfigFile('tests/Subscriber/Application/min-coverage-rules-success.php'),
375 | cleanUpCloverXml: false,
376 | exitter: $this->exitter,
377 | consoleOutput: new ConsoleOutput($this->output, $this->resourceUsageFormatter),
378 | timer: $this->timer,
379 | );
380 |
381 | $this->expectException(\RuntimeException::class);
382 | $this->expectExceptionMessage('Could not determine coverage metrics');
383 |
384 | $subscriber->notify(event: new Finished(
385 | new Info(
386 | current: new Snapshot(
387 | time: HRTime::fromSecondsAndNanoseconds(1, 0),
388 | memoryUsage: MemoryUsage::fromBytes(100),
389 | peakMemoryUsage: MemoryUsage::fromBytes(100),
390 | garbageCollectorStatus: new GarbageCollectorStatus(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
391 | ),
392 | durationSinceStart: Duration::fromSecondsAndNanoseconds(1, 0),
393 | memorySinceStart: MemoryUsage::fromBytes(100),
394 | durationSincePrevious: Duration::fromSecondsAndNanoseconds(1, 0),
395 | memorySincePrevious: MemoryUsage::fromBytes(100),
396 | ),
397 | 0
398 | ));
399 | }
400 |
401 | public function testNotifyWithCleanUpCloverFile(): void
402 | {
403 | copy(dirname(__DIR__, 2).'/clover.xml', dirname(__DIR__, 2).'/clover-to-delete.xml');
404 |
405 | $this->exitter
406 | ->expects($this->once())
407 | ->method('exit');
408 |
409 | $subscriber = new ApplicationFinishedSubscriber(
410 | pathToCloverXml: 'tests/clover-to-delete.xml',
411 | minCoverageRules: MinCoverageRules::fromConfigFile('tests/Subscriber/Application/min-coverage-rules-with-failed-rule.php'),
412 | cleanUpCloverXml: true,
413 | exitter: $this->exitter,
414 | consoleOutput: new ConsoleOutput($this->output, $this->resourceUsageFormatter),
415 | timer: $this->timer,
416 | );
417 |
418 | $subscriber->notify(event: new Finished(
419 | new Info(
420 | current: new Snapshot(
421 | time: HRTime::fromSecondsAndNanoseconds(1, 0),
422 | memoryUsage: MemoryUsage::fromBytes(100),
423 | peakMemoryUsage: MemoryUsage::fromBytes(100),
424 | garbageCollectorStatus: new GarbageCollectorStatus(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
425 | ),
426 | durationSinceStart: Duration::fromSecondsAndNanoseconds(1, 0),
427 | memorySinceStart: MemoryUsage::fromBytes(100),
428 | durationSincePrevious: Duration::fromSecondsAndNanoseconds(1, 0),
429 | memorySincePrevious: MemoryUsage::fromBytes(100),
430 | ),
431 | 0
432 | ));
433 |
434 | $this->assertFileDoesNotExist(dirname(__DIR__, 2).'/clover-to-delete.xml');
435 | }
436 |
437 | public function testNotifyWithDuplicatePatterns(): void
438 | {
439 | $this->exitter
440 | ->expects($this->never())
441 | ->method('exit');
442 |
443 | $this->expectException(\RuntimeException::class);
444 | $this->expectExceptionMessage('Make sure all coverage rule patterns are unique');
445 |
446 | new ApplicationFinishedSubscriber(
447 | pathToCloverXml: 'tests/clover.xml',
448 | minCoverageRules: MinCoverageRules::fromConfigFile('tests/Subscriber/Application/min-coverage-rules-with-duplicates.php'),
449 | cleanUpCloverXml: false,
450 | exitter: $this->exitter,
451 | consoleOutput: new ConsoleOutput($this->output, $this->resourceUsageFormatter),
452 | timer: $this->timer,
453 | );
454 | }
455 |
456 | public function testNotifyWithInvalidRules(): void
457 | {
458 | $this->exitter
459 | ->expects($this->never())
460 | ->method('exit');
461 |
462 | $this->expectException(\RuntimeException::class);
463 | $this->expectExceptionMessage('MinCoverage has to be value between 0 and 100. 203 given');
464 |
465 | new ApplicationFinishedSubscriber(
466 | pathToCloverXml: 'tests/clover.xml',
467 | minCoverageRules: MinCoverageRules::fromConfigFile('tests/Subscriber/Application/min-coverage-rules-invalid.php'),
468 | cleanUpCloverXml: false,
469 | exitter: $this->exitter,
470 | consoleOutput: new ConsoleOutput($this->output, $this->resourceUsageFormatter),
471 | timer: $this->timer,
472 | );
473 | }
474 |
475 | public function testNotifyWithInvalidRuleInstances(): void
476 | {
477 | $this->exitter
478 | ->expects($this->never())
479 | ->method('exit');
480 |
481 | $this->expectException(\RuntimeException::class);
482 | $this->expectExceptionMessage('Make sure all coverage rules are of instance RobinIngelbrecht\PHPUnitCoverageTools\MinCoverage\MinCoverageRule');
483 |
484 | new ApplicationFinishedSubscriber(
485 | pathToCloverXml: 'tests/clover.xml',
486 | minCoverageRules: MinCoverageRules::fromConfigFile('tests/Subscriber/Application/min-coverage-invalid-rule-instances.php'),
487 | cleanUpCloverXml: false,
488 | exitter: $this->exitter,
489 | consoleOutput: new ConsoleOutput($this->output, $this->resourceUsageFormatter),
490 | timer: $this->timer,
491 | );
492 | }
493 |
494 | public function testFromConfigurationAndParameters(): void
495 | {
496 | $this->assertEquals(
497 | new ApplicationFinishedSubscriber(
498 | pathToCloverXml: 'tests/clover.xml',
499 | minCoverageRules: MinCoverageRules::fromInt(90, false),
500 | cleanUpCloverXml: true,
501 | exitter: new Exitter(),
502 | consoleOutput: ConsoleOutput::create(),
503 | timer: SystemTimer::create(),
504 | ),
505 | ApplicationFinishedSubscriber::fromConfigurationAndParameters(
506 | (new Builder())->build([
507 | '--coverage-clover=tests/clover.xml',
508 | ]),
509 | ParameterCollection::fromArray([]),
510 | ['--min-coverage=90', '--clean-up-clover-xml']
511 | ),
512 | );
513 | }
514 |
515 | public function testFromConfigurationAndParameters2(): void
516 | {
517 | $this->assertEquals(
518 | new ApplicationFinishedSubscriber(
519 | pathToCloverXml: 'tests/clover.xml',
520 | minCoverageRules: MinCoverageRules::fromInt(90, true),
521 | cleanUpCloverXml: true,
522 | exitter: new Exitter(),
523 | consoleOutput: ConsoleOutput::create(),
524 | timer: SystemTimer::create(),
525 | ),
526 | ApplicationFinishedSubscriber::fromConfigurationAndParameters(
527 | (new Builder())->build([
528 | '--coverage-clover=tests/clover.xml',
529 | ]),
530 | ParameterCollection::fromArray([
531 | 'exitOnLowCoverage' => '1',
532 | ]),
533 | ['--min-coverage=90', '--clean-up-clover-xml']
534 | ),
535 | );
536 | }
537 |
538 | public function testFromConfigurationAndParametersFromFile(): void
539 | {
540 | $this->assertEquals(
541 | expected: new ApplicationFinishedSubscriber(
542 | pathToCloverXml: 'tests/clover.xml',
543 | minCoverageRules: MinCoverageRules::fromConfigFile('tests/Subscriber/Application/min-coverage-rules-success.php'),
544 | cleanUpCloverXml: false,
545 | exitter: new Exitter(),
546 | consoleOutput: ConsoleOutput::create(),
547 | timer: SystemTimer::create(),
548 | ),
549 | actual: ApplicationFinishedSubscriber::fromConfigurationAndParameters(
550 | configuration: (new Builder())->build([
551 | '--coverage-clover=tests/clover.xml',
552 | ]),
553 | parameters: ParameterCollection::fromArray([]),
554 | args: ['--min-coverage="tests/Subscriber/Application/min-coverage-rules-success.php"']
555 | ),
556 | );
557 | }
558 |
559 | public function testFromConfigurationAndParametersWhenInvalidMinCoverage(): void
560 | {
561 | $this->assertNull(
562 | actual: ApplicationFinishedSubscriber::fromConfigurationAndParameters(
563 | configuration: (new Builder())->build([
564 | '--coverage-clover=tests/clover.xml',
565 | ]),
566 | parameters: ParameterCollection::fromArray([]),
567 | args: ['--min-coverage=a-word']
568 | ),
569 | );
570 | }
571 |
572 | public function testFromConfigurationAndParametersWhenCoverageTooHigh(): void
573 | {
574 | $this->assertNull(
575 | actual: ApplicationFinishedSubscriber::fromConfigurationAndParameters(
576 | configuration: (new Builder())->build([
577 | '--coverage-clover=tests/clover.xml',
578 | ]),
579 | parameters: ParameterCollection::fromArray([]),
580 | args: ['--min-coverage=101']
581 | ),
582 | );
583 | }
584 |
585 | public function testFromConfigurationAndParametersWhenRulesAreEmpty(): void
586 | {
587 | $this->assertNull(
588 | ApplicationFinishedSubscriber::fromConfigurationAndParameters(
589 | configuration: (new Builder())->build([
590 | '--coverage-clover=tests/clover.xml',
591 | ]),
592 | parameters: ParameterCollection::fromArray([]),
593 | args: ['--min-coverage="tests/Subscriber/Application/min-coverage-rules-empty.php"']
594 | ),
595 | );
596 | }
597 | }
598 |
--------------------------------------------------------------------------------