├── .scrutinizer.yml ├── composer.json ├── phpcs.xml └── src ├── CodeCoverageExtension.php └── Listener └── CodeCoverageListener.php /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | build: 2 | nodes: 3 | analysis: 4 | project_setup: 5 | override: 6 | - 'true' 7 | tests: 8 | override: 9 | - php-scrutinizer-run 10 | - 11 | command: phpcs-run 12 | use_website_config: true 13 | environment: 14 | node: 15 | version: 6.0.0 16 | filter: 17 | excluded_paths: 18 | - 'spec/*' 19 | checks: 20 | php: true 21 | coding_style: 22 | php: { } 23 | tools: 24 | php_cs_fixer: 25 | config: { level: Symfony } # or psr1 if you would just like to get fixes for PSR1 26 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "leanphp/phpspec-code-coverage", 3 | "description": "Generate Code Coverage reports for PhpSpec tests", 4 | "type": "library", 5 | "keywords": [ 6 | "phpspec", "code", "coverage", "generate", "generation", "build", 7 | "report", "test", "tests", "code-coverage", "reports", "clover", "spec" 8 | ], 9 | "homepage": "https://github.com/leanphp/phpspec-code-coverage", 10 | "license": "MIT", 11 | "authors": [ 12 | { 13 | "name": "ek9", 14 | "email": "dev@ek9.co", 15 | "homepage": "https://ek9.co" 16 | }, 17 | { 18 | "name": "Henrik Bjornskov" 19 | } 20 | ], 21 | "support": { 22 | "issues": "https://github.com/leanphp/phpspec-code-coverage/issues", 23 | "source": "https://github.com/leanphp/phpspec-code-coverage", 24 | "docs": "https://github.com/leanphp/phpspec-code-coverage#phpspec-code-coverage" 25 | }, 26 | "require": { 27 | "php": "^7.1", 28 | "phpspec/phpspec": "^4.2", 29 | "phpunit/php-code-coverage": "^5.0||^6.0" 30 | }, 31 | "require-dev": { 32 | "escapestudios/symfony2-coding-standard": "^3.1", 33 | "squizlabs/php_codesniffer": "^3.2" 34 | }, 35 | "suggest": { 36 | "ext-xdebug": "Install Xdebug to generate phpspec code coverage if you are not using phpdbg" 37 | }, 38 | "minimum-stability": "stable", 39 | "autoload" : { 40 | "psr-4" : { "LeanPHP\\PhpSpec\\CodeCoverage\\": "src/" } 41 | }, 42 | "extra": { 43 | "branch-alias": { 44 | "dev-master": "4.x-dev", 45 | "dev-v3": "3.x-dev" 46 | } 47 | }, 48 | "config": { 49 | "bin-dir": "bin" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | LeanPHP Coding Standard 5 | 6 | 7 | src 8 | 9 | 10 | test 11 | tests 12 | *Spec.php 13 | 14 | 15 | 16 | > 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/CodeCoverageExtension.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * @license MIT 8 | * 9 | * For the full copyright and license information, please see the LICENSE file 10 | * that was distributed with this source code. 11 | * 12 | */ 13 | namespace LeanPHP\PhpSpec\CodeCoverage; 14 | 15 | use LeanPHP\PhpSpec\CodeCoverage\Listener\CodeCoverageListener; 16 | use PhpSpec\Extension; 17 | use PhpSpec\ServiceContainer; 18 | use SebastianBergmann\CodeCoverage\CodeCoverage; 19 | use SebastianBergmann\CodeCoverage\Filter; 20 | use SebastianBergmann\CodeCoverage\Report; 21 | use SebastianBergmann\CodeCoverage\Version; 22 | use Symfony\Component\Console\Input\InputOption; 23 | 24 | /** 25 | * Injects Code Coverage Event Subscriber into the EventDispatcher. 26 | * The Subscriber will add Code Coverage information before each example 27 | * 28 | * @author Henrik Bjornskov 29 | */ 30 | class CodeCoverageExtension implements Extension 31 | { 32 | /** 33 | * {@inheritDoc} 34 | */ 35 | public function load(ServiceContainer $container, array $params = []) 36 | { 37 | foreach ($container->getByTag('console.commands') as $command) { 38 | $command->addOption('no-coverage', null, InputOption::VALUE_NONE, 'Skip code coverage generation'); 39 | } 40 | 41 | $container->define('code_coverage.filter', function () { 42 | return new Filter(); 43 | }); 44 | 45 | $container->define('code_coverage', function ($container) { 46 | return new CodeCoverage(null, $container->get('code_coverage.filter')); 47 | }); 48 | 49 | $container->define('code_coverage.options', function ($container) use ($params) { 50 | $options = !empty($params) ? $params : $container->getParam('code_coverage'); 51 | 52 | if (!isset($options['format'])) { 53 | $options['format'] = array('html'); 54 | } elseif (!is_array($options['format'])) { 55 | $options['format'] = (array) $options['format']; 56 | } 57 | 58 | if (isset($options['output'])) { 59 | if (!is_array($options['output']) && count($options['format']) === 1) { 60 | $format = $options['format'][0]; 61 | $options['output'] = array($format => $options['output']); 62 | } 63 | } 64 | 65 | if (!isset($options['show_uncovered_files'])) { 66 | $options['show_uncovered_files'] = true; 67 | } 68 | if (!isset($options['lower_upper_bound'])) { 69 | $options['lower_upper_bound'] = 35; 70 | } 71 | if (!isset($options['high_lower_bound'])) { 72 | $options['high_lower_bound'] = 70; 73 | } 74 | 75 | return $options; 76 | }); 77 | 78 | $container->define('code_coverage.reports', function ($container) { 79 | $options = $container->get('code_coverage.options'); 80 | 81 | $reports = array(); 82 | foreach ($options['format'] as $format) { 83 | switch ($format) { 84 | case 'clover': 85 | $reports['clover'] = new Report\Clover(); 86 | break; 87 | case 'php': 88 | $reports['php'] = new Report\PHP(); 89 | break; 90 | case 'text': 91 | $reports['text'] = new Report\Text( 92 | $options['lower_upper_bound'], 93 | $options['high_lower_bound'], 94 | $options['show_uncovered_files'], 95 | /* $showOnlySummary */ false 96 | ); 97 | break; 98 | case 'xml': 99 | $reports['xml'] = new Report\Xml\Facade(Version::id()); 100 | break; 101 | case 'crap4j': 102 | $reports['crap4j'] = new Report\Crap4j(); 103 | break; 104 | case 'html': 105 | $reports['html'] = new Report\Html\Facade(); 106 | break; 107 | } 108 | } 109 | 110 | $container->setParam('code_coverage', $options); 111 | 112 | return $reports; 113 | }); 114 | 115 | $container->define('event_dispatcher.listeners.code_coverage', function ($container) { 116 | 117 | $skipCoverage = false; 118 | $input = $container->get('console.input'); 119 | if ($input->hasOption('no-coverage') && $input->getOption('no-coverage')) { 120 | $skipCoverage = true; 121 | } 122 | 123 | $listener = new CodeCoverageListener( 124 | $container->get('console.io'), 125 | $container->get('code_coverage'), 126 | $container->get('code_coverage.reports'), 127 | $skipCoverage 128 | ); 129 | $listener->setOptions($container->getParam('code_coverage', array())); 130 | 131 | return $listener; 132 | }, ['event_dispatcher.listeners']); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/Listener/CodeCoverageListener.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * @license MIT 8 | * 9 | * For the full copyright and license information, please see the LICENSE file 10 | * that was distributed with this source code. 11 | * 12 | */ 13 | namespace LeanPHP\PhpSpec\CodeCoverage\Listener; 14 | 15 | use PhpSpec\Console\ConsoleIO; 16 | use PhpSpec\Event\ExampleEvent; 17 | use PhpSpec\Event\SuiteEvent; 18 | use SebastianBergmann\CodeCoverage\CodeCoverage; 19 | use SebastianBergmann\CodeCoverage\Report; 20 | use Symfony\Component\EventDispatcher\EventSubscriberInterface; 21 | 22 | /** 23 | * @author Henrik Bjornskov 24 | */ 25 | class CodeCoverageListener implements EventSubscriberInterface 26 | { 27 | private $coverage; 28 | private $reports; 29 | private $io; 30 | private $options; 31 | private $enabled; 32 | private $skipCoverage; 33 | 34 | /** 35 | * @param ConsoleIO $io 36 | * @param CodeCoverage $coverage 37 | * @param array $reports 38 | * @param boolean $skipCoverage 39 | */ 40 | public function __construct(ConsoleIO $io, CodeCoverage $coverage, array $reports, $skipCoverage = false) 41 | { 42 | $this->io = $io; 43 | $this->coverage = $coverage; 44 | $this->reports = $reports; 45 | $this->options = [ 46 | 'whitelist' => ['src', 'lib'], 47 | 'blacklist' => ['test', 'vendor', 'spec'], 48 | 'whitelist_files' => [], 49 | 'blacklist_files' => [], 50 | 'output' => ['html' => 'coverage'], 51 | 'format' => ['html'], 52 | ]; 53 | 54 | $this->enabled = extension_loaded('xdebug') || (PHP_SAPI === 'phpdbg'); 55 | $this->skipCoverage = $skipCoverage; 56 | } 57 | 58 | /** 59 | * Note: We use array_map() instead of array_walk() because the latter expects 60 | * the callback to take the value as the first and the index as the seconds parameter. 61 | * 62 | * @param SuiteEvent $event 63 | */ 64 | public function beforeSuite(SuiteEvent $event) : void 65 | { 66 | if (!$this->enabled || $this->skipCoverage) { 67 | return; 68 | } 69 | 70 | $filter = $this->coverage->filter(); 71 | 72 | array_map( 73 | [$filter, 'addDirectoryToWhitelist'], 74 | $this->options['whitelist'] 75 | ); 76 | array_map( 77 | [$filter, 'removeDirectoryFromWhitelist'], 78 | $this->options['blacklist'] 79 | ); 80 | array_map( 81 | [$filter, 'addFileToWhitelist'], 82 | $this->options['whitelist_files'] 83 | ); 84 | array_map( 85 | [$filter, 'removeFileFromWhitelist'], 86 | $this->options['blacklist_files'] 87 | ); 88 | } 89 | 90 | /** 91 | * @param ExampleEvent $event 92 | */ 93 | public function beforeExample(ExampleEvent $event): void 94 | { 95 | if (!$this->enabled || $this->skipCoverage) { 96 | return; 97 | } 98 | 99 | $example = $event->getExample(); 100 | 101 | $name = strtr('%spec%::%example%', [ 102 | '%spec%' => $example->getSpecification()->getClassReflection()->getName(), 103 | '%example%' => $example->getFunctionReflection()->getName(), 104 | ]); 105 | 106 | $this->coverage->start($name); 107 | } 108 | 109 | /** 110 | * @param ExampleEvent $event 111 | */ 112 | public function afterExample(ExampleEvent $event): void 113 | { 114 | if (!$this->enabled || $this->skipCoverage) { 115 | return; 116 | } 117 | 118 | $this->coverage->stop(); 119 | } 120 | 121 | /** 122 | * @param SuiteEvent $event 123 | */ 124 | public function afterSuite(SuiteEvent $event): void 125 | { 126 | if (!$this->enabled || $this->skipCoverage) { 127 | if ($this->io && $this->io->isVerbose()) { 128 | if (!$this->enabled) { 129 | $this->io->writeln('No code coverage will be generated as neither Xdebug nor phpdbg was detected.'); 130 | } elseif ($this->skipCoverage) { 131 | $this->io->writeln('Skipping code coverage generation'); 132 | } 133 | } 134 | 135 | return; 136 | } 137 | 138 | if ($this->io && $this->io->isVerbose()) { 139 | $this->io->writeln(''); 140 | } 141 | 142 | foreach ($this->reports as $format => $report) { 143 | if ($this->io && $this->io->isVerbose()) { 144 | $this->io->writeln(sprintf('Generating code coverage report in %s format ...', $format)); 145 | } 146 | 147 | if ($report instanceof Report\Text) { 148 | $output = $report->process($this->coverage, /* showColors */ $this->io->isDecorated()); 149 | $this->io->writeln($output); 150 | } else { 151 | $report->process($this->coverage, $this->options['output'][$format]); 152 | } 153 | } 154 | } 155 | 156 | /** 157 | * @param array $options 158 | */ 159 | public function setOptions(array $options): void 160 | { 161 | $this->options = $options + $this->options; 162 | } 163 | 164 | /** 165 | * {@inheritDoc} 166 | */ 167 | public static function getSubscribedEvents(): array 168 | { 169 | return [ 170 | 'beforeExample' => ['beforeExample', -10], 171 | 'afterExample' => ['afterExample', -10], 172 | 'beforeSuite' => ['beforeSuite', -10], 173 | 'afterSuite' => ['afterSuite', -10], 174 | ]; 175 | } 176 | } 177 | --------------------------------------------------------------------------------