Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
429 changes: 393 additions & 36 deletions src/Plugins/Coverage.php

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions src/Plugins/Help.php
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,12 @@ private function getContent(): array
], [
'arg' => '--coverage --only-covered',
'desc' => 'Hide files with 0% coverage from the code coverage report',
], [
'arg' => '--shards-coverage',
'desc' => 'Merge .cov files from .pest/coverage/ and generate a combined coverage report',
], [
'arg' => '--shards-coverage --clean',
'desc' => 'Delete .cov files after generating the combined coverage report',
], ...$content['Code Coverage']];

$content['Mutation Testing'] = [[
Expand Down
9 changes: 8 additions & 1 deletion src/Support/Coverage.php
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,14 @@ public static function report(OutputInterface $output, bool $compact = false, bo
$codeCoverage = require $reportPath;
unlink($reportPath);

// @phpstan-ignore-next-line
return self::render($codeCoverage, $output, $compact, $showOnlyCovered);
}

/**
* Renders the coverage report to the console and returns the total coverage as float.
*/
public static function render(CodeCoverage|array $codeCoverage, OutputInterface $output, bool $compact = false, bool $showOnlyCovered = false): float
{
if (is_array($codeCoverage)) {
$facade = Facade::fromSerializedData($codeCoverage);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,8 @@
--coverage --min Set the minimum required coverage percentage, and fail if not met
--coverage --exactly Set the exact required coverage percentage, and fail if not met
--coverage --only-covered Hide files with 0% coverage from the code coverage report
--shards-coverage Merge .cov files from .pest/coverage/ and generate a combined coverage report
--shards-coverage --clean Delete .cov files after generating the combined coverage report
--coverage-clover [file] Write code coverage report in Clover XML format to file
--coverage-openclover [file] Write code coverage report in OpenClover XML format to file
--coverage-cobertura [file] Write code coverage report in Cobertura XML format to file
Expand Down
169 changes: 99 additions & 70 deletions tests/.snapshots/success.txt

Large diffs are not rendered by default.

42 changes: 41 additions & 1 deletion tests/Features/Coverage.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

use Pest\Plugins\Coverage as CoveragePlugin;
use Pest\Support\Coverage;
use Symfony\Component\Console\Output\BufferedOutput;
use Symfony\Component\Console\Output\ConsoleOutput;

it('has plugin')->assertTrue(class_exists(CoveragePlugin::class));
Expand Down Expand Up @@ -34,7 +35,46 @@
expect($plugin->coverageMin)->toEqual(2.4);
});

it('generates coverage based on file input', function (): void {
it('adds coverage if --exactly exist', function (): void {
$plugin = new CoveragePlugin(new ConsoleOutput);

$plugin->handleArguments(['--exactly=50']);
expect($plugin->coverageExactly)->toEqual(50.0);

$plugin->handleArguments(['--exactly=50.5']);
expect($plugin->coverageExactly)->toEqual(50.5);
});

it('adds coverage if --only-covered exist', function (): void {
$plugin = new CoveragePlugin(new ConsoleOutput);

$plugin->handleArguments(['--only-covered']);
expect($plugin->showOnlyCovered)->toBeTrue();
});

it('routes --coverage-php to .pest/coverage/{n}.cov when --shard is used', function (): void {
$plugin = new CoveragePlugin(new ConsoleOutput);

$arguments = $plugin->handleArguments(['--coverage', '--shard=1/3']);

$phpIdx = array_search('--coverage-php', $arguments, true);
expect($phpIdx)->toBeTrue();

$covPath = $arguments[$phpIdx + 1];
expect($covPath)->toEndWith('.pest'.DIRECTORY_SEPARATOR.'coverage'.DIRECTORY_SEPARATOR.'1.cov');
})->skip(! Coverage::isAvailable() || ! function_exists('xdebug_info') || ! in_array('coverage', xdebug_info('mode'), true), 'Coverage is not available');

it('strips blocked report flags and warns when --shard is used', function (): void {
$output = new BufferedOutput;
$plugin = new CoveragePlugin($output);

$arguments = $plugin->handleArguments(['--coverage', '--shard=1/2', '--coverage-html=out']);

expect($arguments)->not->toContain('--coverage-html=out')
->and($output->fetch())->toContain('WARN');
})->skip(! Coverage::isAvailable() || ! function_exists('xdebug_info') || ! in_array('coverage', xdebug_info('mode'), true), 'Coverage is not available');

it('generates coverage based on file input', function () {
expect(Coverage::getMissingCoverage(new class
{
public function lineCoverageData(): array
Expand Down
207 changes: 203 additions & 4 deletions tests/Plugins/Coverage.php
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
<?php

use Pest\Plugins\Coverage;
use Symfony\Component\Console\Output\BufferedOutput;
use Symfony\Component\Console\Output\NullOutput;

test('compute comparable coverage', function (float $givenValue, float $expectedValue): void {
$output = new NullOutput;

$plugin = new Coverage($output);
test('compute comparable coverage', function (float $givenValue, float $expectedValue) {
$plugin = new Coverage(new NullOutput);

$comparableCoverage = (fn () => $this->computeComparableCoverage($givenValue))->call($plugin);

Expand All @@ -21,3 +20,203 @@
[32.57777771232132, 32.5],
[100.0, 100.0],
]);

test('apply thresholds', function (float $coverage, ?float $min, ?float $exactly, int $expectedExitCode): void {
$output = new BufferedOutput;
$plugin = new Coverage($output);
$plugin->coverageMin = $min ?? 0.0;
$plugin->coverageExactly = $exactly;

$exitCode = (fn () => $this->applyThresholds($coverage))->call($plugin);

expect($exitCode)->toBe($expectedExitCode);

if ($expectedExitCode === 1) {
expect($output->fetch())->toContain('FAIL');
}
})->with([
'min pass' => [91.5, 80.0, null, 0],
'min fail' => [91.5, 95.0, null, 1],
'exactly pass' => [91.5, null, 91.5, 0],
'exactly fail' => [91.5, null, 95.0, 1],
]);

test('strip shard blocked report flags', function (array $args, array $expected, bool $expectWarn): void {
$output = new BufferedOutput;
$plugin = new Coverage($output);

$filtered = (fn () => $this->stripShardBlockedReportFlags($args))->call($plugin);

expect($filtered)->toBe($expected);

$expectWarn
? expect($output->fetch())->toContain('WARN')
: expect($output->fetch())->toBeEmpty();
})->with([
'inline value flag' => [['--coverage-html=out', '--compact'], ['--compact'], true],
'space-separated flag' => [['--compact', '--coverage-clover', 'clover.xml'], ['--compact'], true],
'no blocked flags' => [['--compact', '--stop-on-failure'], ['--compact', '--stop-on-failure'], false],
]);

test('apply thresholds returns 0 when coverage meets min', function (): void {
$plugin = new Coverage(new NullOutput);
$plugin->coverageMin = 80.0;

$exitCode = (fn () => $this->applyThresholds(91.5))->call($plugin);

expect($exitCode)->toBe(0);
});

test('apply thresholds returns 1 and writes FAIL when coverage is below min', function (): void {
$output = new BufferedOutput;
$plugin = new Coverage($output);
$plugin->coverageMin = 95.0;

$exitCode = (fn () => $this->applyThresholds(91.5))->call($plugin);

expect($exitCode)->toBe(1)
->and($output->fetch())->toContain('95.0')->toContain('91.5');
});

test('apply thresholds returns 0 when coverage matches exactly', function (): void {
$plugin = new Coverage(new NullOutput);
$plugin->coverageExactly = 91.5;

$exitCode = (fn () => $this->applyThresholds(91.5))->call($plugin);

expect($exitCode)->toBe(0);
});

test('apply thresholds returns 1 and writes FAIL when coverage does not match exactly', function (): void {
$output = new BufferedOutput;
$plugin = new Coverage($output);
$plugin->coverageExactly = 95.0;

$exitCode = (fn () => $this->applyThresholds(91.5))->call($plugin);

expect($exitCode)->toBe(1)
->and($output->fetch())->toContain('95.0')->toContain('91.5');
});

test('parse threshold options sets coverageMin', function (): void {
$plugin = new Coverage(new NullOutput);

(fn () => $this->parseThresholdOptions(['--min=42.5']))->call($plugin);

expect($plugin->coverageMin)->toBe(42.5);
});

test('parse threshold options sets coverageExactly', function (): void {
$plugin = new Coverage(new NullOutput);

(fn () => $this->parseThresholdOptions(['--exactly=75.0']))->call($plugin);

expect($plugin->coverageExactly)->toBe(75.0);
});

test('parse threshold options sets showOnlyCovered', function (): void {
$plugin = new Coverage(new NullOutput);

(fn () => $this->parseThresholdOptions(['--only-covered']))->call($plugin);

expect($plugin->showOnlyCovered)->toBeTrue();
});

test('parse threshold options ignores unrelated flags', function (): void {
$plugin = new Coverage(new NullOutput);

(fn () => $this->parseThresholdOptions(['--compact', '--verbose']))->call($plugin);

expect($plugin->coverageMin)->toBe(0.0)
->and($plugin->coverageExactly)->toBeNull()
->and($plugin->showOnlyCovered)->toBeFalse();
});

test('detect shard parses equals format', function (): void {
$plugin = new Coverage(new NullOutput);

$result = (fn () => $this->detectShard(['--shard=2/5']))->call($plugin);

expect($result)->toBe([2, 5]);
});

test('detect shard parses space format', function (): void {
$plugin = new Coverage(new NullOutput);

$result = (fn () => $this->detectShard(['--shard', '3/4']))->call($plugin);

expect($result)->toBe([3, 4]);
});

test('detect shard returns null when absent', function (): void {
$plugin = new Coverage(new NullOutput);

$result = (fn () => $this->detectShard(['--compact', '--coverage']))->call($plugin);

expect($result)->toBeNull();
});

test('has shards coverage flag detects --shards-coverage', function (): void {
$plugin = new Coverage(new NullOutput);

expect((fn () => $this->hasShardsCoverageFlag(['--shards-coverage']))->call($plugin))->toBeTrue()
->and((fn () => $this->hasShardsCoverageFlag(['--coverage']))->call($plugin))->toBeFalse();
});

test('pop shards coverage flags removes --shards-coverage and --clean', function (): void {
$plugin = new Coverage(new NullOutput);

$remaining = (fn () => $this->popShardsCoverageFlags(['--shards-coverage', '--min=80', '--clean']))->call($plugin);
$isClean = (fn () => $this->shardsCoverageClean)->call($plugin);

expect($remaining)->toBe(['--min=80'])
->and($isClean)->toBeTrue();
});

test('strip shard blocked report flags removes --coverage-html', function (): void {
$output = new BufferedOutput;
$plugin = new Coverage($output);

$filtered = (fn () => $this->stripShardBlockedReportFlags(['--coverage-html=out', '--compact']))->call($plugin);

expect($filtered)->toBe(['--compact'])
->and($output->fetch())->toContain('WARN');
});

test('strip shard blocked report flags removes --coverage-clover as separate arg', function (): void {
$output = new BufferedOutput;
$plugin = new Coverage($output);

$filtered = (fn () => $this->stripShardBlockedReportFlags(['--compact', '--coverage-clover', 'clover.xml']))->call($plugin);

expect($filtered)->toBe(['--compact'])
->and($output->fetch())->toContain('WARN');
});

test('strip shard blocked report flags keeps non-blocked flags without warning', function (): void {
$output = new BufferedOutput;
$plugin = new Coverage($output);

$filtered = (fn () => $this->stripShardBlockedReportFlags(['--compact', '--stop-on-failure']))->call($plugin);

expect($filtered)->toBe(['--compact', '--stop-on-failure'])
->and($output->fetch())->toBeEmpty();
});

test('getCoverageDir returns path under .pest/coverage', function (): void {
$plugin = new Coverage(new NullOutput);

$path = (fn () => $this->getCoverageDir())->call($plugin);

expect($path)->toEndWith('.pest'.DIRECTORY_SEPARATOR.'coverage');
});

test('mergeAndReportShardsCoverage returns -1 when coverage directory is empty', function (): void {
$output = new BufferedOutput;
$plugin = new Coverage($output);

$result = (fn () => $this->mergeAndReportShardsCoverage())->call($plugin);

expect($result)->toBe(-1.0)
->and($output->fetch())->toContain('ERROR');
});
4 changes: 2 additions & 2 deletions tests/Visual/Parallel.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,13 @@
$file = file_get_contents(__FILE__);
$file = preg_replace(
'/\$expected = \'.*?\';/',
"\$expected = '1 deprecated, 4 warnings, 5 incomplete, 3 notices, 40 todos, 27 skipped, 1487 passed (3228 assertions)';",
"\$expected = '1 deprecated, 4 warnings, 5 incomplete, 3 notices, 40 todos, 97 skipped, 1446 passed (3163 assertions)';",
$file,
);
file_put_contents(__FILE__, $file);
}

$expected = '1 deprecated, 4 warnings, 5 incomplete, 3 notices, 40 todos, 27 skipped, 1487 passed (3228 assertions)';
$expected = '1 deprecated, 4 warnings, 5 incomplete, 3 notices, 40 todos, 97 skipped, 1446 passed (3163 assertions)';

expect($output)
->toContain("Tests: {$expected}")
Expand Down