├── CHANGELOG.md ├── bin └── coverage-check ├── src ├── Configuration │ ├── ConfigurationFactory.php │ └── Configuration.php ├── CoverageChecker.php └── Commands │ └── CheckCoverageCommand.php ├── LICENSE.md ├── .php_cs.dist.php ├── composer.json └── README.md /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `coverage-check` will be documented in this file. 4 | 5 | --- 6 | 7 | ## 1.2.1 - 2022-03-12 8 | 9 | - add support for Symfony v6 10 | - 11 | ## 1.2.0 - 2021-07-23 12 | 13 | - add `--precision/-p` option flag to allow the precision used when calculating the code coverage percentage to be specified 14 | 15 | ## 1.1.0 - 2021-07-04 16 | 17 | - add `--metric/-m` option flag 18 | 19 | ## 1.0.0 - 2021-07-03 20 | 21 | - initial release 22 | 23 | -------------------------------------------------------------------------------- /bin/coverage-check: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | add(new CheckCoverageCommand()); 17 | $application->setDefaultCommand('check', true); 18 | $application->run(); 19 | -------------------------------------------------------------------------------- /src/Configuration/ConfigurationFactory.php: -------------------------------------------------------------------------------- 1 | getArgument('filename'); 12 | $requireMode = $input->hasOption('require') && $input->getOption('require') !== null; 13 | $percentage = $requireMode ? (float)$input->getOption('require') : null; 14 | $displayCoverageOnly = $input->hasOption('coverage-only') && $input->getOption('coverage-only') === true; 15 | $precision = $input->hasOption('precision') ? (int)$input->getOption('precision') : 4; 16 | $metricField = 'element'; 17 | 18 | if ($input->hasOption('metric') && $input->getOption('metric') !== null) { 19 | $metricField = $input->getOption('metric'); 20 | } 21 | 22 | return new Configuration($filename, $requireMode, $percentage, $displayCoverageOnly, $metricField, $precision); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Permafrost Software LLC 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /.php_cs.dist.php: -------------------------------------------------------------------------------- 1 | in([ 5 | __DIR__ . '/src', 6 | __DIR__ . '/tests', 7 | ]) 8 | ->name('*.php') 9 | ->notName('*.blade.php') 10 | ->ignoreDotFiles(true) 11 | ->ignoreVCS(true); 12 | 13 | return (new PhpCsFixer\Config()) 14 | ->setRules([ 15 | '@PSR2' => true, 16 | 'array_syntax' => ['syntax' => 'short'], 17 | 'ordered_imports' => ['sort_algorithm' => 'alpha'], 18 | 'no_unused_imports' => true, 19 | 'not_operator_with_successor_space' => true, 20 | 'trailing_comma_in_multiline' => true, 21 | 'phpdoc_scalar' => true, 22 | 'unary_operator_spaces' => true, 23 | 'binary_operator_spaces' => true, 24 | 'blank_line_before_statement' => [ 25 | 'statements' => ['break', 'continue', 'declare', 'return', 'throw', 'try'], 26 | ], 27 | 'phpdoc_single_line_var_spacing' => true, 28 | 'phpdoc_var_without_name' => true, 29 | 'class_attributes_separation' => [ 30 | 'elements' => [ 31 | 'method' => 'one', 32 | ], 33 | ], 34 | 'method_argument_space' => [ 35 | 'on_multiline' => 'ensure_fully_multiline', 36 | 'keep_multiple_spaces_after_comma' => true, 37 | ], 38 | 'single_trait_insert_per_statement' => true, 39 | ]) 40 | ->setFinder($finder); 41 | -------------------------------------------------------------------------------- /src/Configuration/Configuration.php: -------------------------------------------------------------------------------- 1 | filename = $filename; 28 | $this->required = $required; 29 | $this->requireMode = $requireMode; 30 | $this->displayCoverageOnly = $displayCoverageOnly; 31 | $this->metricField = rtrim(strtolower($metricField), 's'); 32 | $this->precision = $precision; 33 | } 34 | 35 | public function validate(): void 36 | { 37 | if (! file_exists($this->filename) || ! is_file($this->filename)) { 38 | throw new \InvalidArgumentException('Invalid input file provided.'); 39 | } 40 | 41 | if (! in_array($this->metricField, ['element', 'statement', 'method'])) { 42 | throw new \InvalidArgumentException('Invalid metric field name provided. Valid options are "element", "statement", "method".'); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/CoverageChecker.php: -------------------------------------------------------------------------------- 1 | config = $config; 19 | $this->xml = new SimpleXMLElement(file_get_contents($filename)); 20 | } 21 | 22 | protected function getMetricFieldSum(array $metrics, string $propertyName): int 23 | { 24 | $result = 0; 25 | 26 | foreach ($metrics as $metric) { 27 | $result += (int)$metric[$propertyName]; 28 | } 29 | 30 | return $result; 31 | } 32 | 33 | public function getCoveragePercent(): float 34 | { 35 | $metrics = $this->xml->xpath('//metrics'); 36 | 37 | [$totalName, $coveredName] = $this->getMetricFieldNames(); 38 | 39 | $totalElements = $this->getMetricFieldSum($metrics, $totalName); 40 | $checkedElements = $this->getMetricFieldSum($metrics, $coveredName); 41 | 42 | return round(($checkedElements / $totalElements) * 100, $this->config->precision); 43 | } 44 | 45 | public function check(float $minPercentage): bool 46 | { 47 | return $this->getCoveragePercent() >= $minPercentage; 48 | } 49 | 50 | protected function getMetricFieldNames(): array 51 | { 52 | return ["{$this->config->metricField}s", "covered{$this->config->metricField}s"]; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "permafrost-dev/coverage-check", 3 | "type": "library", 4 | "description": "Check a project's code coverage, optionally enforcing a minimum value", 5 | "keywords": [ 6 | "permafrost", 7 | "coverage", 8 | "code-coverage", 9 | "code-quality", 10 | "automation" 11 | ], 12 | "homepage": "https://github.com/permafrost-dev/coverage-check", 13 | "license": "MIT", 14 | "authors": [ 15 | { 16 | "name": "Patrick Organ", 17 | "email": "patrick@permafrost.dev", 18 | "homepage": "https://permafrost.dev", 19 | "role": "Developer" 20 | } 21 | ], 22 | "require": { 23 | "php": "^8.0", 24 | "ext-simplexml": "*", 25 | "symfony/console": "^6.0|^7.0" 26 | }, 27 | "require-dev": { 28 | "phpunit/phpunit": "^9.5|^10.0", 29 | "spatie/phpunit-snapshot-assertions": "^4.2" 30 | }, 31 | "autoload": { 32 | "psr-4": { 33 | "Permafrost\\CoverageCheck\\": "src/" 34 | } 35 | }, 36 | "autoload-dev": { 37 | "psr-4": { 38 | "Permafrost\\CoverageCheck\\Tests\\": "tests" 39 | } 40 | }, 41 | "scripts": { 42 | "test": "vendor/bin/phpunit" 43 | }, 44 | "config": { 45 | "sort-packages": true 46 | }, 47 | "minimum-stability": "dev", 48 | "prefer-stable": true, 49 | "bin": "bin/coverage-check", 50 | "funding": [ 51 | { 52 | "type": "github", 53 | "url": "https://github.com/sponsors/permafrost-dev" 54 | } 55 | ] 56 | } 57 | -------------------------------------------------------------------------------- /src/Commands/CheckCoverageCommand.php: -------------------------------------------------------------------------------- 1 | addArgument('filename') 29 | ->addOption('require', 'r', InputOption::VALUE_REQUIRED, 'Require a minimum code coverage percentage', null) 30 | ->addOption('metric', 'm', InputOption::VALUE_REQUIRED, 'Use a specific metric field (element, statement, or method)') 31 | ->addOption('coverage-only', 'C', InputOption::VALUE_NONE, 'Display only the code coverage percentage') 32 | ->addOption('precision', 'p', InputOption::VALUE_REQUIRED, 'Precision to use when rounding the code coverage percentage', 4) 33 | ->setDescription('Checks a clover-format coverage file for a minimum coverage percentage and optionally enforces it.'); 34 | } 35 | 36 | public function execute(InputInterface $input, OutputInterface $output): int 37 | { 38 | $this->output = $output; 39 | $this->input = $input; 40 | $this->config = ConfigurationFactory::create($input); 41 | 42 | try { 43 | $this->config->validate(); 44 | } catch (\InvalidArgumentException $e) { 45 | $output->writeln("[ERROR] {$e->getMessage()}"); 46 | 47 | return Command::INVALID; 48 | } 49 | 50 | if ($this->config->requireMode) { 51 | [$result, $coverage] = $this->checkCoverage($this->config->filename, $this->config->required); 52 | $this->displayRequireModeResults($result, $coverage, $this->config->required); 53 | 54 | return $result ? Command::SUCCESS : Command::FAILURE; 55 | } 56 | 57 | $checker = new CoverageChecker($this->config->filename, $this->config); 58 | $coverage = $checker->getCoveragePercent(); 59 | 60 | $this->displayCoverageResults($coverage); 61 | 62 | return Command::SUCCESS; 63 | } 64 | 65 | protected function checkCoverage(string $filename, float $requiredPercentage): array 66 | { 67 | $checker = new CoverageChecker($filename, $this->config); 68 | 69 | return [$checker->check($requiredPercentage), $checker->getCoveragePercent()]; 70 | } 71 | 72 | protected function displayRequireModeResults(bool $result, float $coverage, float $percentage): void 73 | { 74 | $prefix = $result ? '[PASS]' : '[FAIL]'; 75 | 76 | $this->output->writeln("$prefix Code coverage is {$coverage}% (required minimum is {$percentage}%)."); 77 | } 78 | 79 | protected function displayCoverageResults(float $coverage): void 80 | { 81 | $message = $this->config->displayCoverageOnly 82 | ? $coverage 83 | : "Code coverage is {$coverage}%."; 84 | 85 | $this->output->writeln($message); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # coverage-check 2 | 3 |

4 | coverage-check logo 5 |

6 | Package Version 7 | Downloads 8 | Test Run Status 9 |
10 | license 11 | code coverage 12 |

13 | 14 | --- 15 | 16 | Display the code coverage for a project using a clover.xml file, optionally enforcing a minimum code coverage percentage. 17 | 18 | This package is designed to be used in your CI/CD or automated testing process _(i.e., using GitHub Workflows)_. 19 | 20 | The concept for this package is based on [this article](https://ocramius.github.io/blog/automated-code-coverage-check-for-github-pull-requests-with-travis/). 21 | 22 | --- 23 | `` 24 | > Note on PHP 7.x support: 25 | > If you are using PHP 7.x, use the 1.x version of this package. 26 | > 27 | 28 | ## Installation 29 | 30 | ```bash 31 | composer require permafrost-dev/coverage-check --dev 32 | ``` 33 | 34 | ## Usage 35 | 36 | Specify a valid clover.xml file and (optionally) a minimum coverage percentage to require using the `--require` or `-r` flag. A percentage can be either a whole number (integer) or a decimal (float). 37 | 38 | If you specify the `--require/-r` flag, the check will fail if coverage percent is below the value you provide, and the process exit code will be non-zero. 39 | 40 | If you don't specify the `--require/-r` flag, only the percentage of code coverage will be displayed and the exit code will always be zero. 41 | 42 | ```bash 43 | ./vendor/bin/coverage-check clover.xml 44 | ./vendor/bin/coverage-check clover.xml --require=50 45 | ./vendor/bin/coverage-check clover.xml -r 80.5 46 | ./vendor/bin/coverage-check clover.xml -m statement -r 75 47 | ./vendor/bin/coverage-check clover.xml --precision=1 48 | ``` 49 | 50 | ## Available Options 51 | 52 | | Option | Description | 53 | | --- | --- | 54 | | `--coverage-only` or `-C` | Only display the code coverage value | 55 | | `--metric` or `-m` `` | Use the specified metric field for calculating coverage. Valid values are `element` _(default)_, `method`, or `statement` | 56 | | `--precision` or `-p` `` | Use the specified precision when calculating the code coverage percentage, where `` is an integer _(default: 4)_ | 57 | | `--require` or `-r` `` | Enforce a minimum code coverage value, where `` is an integer or decimal value | 58 | 59 | ## Metric fields 60 | 61 | The field that is used to calculate code coverage can be specified using the `--metric=` or `-m ` option. 62 | 63 | Valid field names are `element` _(the default)_, `statement`, and `method`. 64 | 65 | ## Generating clover-format coverage files 66 | 67 | PHPUnit can generate coverage reports in clover format: 68 | 69 | ```bash 70 | ./vendor/bin/phpunit --coverage-clover clover.xml 71 | ``` 72 | 73 | ## Sample Github Workflow 74 | 75 | ```yaml 76 | name: run-tests 77 | 78 | on: [push, pull_request] 79 | 80 | jobs: 81 | test: 82 | runs-on: ${{ matrix.os }} 83 | strategy: 84 | fail-fast: true 85 | matrix: 86 | os: [ubuntu-latest] 87 | php: [8.1, 8.0, 7.4, 7.3] 88 | 89 | name: P${{ matrix.php }} - ${{ matrix.os }} 90 | 91 | steps: 92 | - name: Checkout code 93 | uses: actions/checkout@v3 94 | 95 | - name: Setup PHP 96 | uses: shivammathur/setup-php@v2 97 | with: 98 | php-version: ${{ matrix.php }} 99 | extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, intl, iconv, fileinfo 100 | coverage: pcov 101 | 102 | - name: Setup problem matchers 103 | run: | 104 | echo "::add-matcher::${{ runner.tool_cache }}/php.json" 105 | echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" 106 | 107 | - name: Install dependencies 108 | run: composer update --prefer-stable --prefer-dist --no-interaction 109 | 110 | - name: Execute tests 111 | run: ./vendor/bin/phpunit --coverage-clover clover.xml 112 | 113 | - name: Enforce 75% code coverage 114 | run: ./vendor/bin/coverage-check clover.xml --require=75 --precision=2 115 | ``` 116 | 117 | ## Testing 118 | 119 | ```bash 120 | ./vendor/bin/phpunit 121 | ``` 122 | 123 | ## Changelog 124 | 125 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 126 | 127 | ## Contributing 128 | 129 | Please see [CONTRIBUTING](.github/CONTRIBUTING.md) for details. 130 | 131 | ## Security Vulnerabilities 132 | 133 | Please review [our security policy](../../security/policy) on how to report security vulnerabilities. 134 | 135 | ## Credits 136 | 137 | - [Patrick Organ](https://github.com/patinthehat) 138 | - [All Contributors](../../contributors) 139 | 140 | ## License 141 | 142 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 143 | --------------------------------------------------------------------------------