Skip to content

Commit bde983e

Browse files
committed
Initial commit of working command line app
0 parents  commit bde983e

File tree

5 files changed

+230
-0
lines changed

5 files changed

+230
-0
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
.idea/
2+
vendor/
3+
composer.lock
4+
covpf.phar

composer.json

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"name": "cooperaj/php-coverage-path-fixer",
3+
"description": "Manipulates .cov files path information when source files have moved or are in a different location",
4+
"minimum-stability": "stable",
5+
"license": "MIT",
6+
"authors": [
7+
{
8+
"name": "Adam Cooper",
9+
"email": "[email protected]"
10+
}
11+
],
12+
13+
"require": {
14+
"phpunit/php-code-coverage": "^8.0.0",
15+
"symfony/console": "^v5.0.4"
16+
},
17+
"require-dev": {
18+
"phpunit/phpunit": "9.0.1"
19+
},
20+
"autoload": {
21+
"psr-4": {
22+
"CoveragePathFixer\\": "src/"
23+
}
24+
},
25+
"bin": [ "covpf" ]
26+
}

covpf

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
#!/usr/bin/env php
2+
<?php
3+
4+
declare(strict_types=1);
5+
6+
use CoveragePathFixer\Application;
7+
8+
include 'vendor/autoload.php';
9+
10+
$application = new Application();
11+
$application->run();

src/Application.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace CoveragePathFixer;
6+
7+
use CoveragePathFixer\Command\ChangePathPrefix;
8+
use Symfony\Component\Console\Application as ConsoleApplication;
9+
10+
class Application extends ConsoleApplication
11+
{
12+
public function __construct(string $name = 'CoveragePathFixer', string $version = 'UNKNOWN')
13+
{
14+
parent::__construct($name, $version);
15+
16+
$command = new ChangePathPrefix();
17+
$this->add($command);
18+
$this->setDefaultCommand($command->getName(), true);
19+
}
20+
}

src/Command/ChangePathPrefix.php

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace CoveragePathFixer\Command;
6+
7+
use RecursiveDirectoryIterator;
8+
use RecursiveIteratorIterator;
9+
use RecursiveRegexIterator;
10+
use RegexIterator;
11+
use SebastianBergmann\CodeCoverage\CodeCoverage;
12+
use SebastianBergmann\CodeCoverage\Filter;
13+
use SebastianBergmann\CodeCoverage\Report\Clover;
14+
use SebastianBergmann\CodeCoverage\Report\PHP;
15+
use Symfony\Component\Console\Command\Command;
16+
use Symfony\Component\Console\Input\InputArgument;
17+
use Symfony\Component\Console\Input\InputInterface;
18+
use Symfony\Component\Console\Input\InputOption;
19+
use Symfony\Component\Console\Output\OutputInterface;
20+
21+
class ChangePathPrefix extends Command
22+
{
23+
protected static $defaultName = 'fix';
24+
25+
protected function configure(): void
26+
{
27+
$this->setDescription('Swaps a given path prefix with another in your coverage files')
28+
->setHelp(
29+
'This command will recursively search for coverage (.cov) files and alter the code ' .
30+
'paths held within such that the files can be found. \n\n' .
31+
'e.g. "/app/src" can be swapped with e.g. "/home/ci/src"'
32+
)
33+
34+
->addArgument(
35+
'directory_to_search',
36+
InputArgument::REQUIRED,
37+
'The directory to recursively search for ".cov" files',
38+
)
39+
40+
->addArgument(
41+
'original_prefix',
42+
InputArgument::REQUIRED,
43+
'The orginal path prefix e.g. "/app/src"'
44+
)
45+
46+
->addArgument(
47+
'replacement_prefix',
48+
InputArgument::REQUIRED,
49+
'The new path prefix e.g. "/home/ci/src"'
50+
)
51+
52+
->addOption(
53+
'merge',
54+
'm',
55+
InputOption::VALUE_REQUIRED,
56+
'Merge the discovered files into the specified single output coverage (.cov) file'
57+
)
58+
59+
->addOption(
60+
'clover',
61+
'c',
62+
InputOption::VALUE_NONE,
63+
'Output an additional clover format coverage file alongside each .cov file that is processed'
64+
);
65+
}
66+
67+
protected function execute(InputInterface $input, OutputInterface $output): int
68+
{
69+
try {
70+
$files = $this->findCoverageFiles($input->getArgument('directory_to_search'));
71+
72+
$files = $this->iterateCoverageFiles(
73+
$files,
74+
$input->getArgument('original_prefix'),
75+
$input->getArgument('replacement_prefix')
76+
);
77+
78+
if ($path = $input->getOption('merge')) {
79+
$files = $this->mergeCoverageFiles($files, $path);
80+
}
81+
82+
$this->outputCoverageFiles($files, $input->getOption('clover'));
83+
} catch (\Exception $ex) {
84+
return $ex->getCode();
85+
}
86+
87+
return 0;
88+
}
89+
90+
private function changePrefix(array $data, string $originalPrefix, string $replacementPrefix): array
91+
{
92+
return array_combine(array_map(function($el) use ($originalPrefix, $replacementPrefix) {
93+
$el = preg_replace('#^' . $originalPrefix . '#', $replacementPrefix, $el);
94+
return $el;
95+
}, array_keys($data)), array_values($data));
96+
}
97+
98+
private function iterateCoverageFiles(array $files, string $originalPrefix, string $replacementPrefix): array
99+
{
100+
return array_map(function(array $file) use ($originalPrefix, $replacementPrefix) {
101+
$coverage = $this->loadCoverageFile($file[0]);
102+
103+
$data = $this->changePrefix($coverage->getData(), $originalPrefix, $replacementPrefix);
104+
$whiteList = $this->changePrefix(
105+
$coverage->filter()->getWhitelistedFiles(),
106+
$originalPrefix,
107+
$replacementPrefix
108+
);
109+
110+
$filter = new Filter();
111+
$filter->setWhitelistedFiles($whiteList);
112+
113+
$coverage = new CodeCoverage(null, $filter);
114+
$coverage->setData($data);
115+
116+
return $coverage;
117+
}, $files);
118+
}
119+
120+
private function findCoverageFiles(string $directory): array
121+
{
122+
$path = realpath($directory);
123+
124+
$directory = new RecursiveDirectoryIterator($path);
125+
$iterator = new RecursiveIteratorIterator($directory);
126+
$filtered = new RegexIterator($iterator, '/^.+\.cov$/i', RecursiveRegexIterator::GET_MATCH);
127+
128+
return iterator_to_array($filtered);
129+
}
130+
131+
private function loadCoverageFile(string $file): CodeCoverage
132+
{
133+
$coverage = include $file;
134+
135+
if (!($coverage instanceof CodeCoverage)) {
136+
unset($coverage);
137+
throw new \Exception('File with coverage extension not resolved to CodeCoverage class');
138+
}
139+
140+
return $coverage;
141+
}
142+
143+
private function mergeCoverageFiles(array $files, string $path): array
144+
{
145+
$coverage = new CodeCoverage();
146+
147+
foreach ($files as $file => $coverage) {
148+
$coverage->merge($coverage);
149+
}
150+
151+
return [$path => $coverage];
152+
}
153+
154+
private function outputCoverageFiles(array $files, bool $asClover = false): void
155+
{
156+
array_walk($files, function($coverage, $path) use ($asClover) {
157+
if ($asClover) {
158+
$filename = basename($path, '.cov');
159+
$directory = dirname($path);
160+
161+
$reportWriter = new Clover();
162+
$reportWriter->process($coverage, $directory . DIRECTORY_SEPARATOR . $filename . '.xml');
163+
}
164+
165+
$reportWriter = new PHP();
166+
$reportWriter->process($coverage, $path);
167+
});
168+
}
169+
}

0 commit comments

Comments
 (0)