Skip to content

Commit b36aa02

Browse files
committed
Add BadgeComposer class and update existing files
Implemented BadgeComposer class to generate coverage badges based on multiple input files and calculate the total coverage. Furthermore, refactored coverage-badge script, created tests, updated README with new usage instructions, and made minor amendments in composer.json and phpunit.xml.
1 parent ed60cfe commit b36aa02

File tree

9 files changed

+400
-58
lines changed

9 files changed

+400
-58
lines changed

Includes/BadgeComposer.php

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
<?php
2+
3+
namespace BadgeManager\Includes;
4+
5+
use Exception;
6+
7+
/**
8+
* Class BadgeComposer
9+
*
10+
* This class is responsible for generating a coverage badge based on the input files provided.
11+
*
12+
* @package BadgeManager
13+
*/
14+
class BadgeComposer
15+
{
16+
private array $inputFiles;
17+
private string $outputFile;
18+
private string $coverageName;
19+
public int $totalCoverage = 0;
20+
private int $totalElements = 0;
21+
private int $checkedElements = 0;
22+
23+
/**
24+
* @throws Exception
25+
*/
26+
public function __construct(string $inputFiles, string $outputFile, string $coverageName = 'coverage')
27+
{
28+
$this->inputFiles = explode(',', $inputFiles);
29+
$this->outputFile = $outputFile;
30+
$this->coverageName = $coverageName;
31+
32+
$this->validateFiles($this->inputFiles, $this->outputFile);
33+
}
34+
35+
/**
36+
* Validates the input files and output file.
37+
*
38+
* This method checks if the input files exist and if the output file is provided.
39+
*
40+
* @param array $inputFiles The array of input files to validate.
41+
* @param string $outputFile The output file to validate.
42+
*
43+
* @return void
44+
* @throws Exception If any of the input files do not exist or if the output file is not provided.
45+
*/
46+
private function validateFiles(array $inputFiles, string $outputFile): void
47+
{
48+
foreach ($inputFiles as $inputFile) {
49+
if (!file_exists($inputFile)) {
50+
throw new \Exception("input file does not exist: " . $inputFile);
51+
}
52+
}
53+
54+
if (empty($outputFile)) {
55+
throw new \Exception("output file name is mandatory");
56+
}
57+
}
58+
59+
/**
60+
* Runs the coverage process for each input file.
61+
*
62+
* This method iterates over the input files array and processes each file using the `processFile` method.
63+
* After processing all input files, the coverage is finalized using the `finalizeCoverage` method.
64+
*
65+
* @return void
66+
* @throws Exception
67+
*/
68+
public function run(): void
69+
{
70+
foreach ($this->inputFiles as $inputFile) {
71+
$this->processFile($inputFile);
72+
}
73+
$this->finalizeCoverage();
74+
}
75+
76+
/**
77+
* Process a file and update the total and checked elements count.
78+
*
79+
* @param string $inputFile The path to the XML file to process.
80+
*
81+
* @return void
82+
* @throws Exception When there is an error processing the file.
83+
*
84+
*/
85+
private function processFile(string $inputFile): void
86+
{
87+
try {
88+
$xml = new \SimpleXMLElement(file_get_contents($inputFile));
89+
$metrics = $xml->xpath('//metrics');
90+
foreach ($metrics as $metric) {
91+
$this->totalElements += (int) $metric['elements'];
92+
$this->checkedElements += (int) $metric['coveredelements'];
93+
}
94+
95+
$coverage = round(($this->totalElements === 0) ? 0 : ($this->checkedElements / $this->totalElements) * 100);
96+
$this->totalCoverage += $coverage;
97+
} catch (Exception $e) {
98+
throw new Exception('Error processing file: ' . $inputFile);
99+
}
100+
}
101+
102+
/**
103+
* Finalize the coverage report by generating a badge with the average coverage across all input files.
104+
*
105+
* @return void
106+
* @throws Exception If there is an error generating the badge.
107+
*/
108+
private function finalizeCoverage(): void
109+
{
110+
$totalCoverage = $this->totalCoverage / count($this->inputFiles); // Average coverage across all files
111+
$template = file_get_contents(__DIR__ . '/../badge-templates/badge.svg');
112+
113+
$template = str_replace('{{ total }}', $totalCoverage, $template);
114+
115+
$template = str_replace('{{ coverage }}', $this->coverageName, $template);
116+
117+
$color = '#a4a61d'; // Yellow-Green
118+
if ($totalCoverage < 40) {
119+
$color = '#e05d44'; // Red
120+
} elseif ($totalCoverage < 60) {
121+
$color = '#fe7d37'; // Orange
122+
} elseif ($totalCoverage < 75) {
123+
$color = '#dfb317'; // Yellow
124+
} elseif ($totalCoverage < 95) {
125+
$color = '#97CA00'; // Green
126+
} elseif ($totalCoverage <= 100) {
127+
$color = '#4c1'; // Bright Green
128+
}
129+
130+
$template = str_replace('{{ total }}', $totalCoverage, $template);
131+
$template = str_replace('{{ color }}', $color, $template);
132+
133+
file_put_contents($this->outputFile, $template);
134+
}
135+
}

README.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,14 @@ Composer
88

99
`composer require codebtech/coveragebadge --dev`
1010

11+
## Features
12+
- Generate coverage badge from PHPUnit Clover XML file
13+
- Generate coverage badge based on multiple Clover XML files, and it merges the coverage percentage automatically
14+
- Can accept coverage name and the badge will be generated with the coverage name
15+
1116

1217
## Usage
1318

14-
1. Generate [XML Code Coverage](https://phpunit.de/manual/current/en/logging.html#logging.codecoverage.xml) using [PHPUnit](https://phpunit.de/manual/current/en/appendixes.configuration.html#appendixes.configuration.logging)
15-
2. Run `vendor/bin/coverage-badge /path/to/clover.xml /path/to/badge/destination.svg type_of_test`
16-
* e.g. `vendor/bin/php-coverage-badge build/clover.xml report/coverage.svg unit-test`
19+
1. Generate [XML Code Coverage](https://docs.phpunit.de/en/11.1/code-coverage.html) and generate [Clover](https://docs.phpunit.de/en/11.1/configuration.html#the-report-element) XML files.
20+
2. Run `vendor/bin/coverage-badge /path/to/clover.xml /path/to/badge/destination.svg test-name`
21+
3. To merge multiple clover files provide the input XML files `comma` separated run `vendor/bin/coverage-badge /path/to/clover.xml,/path/to/clover2.xml /path/to/badge/destination.svg test-name`

Tests/BadgeComposerTest.php

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
<?php
2+
3+
namespace BadgeManager\Tests;
4+
5+
use Exception;
6+
use ReflectionException;
7+
use PHPUnit\Framework\TestCase;
8+
use BadgeManager\Includes\BadgeComposer;
9+
10+
class BadgeComposerTest extends TestCase
11+
{
12+
private BadgeComposer $badgeComposer;
13+
14+
private string $inputFile = __DIR__ . "/test-input1.xml";
15+
private string $inputFile2 = __DIR__ . "/test-input2.xml";
16+
private string $outputFile = "output.svg";
17+
private string $coverageName = "unit";
18+
19+
/**
20+
* @throws Exception
21+
*/
22+
public function setUp(): void
23+
{
24+
$this->badgeComposer = new BadgeComposer($this->inputFile, $this->outputFile, $this->coverageName);
25+
}
26+
27+
/**
28+
* @throws ReflectionException
29+
*/
30+
public function validateFiles(array $files, string $output)
31+
{
32+
$method = (new \ReflectionClass($this->badgeComposer))->getMethod('validateFiles');
33+
34+
return $method->invoke($this->badgeComposer, $files, $output);
35+
}
36+
37+
/**
38+
* Check if an exception is thrown when trying to validate files that do not exist.
39+
* @throws ReflectionException
40+
*/
41+
public function testErrorIsThrownWhenInputFileDoesNotExist(): void
42+
{
43+
$this->expectException(\Exception::class);
44+
$this->expectExceptionMessage('input file does not exist: file_does_not_exist.xml');
45+
46+
$this->validateFiles(
47+
["file_does_not_exist.xml"],
48+
"output.svg"
49+
);
50+
}
51+
52+
/**
53+
* Check if an exception is thrown when the output file is not specified.
54+
* @throws ReflectionException
55+
*/
56+
public function testErrorIsThrownWhenOutputFileDoesNotExist(): void
57+
{
58+
$this->expectException(\Exception::class);
59+
$this->expectExceptionMessage('output file name is mandatory');
60+
61+
$this->validateFiles(
62+
[__DIR__ . "/test-input1.xml"],
63+
"" // Empty name to simulate missing output file
64+
);
65+
}
66+
67+
/**
68+
* @throws ReflectionException
69+
*/
70+
public function processFile(string $inputFile)
71+
{
72+
$method = (new \ReflectionClass($this->badgeComposer))->getMethod('processFile');
73+
74+
return $method->invoke($this->badgeComposer, $inputFile);
75+
}
76+
77+
/**
78+
* @throws ReflectionException
79+
*/
80+
public function testProcessTheCloverFileAndCalculateTheCoverage(): void
81+
{
82+
$this->processFile($this->inputFile);
83+
84+
$this->assertEquals(51, $this->badgeComposer->totalCoverage);
85+
}
86+
87+
/**
88+
* @throws ReflectionException
89+
*/
90+
public function testProcessMultipleCloverFilesAndCalculateTheCoverage(): void
91+
{
92+
$this->processFile($this->inputFile);
93+
$this->processFile($this->inputFile2);
94+
95+
$this->assertEquals(94, $this->badgeComposer->totalCoverage);
96+
}
97+
}

Tests/test-input1.xml

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<coverage generated="1717008513">
3+
<project timestamp="1717008513">
4+
<file name="/var/www/html/wp-content/plugins/nuk-wp-block-plugin-template/inc/Base/ScriptsEnqueue.php">
5+
<class name="NUK\WP\Inc\Base\ScriptsEnqueue" namespace="global">
6+
<metrics complexity="2" methods="2" coveredmethods="0" conditionals="0" coveredconditionals="0" statements="10" coveredstatements="0" elements="12" coveredelements="12"/>
7+
</class>
8+
<line num="28" type="method" name="register" visibility="public" complexity="1" crap="2" count="0"/>
9+
<line num="29" type="stmt" count="0"/>
10+
<line num="37" type="method" name="enqueue_admin_scripts" visibility="public" complexity="1" crap="2" count="0"/>
11+
<line num="38" type="stmt" count="0"/>
12+
<line num="39" type="stmt" count="0"/>
13+
<line num="41" type="stmt" count="0"/>
14+
<line num="42" type="stmt" count="0"/>
15+
<line num="43" type="stmt" count="0"/>
16+
<line num="44" type="stmt" count="0"/>
17+
<line num="45" type="stmt" count="0"/>
18+
<line num="46" type="stmt" count="0"/>
19+
<line num="47" type="stmt" count="0"/>
20+
<metrics loc="50" ncloc="27" classes="1" methods="2" coveredmethods="0" conditionals="0" coveredconditionals="0" statements="10" coveredstatements="0" elements="12" coveredelements="4"/>
21+
</file>
22+
<file name="/var/www/html/wp-content/plugins/nuk-wp-block-plugin-template/inc/Init.php">
23+
<class name="NUK\WP\Inc\Init" namespace="global">
24+
<metrics complexity="9" methods="5" coveredmethods="0" conditionals="0" coveredconditionals="0" statements="15" coveredstatements="0" elements="20" coveredelements="12"/>
25+
</class>
26+
<line num="32" type="method" name="get_services" visibility="public" complexity="1" crap="2" count="0"/>
27+
<line num="33" type="stmt" count="0"/>
28+
<line num="34" type="stmt" count="0"/>
29+
<line num="35" type="stmt" count="0"/>
30+
<line num="43" type="method" name="register_services" visibility="public" complexity="2" crap="6" count="0"/>
31+
<line num="44" type="stmt" count="0"/>
32+
<line num="45" type="stmt" count="0"/>
33+
<line num="46" type="stmt" count="0"/>
34+
<line num="55" type="method" name="deactivate" visibility="public" complexity="3" crap="12" count="0"/>
35+
<line num="56" type="stmt" count="0"/>
36+
<line num="57" type="stmt" count="0"/>
37+
<line num="58" type="stmt" count="0"/>
38+
<line num="59" type="stmt" count="0"/>
39+
<line num="71" type="method" name="register" visibility="private" complexity="1" crap="2" count="0"/>
40+
<line num="72" type="stmt" count="0"/>
41+
<line num="83" type="method" name="instantiate" visibility="private" complexity="2" crap="6" count="0"/>
42+
<line num="84" type="stmt" count="0"/>
43+
<line num="86" type="stmt" count="0"/>
44+
<line num="88" type="stmt" count="0"/>
45+
<line num="91" type="stmt" count="0"/>
46+
<metrics loc="94" ncloc="50" classes="1" methods="5" coveredmethods="0" conditionals="0" coveredconditionals="0" statements="15" coveredstatements="0" elements="20" coveredelements="5"/>
47+
</file>
48+
<file name="/var/www/html/wp-content/plugins/nuk-wp-block-plugin-template/inc/Interfaces/Registrable.php">
49+
<metrics loc="29" ncloc="11" classes="0" methods="0" coveredmethods="0" conditionals="0" coveredconditionals="0" statements="0" coveredstatements="0" elements="0" coveredelements="4"/>
50+
</file>
51+
<metrics files="3" loc="173" ncloc="88" classes="2" methods="7" coveredmethods="0" conditionals="0" coveredconditionals="0" statements="25" coveredstatements="0" elements="32" coveredelements="12"/>
52+
</project>
53+
</coverage>

Tests/test-input2.xml

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<coverage generated="1717008513">
3+
<project timestamp="1717008513">
4+
<file name="/var/www/html/wp-content/plugins/nuk-wp-block-plugin-template/inc/Base/ScriptsEnqueue.php">
5+
<class name="NUK\WP\Inc\Base\ScriptsEnqueue" namespace="global">
6+
<metrics complexity="2" methods="2" coveredmethods="0" conditionals="0" coveredconditionals="0" statements="10" coveredstatements="0" elements="12" coveredelements="33"/>
7+
</class>
8+
<line num="28" type="method" name="register" visibility="public" complexity="1" crap="2" count="0"/>
9+
<line num="29" type="stmt" count="0"/>
10+
<line num="37" type="method" name="enqueue_admin_scripts" visibility="public" complexity="1" crap="2" count="0"/>
11+
<line num="38" type="stmt" count="0"/>
12+
<line num="39" type="stmt" count="0"/>
13+
<line num="41" type="stmt" count="0"/>
14+
<line num="42" type="stmt" count="0"/>
15+
<line num="43" type="stmt" count="0"/>
16+
<line num="44" type="stmt" count="0"/>
17+
<line num="45" type="stmt" count="0"/>
18+
<line num="46" type="stmt" count="0"/>
19+
<line num="47" type="stmt" count="0"/>
20+
<metrics loc="50" ncloc="27" classes="1" methods="2" coveredmethods="0" conditionals="0" coveredconditionals="0" statements="10" coveredstatements="0" elements="12" coveredelements="0"/>
21+
</file>
22+
<file name="/var/www/html/wp-content/plugins/nuk-wp-block-plugin-template/inc/Init.php">
23+
<class name="NUK\WP\Inc\Init" namespace="global">
24+
<metrics complexity="9" methods="5" coveredmethods="0" conditionals="0" coveredconditionals="0" statements="15" coveredstatements="0" elements="20" coveredelements="0"/>
25+
</class>
26+
<line num="32" type="method" name="get_services" visibility="public" complexity="1" crap="2" count="0"/>
27+
<line num="33" type="stmt" count="0"/>
28+
<line num="34" type="stmt" count="0"/>
29+
<line num="35" type="stmt" count="0"/>
30+
<line num="43" type="method" name="register_services" visibility="public" complexity="2" crap="6" count="0"/>
31+
<line num="44" type="stmt" count="0"/>
32+
<line num="45" type="stmt" count="0"/>
33+
<line num="46" type="stmt" count="0"/>
34+
<line num="55" type="method" name="deactivate" visibility="public" complexity="3" crap="12" count="0"/>
35+
<line num="56" type="stmt" count="0"/>
36+
<line num="57" type="stmt" count="0"/>
37+
<line num="58" type="stmt" count="0"/>
38+
<line num="59" type="stmt" count="0"/>
39+
<line num="71" type="method" name="register" visibility="private" complexity="1" crap="2" count="0"/>
40+
<line num="72" type="stmt" count="0"/>
41+
<line num="83" type="method" name="instantiate" visibility="private" complexity="2" crap="6" count="0"/>
42+
<line num="84" type="stmt" count="0"/>
43+
<line num="86" type="stmt" count="0"/>
44+
<line num="88" type="stmt" count="0"/>
45+
<line num="91" type="stmt" count="0"/>
46+
<metrics loc="94" ncloc="50" classes="1" methods="5" coveredmethods="0" conditionals="0" coveredconditionals="0" statements="15" coveredstatements="0" elements="20" coveredelements="0"/>
47+
</file>
48+
<file name="/var/www/html/wp-content/plugins/nuk-wp-block-plugin-template/inc/Interfaces/Registrable.php">
49+
<metrics loc="29" ncloc="11" classes="0" methods="0" coveredmethods="0" conditionals="0" coveredconditionals="0" statements="0" coveredstatements="0" elements="0" coveredelements="0"/>
50+
</file>
51+
<metrics files="3" loc="173" ncloc="88" classes="2" methods="7" coveredmethods="0" conditionals="0" coveredconditionals="0" statements="25" coveredstatements="0" elements="32" coveredelements="0"/>
52+
</project>
53+
</coverage>
File renamed without changes.

composer.json

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "codebtech/coveragebadge",
3-
"description": "Creates code coverage badge from a Clover XML file.",
3+
"description": "Creates code coverage badge from Clover XML files.",
44
"type": "library",
55
"license": "MIT",
66
"homepage": "https://github.com/codebtech/coveragebadge",
@@ -10,7 +10,26 @@
1010
"homepage": "https://github.com/m0hanraj"
1111
}
1212
],
13+
"autoload": {
14+
"psr-4": {
15+
"BadgeManager\\": [""]
16+
}
17+
},
18+
"autoload-dev": {
19+
"psr-4": {
20+
"BadgeManager\\Tests\\": "Tests/"
21+
}
22+
},
1323
"bin": [
1424
"bin/coverage-badge"
15-
]
25+
],
26+
"require-dev": {
27+
"squizlabs/php_codesniffer": "^3.10",
28+
"phpunit/phpunit": "^11.1"
29+
},
30+
"scripts": {
31+
"lint": "phpcs --ignore=/vendor/* --standard=PSR12 .",
32+
"lint:fix": "phpcbf --ignore=/vendor/* --standard=PSR12 .",
33+
"test": "phpunit --testdox"
34+
}
1635
}

0 commit comments

Comments
 (0)