| Server IP : 35.80.110.71 / Your IP : 216.73.216.221 Web Server : Apache/2.4.58 (Ubuntu) System : Linux ip-172-31-21-44 6.17.0-1019-aws #19~24.04.1-Ubuntu SMP Tue Jun 23 18:53:06 UTC 2026 x86_64 User : ubuntu ( 1000) PHP Version : 8.3.31 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : OFF Directory : /var/www/client-portal-laravel/current/scripts/ |
Upload File : |
#!/usr/bin/env php
<?php
/**
* Coverage Gaps Script
*
* Shows files without 100% line coverage and their uncovered line numbers.
*
* Usage:
* php scripts/coverage-gaps.php app/Models
* php scripts/coverage-gaps.php app/Services/Notifications
* php scripts/coverage-gaps.php --clover=path/to/coverage.xml app/Models
* php scripts/coverage-gaps.php --generate app/Models
*/
// Parse args manually (PHP getopt stops at first non-option)
$positional = [];
$cloverFile = null;
$generate = false;
$showHelp = false;
foreach (array_slice($argv, 1) as $arg) {
if (str_starts_with($arg, '--clover=')) {
$cloverFile = substr($arg, 9);
} elseif ($arg === '--generate') {
$generate = true;
} elseif ($arg === '--help') {
$showHelp = true;
} elseif (! str_starts_with($arg, '-')) {
$positional[] = $arg;
}
}
if ($showHelp || empty($positional)) {
fwrite(STDERR, <<<'USAGE'
Usage: php scripts/coverage-gaps.php [options] <directory>
Options:
--clover=FILE Use existing Clover XML coverage file
--generate Run tests first to generate fresh coverage
--help Show this help
If neither --clover nor --generate is given, the script looks for the
most recent coverage XML in tests/.me/
Examples:
php scripts/coverage-gaps.php app/Models
php scripts/coverage-gaps.php --generate app/Services
php scripts/coverage-gaps.php --clover=tests/.me/coverage.xml app/Models
USAGE);
exit(1);
}
$targetDir = rtrim($positional[0], '/');
$projectRoot = dirname(__DIR__);
if (! is_file("$projectRoot/artisan") && is_file(getcwd().'/artisan')) {
$projectRoot = getcwd();
}
$absoluteTarget = realpath("$projectRoot/$targetDir");
if (! $absoluteTarget || ! is_dir($absoluteTarget)) {
fwrite(STDERR, "Error: Directory '$targetDir' not found.\n");
exit(1);
}
// Determine coverage XML path
$cloverPath = null;
if ($cloverFile !== null) {
$cloverPath = $cloverFile;
if (! str_starts_with($cloverPath, '/')) {
$cloverPath = "$projectRoot/$cloverPath";
}
} elseif ($generate) {
$cloverPath = "$projectRoot/tests/.coverage/.gaps/coverage.xml";
@mkdir(dirname($cloverPath), 0755, true);
fwrite(STDERR, "Generating coverage report...\n");
$cmd = sprintf(
'cd %s && php artisan test --parallel --coverage-clover=%s 2>&1',
escapeshellarg($projectRoot),
escapeshellarg($cloverPath)
);
passthru($cmd, $exitCode);
if ($exitCode !== 0) {
fwrite(STDERR, "Error: Test run failed.\n");
exit(1);
}
fwrite(STDERR, "\n");
} else {
// Find most recent clover XML
$searchDirs = [
"$projectRoot/tests/.me",
];
$candidates = [];
foreach ($searchDirs as $dir) {
if (! is_dir($dir)) {
continue;
}
$it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir));
foreach ($it as $file) {
if ($file->getFilename() === 'coverage.xml') {
$candidates[] = $file->getPathname();
}
}
}
if (! empty($candidates)) {
usort($candidates, fn ($a, $b) => filemtime($b) - filemtime($a));
$cloverPath = $candidates[0];
}
}
if (! $cloverPath || ! file_exists($cloverPath)) {
fwrite(STDERR, "Error: No coverage XML found. Run with --generate or --clover=FILE\n");
exit(1);
}
$age = time() - filemtime($cloverPath);
$ageMin = round($age / 60);
if ($ageMin > 60) {
$ageStr = round($ageMin / 60, 1).'h';
} else {
$ageStr = $ageMin.'m';
}
fwrite(STDERR, "Using: $cloverPath ($ageStr old)\n\n");
// Parse Clover XML
$xml = simplexml_load_file($cloverPath);
if (! $xml) {
fwrite(STDERR, "Error: Failed to parse coverage XML.\n");
exit(1);
}
$results = [];
$perfectCount = 0;
// Process files from both <package><file> and top-level <file> nodes
$fileNodes = [];
foreach ($xml->project->package as $package) {
foreach ($package->file as $file) {
$fileNodes[] = $file;
}
}
foreach ($xml->project->file as $file) {
$fileNodes[] = $file;
}
foreach ($fileNodes as $file) {
$filePath = (string) $file['name'];
if (! str_starts_with($filePath, $absoluteTarget.'/')) {
continue;
}
$uncoveredLines = [];
foreach ($file->line as $line) {
if ((string) $line['type'] === 'stmt' && (int) $line['count'] === 0) {
$uncoveredLines[] = (int) $line['num'];
}
}
$relativePath = ltrim(str_replace($projectRoot, '', $filePath), '/');
if (empty($uncoveredLines)) {
$perfectCount++;
continue;
}
sort($uncoveredLines);
$results[$relativePath] = formatLineRanges($uncoveredLines);
}
if (empty($results) && $perfectCount === 0) {
fwrite(STDERR, "No coverage data found for '$targetDir'.\n");
fwrite(STDERR, "This may mean the directory has no executable lines, or tests don't cover it.\n");
exit(1);
}
// Sort by file path
ksort($results);
// Output
foreach ($results as $file => $lines) {
echo "$file $lines\n";
}
$totalFiles = count($results) + $perfectCount;
fwrite(STDERR, "\n{$perfectCount}/{$totalFiles} files at 100% coverage");
if (! empty($results)) {
fwrite(STDERR, ', '.count($results).' with gaps');
}
fwrite(STDERR, "\n");
exit(empty($results) ? 0 : 1);
/**
* Collapse consecutive line numbers into ranges.
* [1,2,3,5,7,8,9] => "1-3, 5, 7-9"
*/
function formatLineRanges(array $lines): string
{
if (empty($lines)) {
return '';
}
$ranges = [];
$start = $lines[0];
$end = $lines[0];
for ($i = 1; $i < count($lines); $i++) {
if ($lines[$i] === $end + 1) {
$end = $lines[$i];
} else {
$ranges[] = $start === $end ? (string) $start : "$start-$end";
$start = $lines[$i];
$end = $lines[$i];
}
}
$ranges[] = $start === $end ? (string) $start : "$start-$end";
return implode(', ', $ranges);
}