├── .gitignore ├── LICENSE ├── README.md ├── composer.json └── src └── BehatRemoteCodeCoverage ├── RemoteCodeCoverageExtension.php ├── RemoteCodeCoverageListener.php └── Resources └── config └── services.yml /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | composer.lock -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 Matthias Noback 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Behat remote code coverage extension 2 | 3 | This extension can be used to collect code coverage data from the web server that's called by Mink while running Behat. 4 | 5 | To use this extension, enable it under `extensions` and for every suite that needs remote code coverage collection, set `remote_coverage_enabled` to `true`. 6 | 7 | ```yaml 8 | default: 9 | extensions: 10 | BehatRemoteCodeCoverage\RemoteCodeCoverageExtension: 11 | target_directory: '%paths.base%/var/coverage' 12 | suites: 13 | default: 14 | remote_coverage_enabled: true 15 | ``` 16 | 17 | Now modify the front controller of your web application to look like this: 18 | 19 | ```php 20 | use LiveCodeCoverage\RemoteCodeCoverage; 21 | 22 | $shutDownCodeCoverage = RemoteCodeCoverage::bootstrap( 23 | (bool)getenv('CODE_COVERAGE_ENABLED'), 24 | sys_get_temp_dir(), 25 | __DIR__ . '/../phpunit.xml.dist' 26 | ); 27 | 28 | // Run your web application now... 29 | 30 | // This will save and store collected coverage data: 31 | $shutDownCodeCoverage(); 32 | ``` 33 | 34 | Make sure to modify the call to `RemoteCodeCoverage::bootstrap()` if needed: 35 | 36 | 1. Provide your own logic to determine if code coverage should be enabled in the first place (this example uses an environment variable for that). This is important for security reasons. It helps you make sure that the production server won't expose any collected coverage data. 37 | 2. Provide your own directory for storing the coverage data files (`.cov`). 38 | 3. Provide the path to your own `phpunit.xml(.dist)` file. This file is only used for its [code coverage filter configuration](https://phpunit.de/manual/current/en/appendixes.configuration.html#appendixes.configuration.whitelisting-files). 39 | 40 | After a test run, the extension makes a special call (`/?code_coverage_export=true&...`) to the web application. The response to this call contains the serialized code coverage data. It will be stored as a file in `target_directory`, named after the test suite itself, e.g. `default.cov`. 41 | 42 | You can use these `.cov` files to generate nice reports, using [`phpcov`](https://github.com/sebastianbergmann/phpcov). 43 | 44 | You could even configure PHPUnit to generate a `.cov` file in the same directory, so you can combine coverage data from PHPUnit and Behat in one report. 45 | 46 | To (also) generate (local) code coverage during a Behat test run, use the [`LocalCodeCoverageExtension`](https://github.com/matthiasnoback/behat-local-code-coverage-extension/). 47 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "matthiasnoback/behat-remote-code-coverage-extension", 3 | "license": "MIT", 4 | "require": { 5 | "php": ">=7.0", 6 | "behat/behat": "^3.0", 7 | "matthiasnoback/live-code-coverage": "^1.0", 8 | "behat/mink-extension": "^2.0" 9 | }, 10 | "autoload": { 11 | "psr-4": { 12 | "": "src/" 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/BehatRemoteCodeCoverage/RemoteCodeCoverageExtension.php: -------------------------------------------------------------------------------- 1 | children() 32 | ->scalarNode('target_directory') 33 | ->isRequired() 34 | ->info('The directory where the generated coverage files should be stored.') 35 | ->end() 36 | ->enumNode('split_by') 37 | ->defaultValue('suite') 38 | ->values(['suite', 'feature', 'scenario']) 39 | ->info('The strategy to save/split coverage files by (suite, feature or scenario).') 40 | ->end() 41 | ->scalarNode('base_url') 42 | ->defaultNull() 43 | ->info('The base url of the php application, leave null to use mink base url.') 44 | ->end() 45 | ->end(); 46 | } 47 | 48 | public function load(ContainerBuilder $container, array $config) 49 | { 50 | $loader = new YamlFileLoader($container, new FileLocator(__DIR__ . '/Resources/config')); 51 | $loader->load('services.yml'); 52 | 53 | $container->setParameter('remote_code_coverage.target_directory', $config['target_directory']); 54 | $container->setParameter('remote_code_coverage.split_by', $config['split_by']); 55 | $container->setParameter('remote_code_coverage.base_url', $config['base_url'] ?: '%mink.base_url%'); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/BehatRemoteCodeCoverage/RemoteCodeCoverageListener.php: -------------------------------------------------------------------------------- 1 | mink = $mink; 57 | 58 | Assert::string($baseUrl); 59 | $this->baseUrl = $baseUrl; 60 | 61 | Assert::string($targetDirectory, 'Coverage target directory should be a string'); 62 | $this->targetDirectory = $targetDirectory; 63 | 64 | Assert::string($splitBy, 'Split coverage files by should be a string'); 65 | $this->splitBy = $splitBy; 66 | } 67 | 68 | public static function getSubscribedEvents() 69 | { 70 | return [ 71 | SuiteTested::BEFORE => 'beforeSuite', 72 | ScenarioTested::BEFORE => 'beforeScenario', 73 | ScenarioTested::AFTER => 'afterScenario', 74 | FeatureTested::AFTER => 'afterFeature', 75 | SuiteTested::AFTER => 'afterSuite' 76 | ]; 77 | } 78 | 79 | public function beforeSuite(BeforeSuiteTested $event) 80 | { 81 | $this->coverageEnabled = $event->getSuite()->hasSetting('remote_coverage_enabled') 82 | && $event->getSuite()->getSetting('remote_coverage_enabled'); 83 | 84 | if (!$this->coverageEnabled) { 85 | return; 86 | } 87 | 88 | $this->coverageGroup = uniqid($event->getSuite()->getName(), true); 89 | } 90 | 91 | public function beforeScenario(ScenarioLikeTested $event) 92 | { 93 | if (!$this->coverageEnabled) { 94 | return; 95 | } 96 | 97 | $coverageId = $event->getFeature()->getFile() . ':' . $event->getNode()->getLine(); 98 | 99 | $minkSession = $this->mink->getSession(); 100 | 101 | if (!$minkSession->isStarted()) { 102 | $minkSession->start(); 103 | } 104 | 105 | $minkSession->setCookie('collect_code_coverage', true); 106 | $minkSession->setCookie('coverage_group', $this->coverageGroup); 107 | $minkSession->setCookie('coverage_id', $coverageId); 108 | } 109 | 110 | public function afterScenario(AfterScenarioTested $event) 111 | { 112 | if (!$this->coverageEnabled || 'scenario' !== $this->splitBy) { 113 | return; 114 | } 115 | 116 | $parts = pathinfo($event->getFeature()->getFile()); 117 | Storage::storeCodeCoverage($this->getCoverage(), $this->targetDirectory, sprintf('%s-%s_%s', basename($parts['dirname']), $parts['filename'], $event->getNode()->getLine())); 118 | } 119 | 120 | public function afterFeature(AfterFeatureTested $event) 121 | { 122 | if (!$this->coverageEnabled || 'feature' !== $this->splitBy) { 123 | return; 124 | } 125 | 126 | $parts = pathinfo($event->getFeature()->getFile()); 127 | Storage::storeCodeCoverage($this->getCoverage(), $this->targetDirectory, sprintf('%s-%s', basename($parts['dirname']), $parts['filename'])); 128 | } 129 | 130 | public function afterSuite(AfterSuiteTested $event) 131 | { 132 | if (!$this->coverageEnabled) { 133 | return; 134 | } 135 | 136 | if ('suite' === $this->splitBy) { 137 | Storage::storeCodeCoverage($this->getCoverage(), $this->targetDirectory, $event->getSuite()->getName()); 138 | } 139 | 140 | $this->reset(); 141 | } 142 | 143 | private function reset() 144 | { 145 | $this->coverageGroup = null; 146 | $this->coverageEnabled = false; 147 | } 148 | 149 | /** 150 | * @return mixed 151 | * @throws RuntimeException 152 | */ 153 | private function getCoverage() 154 | { 155 | $requestUrl = $this->baseUrl . '/?export_code_coverage=true&coverage_group=' . urlencode($this->coverageGroup); 156 | $response = file_get_contents($requestUrl); 157 | $coverage = unserialize($response); 158 | 159 | if (!$coverage instanceof CodeCoverage) { 160 | throw new RuntimeException(sprintf( 161 | 'The response for "%s" did not contain a serialized CodeCoverage object: %s', 162 | $requestUrl, 163 | $response 164 | )); 165 | } 166 | 167 | return $coverage; 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/BehatRemoteCodeCoverage/Resources/config/services.yml: -------------------------------------------------------------------------------- 1 | services: 2 | remote_code_coverage.remote_code_coverage_listener: 3 | class: BehatRemoteCodeCoverage\RemoteCodeCoverageListener 4 | arguments: 5 | - '@mink' 6 | - '%remote_code_coverage.base_url%' 7 | - '%remote_code_coverage.target_directory%' 8 | - '%remote_code_coverage.split_by%' 9 | tags: 10 | - { name: event_dispatcher.subscriber } 11 | --------------------------------------------------------------------------------