├── .github └── workflows │ └── main.yml ├── .gitignore ├── .mailmap ├── Installer.php ├── README.md ├── c3.php └── composer.json /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | codeception3: 7 | runs-on: ubuntu-latest 8 | 9 | strategy: 10 | matrix: 11 | php: [5.6, 7.0, 7.1, 7.2, 7.3, 7.4] 12 | 13 | env: 14 | extensions: zip 15 | 16 | steps: 17 | - name: Checkout code 18 | uses: actions/checkout@v3 19 | 20 | - name: Setup PHP 21 | uses: shivammathur/setup-php@v2 22 | with: 23 | php-version: ${{ matrix.php }} 24 | 25 | - name: Use Composer v1 26 | run: composer self-update --1 27 | 28 | - name: Validate composer.json 29 | run: composer validate 30 | 31 | - name: Install dependencies 32 | run: | 33 | composer require codeception/codeception:"^3.0" --dev --prefer-source 34 | cp c3.php vendor/codeception/codeception/tests/data/claypit 35 | 36 | - name: Start PHP Server 37 | run: | 38 | cd vendor/codeception/codeception && php -S 127.0.0.1:8000 -t tests/data/app -d pcov.directory=$(pwd)/tests/data/app >/dev/null 2>&1 & 39 | 40 | - name: Run test suite 41 | run: cd vendor/codeception/codeception && php ./codecept run coverage 42 | 43 | composer1: 44 | runs-on: ubuntu-latest 45 | 46 | strategy: 47 | matrix: 48 | php: [5.6, 7.0, 7.1, 7.2, 7.3, 7.4, 8.0, 8.1] 49 | 50 | steps: 51 | - name: Checkout code 52 | uses: actions/checkout@v3 53 | 54 | - name: Setup PHP 55 | uses: shivammathur/setup-php@v2 56 | with: 57 | php-version: ${{ matrix.php }} 58 | 59 | - name: Use Composer v1 60 | run: composer self-update --1 61 | 62 | - name: Validate composer.json 63 | run: composer validate 64 | 65 | - name: Install dependencies 66 | run: | 67 | composer require codeception/codeception:"^4.2" --dev --prefer-source 68 | composer require codeception/module-filesystem:"^1.0" codeception/module-cli:"^1.0" codeception/module-asserts:"^1.0|^2.0" codeception/module-phpbrowser:"^1.0" --dev 69 | cp c3.php vendor/codeception/codeception/tests/data/claypit 70 | 71 | - name: Start PHP Server 72 | run: | 73 | cd vendor/codeception/codeception && php -S 127.0.0.1:8000 -t tests/data/app -d pcov.directory=$(pwd)/tests/data/app >/dev/null 2>&1 & 74 | 75 | - name: Run test suite 76 | run: cd vendor/codeception/codeception && php ./codecept run coverage 77 | 78 | composer2: 79 | runs-on: ubuntu-latest 80 | 81 | strategy: 82 | matrix: 83 | php: [7.2, 7.3, 7.4, 8.0, 8.1] 84 | 85 | steps: 86 | - name: Checkout code 87 | uses: actions/checkout@v3 88 | 89 | - name: Setup PHP 90 | uses: shivammathur/setup-php@v2 91 | with: 92 | php-version: ${{ matrix.php }} 93 | 94 | - name: Use Composer v2 95 | run: composer self-update --2 96 | 97 | - name: Validate composer.json 98 | run: composer validate 99 | 100 | - name: Install dependencies 101 | run: | 102 | composer require codeception/codeception:"^4.1" --dev --prefer-source 103 | composer require codeception/module-filesystem:"^1.0" codeception/module-cli:"^1.0" codeception/module-asserts:"^1.0|^2.0" codeception/module-phpbrowser:"^1.0" --dev 104 | cp c3.php vendor/codeception/codeception/tests/data/claypit 105 | 106 | - name: Start PHP Server 107 | run: | 108 | cd vendor/codeception/codeception && php -S 127.0.0.1:8000 -t tests/data/app -d pcov.directory=$(pwd)/tests/data/app >/dev/null 2>&1 & 109 | 110 | - name: Run test suite 111 | run: cd vendor/codeception/codeception && php ./codecept run coverage 112 | 113 | codeception5: 114 | runs-on: ubuntu-latest 115 | 116 | strategy: 117 | matrix: 118 | php: [8.0, 8.1, 8.2] 119 | 120 | steps: 121 | - name: Checkout code 122 | uses: actions/checkout@v3 123 | 124 | - name: Setup PHP 125 | uses: shivammathur/setup-php@v2 126 | with: 127 | php-version: ${{ matrix.php }} 128 | 129 | - name: Use Composer v2 130 | run: composer self-update --2 131 | 132 | - name: Validate composer.json 133 | run: composer validate 134 | 135 | - name: Install dependencies 136 | run: | 137 | composer require codeception/codeception:"^5.0" --dev --prefer-source 138 | composer require codeception/module-filesystem:"^3.0" codeception/module-cli:"^2.0" codeception/module-asserts:"^3.0" codeception/module-phpbrowser:"^3.0" --dev 139 | cp c3.php vendor/codeception/codeception/tests/data/claypit 140 | 141 | - name: Start PHP Server 142 | run: | 143 | cd vendor/codeception/codeception && php -S 127.0.0.1:8008 -t tests/data/app -d pcov.directory=$(pwd)/tests/data/app >/dev/null 2>&1 & 144 | 145 | - name: Run test suite 146 | run: cd vendor/codeception/codeception && php ./codecept run coverage 147 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | composer.phar 3 | vendor/ 4 | RoboFile.php 5 | composer.lock -------------------------------------------------------------------------------- /.mailmap: -------------------------------------------------------------------------------- 1 | Michael Bodnarchuk "Davert" 2 | Michael Bodnarchuk "Davert" 3 | Michael Bodnarchuk "Davert" 4 | Michael Bodnarchuk "Davert" 5 | Michael Bodnarchuk "Davert" 6 | -------------------------------------------------------------------------------- /Installer.php: -------------------------------------------------------------------------------- 1 | io = $io; 26 | } 27 | 28 | public function deactivate(Composer $composer, IOInterface $io) 29 | { 30 | } 31 | 32 | public function uninstall(Composer $composer, IOInterface $io) { 33 | $this->deleteFile(); 34 | } 35 | 36 | protected function isOperationOnC3(PackageEvent $event) 37 | { 38 | if (static::composerV2()) { 39 | return true; 40 | } 41 | $name = ''; 42 | 43 | if ($event->getOperation() instanceof InstallOperation) { 44 | list(, $name) = explode('/', $event->getOperation()->getPackage()->getName()); 45 | } elseif ($event->getOperation() instanceof UpdateOperation) { 46 | list(, $name) = explode('/', $event->getOperation()->getTargetPackage()->getName()); 47 | } elseif ($event->getOperation() instanceof UninstallOperation) { 48 | list(, $name) = explode('/', $event->getOperation()->getPackage()->getName()); 49 | } 50 | 51 | return $name === 'c3'; 52 | } 53 | 54 | public static function getSubscribedEvents() 55 | { 56 | if (static::composerV2()) { 57 | return [ 58 | ScriptEvents::POST_INSTALL_CMD => [ 59 | ['copyC3V2', 0] 60 | ], 61 | ScriptEvents::POST_UPDATE_CMD => [ 62 | ['askForUpdateV2', 0] 63 | ], 64 | ]; 65 | } 66 | return [ 67 | PackageEvents::POST_PACKAGE_INSTALL => [ 68 | ['copyC3', 0] 69 | ], 70 | PackageEvents::POST_PACKAGE_UPDATE => [ 71 | ['askForUpdate', 0] 72 | ], 73 | PackageEvents::POST_PACKAGE_UNINSTALL => [ 74 | ['deleteC3', 0] 75 | ] 76 | ]; 77 | } 78 | 79 | public function copyC3(PackageEvent $event) 80 | { 81 | if (!$this->isOperationOnC3($event)) { 82 | return; 83 | } 84 | 85 | $this->copyC3V2($event); 86 | } 87 | 88 | public function copyC3V2(Event $event) 89 | { 90 | if ($this->c3NotChanged()) { 91 | $this->io->write("[codeception/c3] c3.php is already up-to-date"); 92 | return; 93 | } 94 | if (file_exists(getcwd() . DIRECTORY_SEPARATOR . 'c3.php')) { 95 | $replace = $this->io->askConfirmation("c3.php has changed Do you want to replace c3.php with latest version?", false); 96 | if (!$replace) { 97 | return; 98 | } 99 | } 100 | 101 | $this->io->write("[codeception/c3] Copying c3.php to the root of your project..."); 102 | copy(__DIR__ . DIRECTORY_SEPARATOR . 'c3.php', getcwd() . DIRECTORY_SEPARATOR.'c3.php'); 103 | $this->io->write("[codeception/c3] Include c3.php into index.php in order to collect codecoverage from server scripts"); 104 | } 105 | 106 | public function askForUpdate(PackageEvent $event) 107 | { 108 | if (!$this->isOperationOnC3($event) || $this->c3NotChanged()) { 109 | return; 110 | } 111 | $this->copyC3($event); 112 | } 113 | 114 | public function askForUpdateV2(Event $event) 115 | { 116 | if ($this->c3NotChanged()) { 117 | return; 118 | } 119 | $this->copyC3V2($event); 120 | } 121 | 122 | private function c3NotChanged() 123 | { 124 | return file_exists(getcwd() . DIRECTORY_SEPARATOR . 'c3.php') && 125 | md5_file(__DIR__ . DIRECTORY_SEPARATOR . 'c3.php') === md5_file(getcwd() . DIRECTORY_SEPARATOR . 'c3.php'); 126 | } 127 | 128 | public function deleteC3(PackageEvent $event) 129 | { 130 | if (!$this->isOperationOnC3($event)) { 131 | return; 132 | } 133 | $this->deleteFile(); 134 | } 135 | 136 | private function deleteFile() { 137 | if (file_exists(getcwd() . DIRECTORY_SEPARATOR . 'c3.php')) { 138 | $this->io->write("[codeception/c3] Deleting c3.php from the root of your project..."); 139 | unlink(getcwd() . DIRECTORY_SEPARATOR . 'c3.php'); 140 | } 141 | } 142 | 143 | private static function composerV2() 144 | { 145 | return Comparator::greaterThanOrEqualTo(PluginInterface::PLUGIN_API_VERSION, '2.0.0'); 146 | } 147 | 148 | } 149 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **[Issues](https://github.com/Codeception/Codeception/issues)** | **[Usage Guide](https://codeception.com/docs/11-Codecoverage)** 2 | 3 | ## Remote CodeCoverage for Codeception [![Build Status](https://travis-ci.org/Codeception/c3.svg?branch=2.0)](https://travis-ci.org/Codeception/c3) 4 | 5 | This file `c3.php` should be included into the application you are testing in the very first line. 6 | It will start remote code coverage collection. Coverage data will be stored to disk and retrieved by `codeception` when tests from the suite are finished. 7 | This file won't affect your application in any way. It is executed **only** when a special header `X-Codeception-CodeCoverage` is sent. Alternatively, if you use Selenium, special cookie `CODECEPTION_CODECOVERAGE` is used. In any other case your application run as usually with no overheads. 8 | 9 | ### Local Code Coverage 10 | 11 | If you don't run tests on remote server but use a webserver (Apache, Nginx, PhpWebserver) you need `c3.php` installed just the same way. 12 | In this case coverage result will be merged with local code coverage. 13 | 14 | ### Installation 15 | 16 | File `c3.php` should be put in project root, into the same directory where `codeception.yml` config is located. 17 | Also, make sure Codeception is available on remote server either in phar/pear/composer packages. 18 | 19 | #### Via Composer 20 | 21 | Add to `composer.json`: 22 | 23 | ``` 24 | "require-dev": { 25 | "codeception/codeception": "3.*", 26 | "codeception/c3": "2.*" 27 | } 28 | ``` 29 | 30 | C3 installer will copy `c3.php` to the project root. 31 | 32 | #### Manually 33 | 34 | ``` 35 | wget https://raw.github.com/Codeception/c3/2.0/c3.php 36 | ``` 37 | 38 | ### Setup 39 | 40 | Now you should include c3.php in your front script, like `index.php`. 41 | 42 | Example file: `web/index.php`: 43 | 44 | ``` php 45 | 53 | ``` 54 | 55 | Now on when is Codeception launched with code coverage enabled you will receive a coverage report from this remote server. 56 | 57 | ### Configuration 58 | 59 | To enable remote (and local) codecoverage by c3.script you should edit global configuration file `codeception.yml`, or one of the suite configuration files. 60 | 61 | Example: codeception.yml 62 | 63 | ``` yml 64 | 65 | coverage: 66 | enabled: true 67 | remote: true 68 | include: 69 | - app/* 70 | exclude: 71 | - app/cache/* 72 | ``` 73 | 74 | The `remote` option specifies if you run your application actually runs on another server. If your webserver runs on the same node and uses the same codebase, 75 | disable this option. 76 | 77 | ## Predefined Routes 78 | 79 | c3 file shouldn't break your application, but there are predefined routes that will be managed by c3. 80 | Codeception will access routes in order to receive collected coverage report in different formats. 81 | 82 | * `c3/report/clover` 83 | * `c3/report/serialized` 84 | * `c3/report/html` 85 | * `c3/report/clear` 86 | 87 | ## Debug 88 | 89 | In case you got into troubles and remote debugging still doesn't start you can try the following. Edit `c3.php` file and remove the header check 90 | 91 | ``` php 92 | // to remove 93 | if (!array_key_exists('HTTP_X_CODECEPTION_CODECOVERAGE', $_SERVER)) { 94 | return; 95 | } 96 | ``` 97 | then add this line to the top of file: 98 | 99 | ``` php 100 | $_SERVER['HTTP_X_CODECEPTION_CODECOVERAGE_DEBUG'] = 1; 101 | ``` 102 | 103 | now access `http://yourhost/c3/report/clear` url and see if it has errors. Please check that error_reporting is set to E_ALL 104 | 105 | ## Temp directories 106 | 107 | In root of your project `c3tmp` dir will be created during code coverage. 108 | It will not be deleted after suite ends for testing and debugging purposes. 109 | Serialized data as well as xml and html code coverage reports will be stored there. 110 | -------------------------------------------------------------------------------- /c3.php: -------------------------------------------------------------------------------- 1 | $value) { 29 | if (!empty($value)) { 30 | $_SERVER['HTTP_X_CODECEPTION_' . strtoupper($key)] = $value; 31 | } 32 | } 33 | } 34 | } 35 | 36 | if (!array_key_exists('HTTP_X_CODECEPTION_CODECOVERAGE', $_SERVER)) { 37 | return; 38 | } 39 | 40 | if (!function_exists('__c3_error')) { 41 | function __c3_error($message) 42 | { 43 | $errorLogFile = defined('C3_CODECOVERAGE_ERROR_LOG_FILE') ? 44 | C3_CODECOVERAGE_ERROR_LOG_FILE : 45 | C3_CODECOVERAGE_MEDIATE_STORAGE . DIRECTORY_SEPARATOR . 'error.txt'; 46 | if (file_put_contents($errorLogFile, $message, FILE_APPEND | LOCK_EX) === false) { 47 | $message = "Could not write error to log file ($errorLogFile), original message: $message"; 48 | } 49 | if (!headers_sent()) { 50 | header('X-Codeception-CodeCoverage-Error: ' . str_replace("\n", ' ', $message), true, 500); 51 | setcookie('CODECEPTION_CODECOVERAGE_ERROR', $message); 52 | } 53 | } 54 | } 55 | 56 | // Autoload Codeception classes 57 | if (!class_exists('\\Codeception\\Codecept') || !function_exists('codecept_is_path_absolute')) { 58 | if (file_exists(__DIR__ . '/codecept.phar')) { 59 | require_once 'phar://' . __DIR__ . '/codecept.phar/autoload.php'; 60 | } elseif (stream_resolve_include_path(__DIR__ . '/vendor/autoload.php')) { 61 | require_once __DIR__ . '/vendor/autoload.php'; 62 | // Required to load some methods only available at codeception/autoload.php 63 | if (stream_resolve_include_path(__DIR__ . '/vendor/codeception/codeception/autoload.php')) { 64 | require_once __DIR__ . '/vendor/codeception/codeception/autoload.php'; 65 | } 66 | } elseif (stream_resolve_include_path('Codeception/autoload.php')) { 67 | require_once 'Codeception/autoload.php'; 68 | } else { 69 | __c3_error('Codeception is not loaded. Please check that either PHAR or Composer package can be used'); 70 | } 71 | } 72 | 73 | // phpunit codecoverage shimming 74 | if (!class_exists('PHP_CodeCoverage') and class_exists('SebastianBergmann\CodeCoverage\CodeCoverage')) { 75 | class_alias('SebastianBergmann\CodeCoverage\CodeCoverage', 'PHP_CodeCoverage'); 76 | class_alias('SebastianBergmann\CodeCoverage\Report\Text', 'PHP_CodeCoverage_Report_Text'); 77 | class_alias('SebastianBergmann\CodeCoverage\Report\PHP', 'PHP_CodeCoverage_Report_PHP'); 78 | class_alias('SebastianBergmann\CodeCoverage\Report\Clover', 'PHP_CodeCoverage_Report_Clover'); 79 | class_alias('SebastianBergmann\CodeCoverage\Report\Crap4j', 'PHP_CodeCoverage_Report_Crap4j'); 80 | class_alias('SebastianBergmann\CodeCoverage\Report\Html\Facade', 'PHP_CodeCoverage_Report_HTML'); 81 | class_alias('SebastianBergmann\CodeCoverage\Report\Xml\Facade', 'PHP_CodeCoverage_Report_XML'); 82 | class_alias('SebastianBergmann\CodeCoverage\Exception', 'PHP_CodeCoverage_Exception'); 83 | } 84 | // phpunit version 85 | if (!class_exists('PHPUnit_Runner_Version') && class_exists('PHPUnit\Runner\Version')) { 86 | class_alias('PHPUnit\Runner\Version', 'PHPUnit_Runner_Version'); 87 | } 88 | 89 | // Load Codeception Config 90 | $configDistFile = realpath(__DIR__) . DIRECTORY_SEPARATOR . 'codeception.dist.yml'; 91 | $configFile = realpath(__DIR__) . DIRECTORY_SEPARATOR . 'codeception.yml'; 92 | 93 | if (isset($_SERVER['HTTP_X_CODECEPTION_CODECOVERAGE_CONFIG'])) { 94 | $configFile = realpath(__DIR__) . DIRECTORY_SEPARATOR . $_SERVER['HTTP_X_CODECEPTION_CODECOVERAGE_CONFIG']; 95 | } 96 | if (file_exists($configFile)) { 97 | // Use codeception.yml for configuration. 98 | } elseif (file_exists($configDistFile)) { 99 | // Use codeception.dist.yml for configuration. 100 | $configFile = $configDistFile; 101 | } else { 102 | __c3_error(sprintf("Codeception config file '%s' not found", $configFile)); 103 | } 104 | try { 105 | \Codeception\Configuration::config($configFile); 106 | } catch (\Exception $e) { 107 | __c3_error($e->getMessage()); 108 | } 109 | 110 | if (!defined('C3_CODECOVERAGE_MEDIATE_STORAGE')) { 111 | 112 | // workaround for 'zend_mm_heap corrupted' problem 113 | gc_disable(); 114 | 115 | $memoryLimit = ini_get('memory_limit'); 116 | $requiredMemory = '384M'; 117 | if ((substr($memoryLimit, -1) === 'M' && (int)$memoryLimit < (int)$requiredMemory) 118 | || (substr($memoryLimit, -1) === 'K' && (int)$memoryLimit < (int)$requiredMemory * 1024) 119 | || (ctype_digit($memoryLimit) && (int)$memoryLimit < (int)$requiredMemory * 1024 * 1024) 120 | ) { 121 | ini_set('memory_limit', $requiredMemory); 122 | } 123 | 124 | define('C3_CODECOVERAGE_MEDIATE_STORAGE', Codeception\Configuration::outputDir() . 'c3tmp'); 125 | define('C3_CODECOVERAGE_PROJECT_ROOT', Codeception\Configuration::projectDir()); 126 | define('C3_CODECOVERAGE_TESTNAME', $_SERVER['HTTP_X_CODECEPTION_CODECOVERAGE']); 127 | 128 | function __c3_build_html_report(PHP_CodeCoverage $codeCoverage, $path) 129 | { 130 | $writer = new PHP_CodeCoverage_Report_HTML(); 131 | $writer->process($codeCoverage, $path . 'html'); 132 | 133 | if (file_exists($path . '.tar')) { 134 | unlink($path . '.tar'); 135 | } 136 | 137 | $phar = new PharData($path . '.tar'); 138 | $phar->setSignatureAlgorithm(Phar::SHA1); 139 | $files = $phar->buildFromDirectory($path . 'html'); 140 | array_map('unlink', $files); 141 | 142 | if (in_array('GZ', Phar::getSupportedCompression())) { 143 | if (file_exists($path . '.tar.gz')) { 144 | unlink($path . '.tar.gz'); 145 | } 146 | 147 | $phar->compress(\Phar::GZ); 148 | 149 | // close the file so that we can rename it 150 | unset($phar); 151 | 152 | unlink($path . '.tar'); 153 | rename($path . '.tar.gz', $path . '.tar'); 154 | } 155 | 156 | return $path . '.tar'; 157 | } 158 | 159 | function __c3_build_clover_report(PHP_CodeCoverage $codeCoverage, $path) 160 | { 161 | $writer = new PHP_CodeCoverage_Report_Clover(); 162 | $writer->process($codeCoverage, $path . '.clover.xml'); 163 | 164 | return $path . '.clover.xml'; 165 | } 166 | 167 | function __c3_build_crap4j_report(PHP_CodeCoverage $codeCoverage, $path) 168 | { 169 | $writer = new PHP_CodeCoverage_Report_Crap4j(); 170 | $writer->process($codeCoverage, $path . '.crap4j.xml'); 171 | 172 | return $path . '.crap4j.xml'; 173 | } 174 | 175 | function __c3_build_cobertura_report(PHP_CodeCoverage $codeCoverage, $path) 176 | { 177 | if (!class_exists(\SebastianBergmann\CodeCoverage\Report\Cobertura::class)) { 178 | throw new Exception('Cobertura report requires php-code-coverage >= 9.2'); 179 | } 180 | $writer = new \SebastianBergmann\CodeCoverage\Report\Cobertura(); 181 | $writer->process($codeCoverage, $path . '.cobertura.xml'); 182 | 183 | return $path . '.cobertura.xml'; 184 | } 185 | 186 | function __c3_build_phpunit_report(PHP_CodeCoverage $codeCoverage, $path) 187 | { 188 | $writer = new PHP_CodeCoverage_Report_XML(\PHPUnit_Runner_Version::id()); 189 | $writer->process($codeCoverage, $path . 'phpunit'); 190 | 191 | if (file_exists($path . '.tar')) { 192 | unlink($path . '.tar'); 193 | } 194 | 195 | $phar = new PharData($path . '.tar'); 196 | $phar->setSignatureAlgorithm(Phar::SHA1); 197 | $files = $phar->buildFromDirectory($path . 'phpunit'); 198 | array_map('unlink', $files); 199 | 200 | if (in_array('GZ', Phar::getSupportedCompression())) { 201 | if (file_exists($path . '.tar.gz')) { 202 | unlink($path . '.tar.gz'); 203 | } 204 | 205 | $phar->compress(\Phar::GZ); 206 | 207 | // close the file so that we can rename it 208 | unset($phar); 209 | 210 | unlink($path . '.tar'); 211 | rename($path . '.tar.gz', $path . '.tar'); 212 | } 213 | 214 | return $path . '.tar'; 215 | } 216 | 217 | function __c3_send_file($filename) 218 | { 219 | if (!headers_sent()) { 220 | readfile($filename); 221 | } 222 | 223 | return __c3_exit(); 224 | } 225 | 226 | /** 227 | * Keep track of the number of running tests 228 | * @param bool $decrease default false. Whether to increase or decrease the counter 229 | */ 230 | function __c3_testcounter($decrease = false) 231 | { 232 | $blockfilename = realpath(C3_CODECOVERAGE_MEDIATE_STORAGE) . DIRECTORY_SEPARATOR . 'block_report'; 233 | $file = fopen($blockfilename, 'c+'); 234 | if (flock($file, LOCK_EX)){ 235 | // 24 bytes is enough to hold largest integer supported in 64 bit systems 236 | $testcounter = intval(fread($file, 24)) + ($decrease ? -1 : 1); 237 | ftruncate($file, 0); 238 | rewind($file); 239 | fwrite($file, $testcounter); 240 | } else { 241 | __c3_error("Failed to acquire write-lock for $blockfilename"); 242 | } 243 | fclose($file); 244 | } 245 | 246 | /** 247 | * @param $filename 248 | * @param bool $lock Lock the file for writing? 249 | * @return [null|PHP_CodeCoverage|\SebastianBergmann\CodeCoverage\CodeCoverage, resource] 250 | */ 251 | function __c3_factory($filename, $lock = false) 252 | { 253 | $file = null; 254 | if ($filename !== null && is_readable($filename)) { 255 | if ($lock) { 256 | $file = fopen($filename, 'r+'); 257 | if (flock($file, LOCK_EX)) { 258 | $phpCoverage = unserialize(stream_get_contents($file)); 259 | } else { 260 | __c3_error("Failed to acquire write-lock for $filename"); 261 | } 262 | } else { 263 | // wait until serialized coverage data of all tests is written to file 264 | $blockfilename = realpath(C3_CODECOVERAGE_MEDIATE_STORAGE) . DIRECTORY_SEPARATOR . 'block_report'; 265 | if (file_exists($blockfilename) && filesize($blockfilename) !== 0) { 266 | $retries = 120; // 30 sec total 267 | while (file_get_contents($blockfilename) !== '0' && --$retries >= 0) { 268 | usleep(250000); // 0.25 sec 269 | } 270 | } 271 | $phpCoverage = unserialize(file_get_contents($filename)); 272 | } 273 | 274 | return [$phpCoverage, $file]; 275 | } 276 | 277 | if (isset($_SERVER['HTTP_X_CODECEPTION_CODECOVERAGE_SUITE'])) { 278 | $suite = $_SERVER['HTTP_X_CODECEPTION_CODECOVERAGE_SUITE']; 279 | try { 280 | $settings = \Codeception\Configuration::suiteSettings($suite, \Codeception\Configuration::config()); 281 | } catch (Exception $e) { 282 | __c3_error($e->getMessage()); 283 | $settings = []; 284 | } 285 | } else { 286 | $settings = \Codeception\Configuration::config(); 287 | } 288 | 289 | $pathCoverage = false; 290 | if (isset($settings['coverage']['path_coverage'])) { 291 | $pathCoverage = (bool)$settings['coverage']['path_coverage']; 292 | } 293 | 294 | if (class_exists(Selector::class)) { 295 | //php-code-coverage >= 9.1.10 296 | $filter = new CodeCoverageFilter(); 297 | if ($pathCoverage) { 298 | $driver = (new Selector())->forLineAndPathCoverage($filter); 299 | } else { 300 | $driver = (new Selector())->forLineCoverage($filter); 301 | } 302 | $phpCoverage = new CodeCoverage($driver, $filter); 303 | } elseif (method_exists(Driver::class, 'forLineCoverage')) { 304 | //php-code-coverage 9.0.0 - 9.1.9 305 | $filter = new CodeCoverageFilter(); 306 | if ($pathCoverage) { 307 | $driver = Driver::forLineAndPathCoverage($filter); 308 | } else { 309 | $driver = Driver::forLineCoverage($filter); 310 | } 311 | $phpCoverage = new CodeCoverage($driver, $filter); 312 | } else { 313 | //php-code-coverage 8 or older 314 | $phpCoverage = new PHP_CodeCoverage(); 315 | } 316 | 317 | try { 318 | \Codeception\Coverage\Filter::setup($phpCoverage) 319 | ->whiteList($settings) 320 | ->blackList($settings); 321 | } catch (Exception $e) { 322 | __c3_error($e->getMessage()); 323 | } 324 | 325 | return [$phpCoverage, $file]; 326 | } 327 | 328 | function __c3_exit() 329 | { 330 | if (!isset($_SERVER['HTTP_X_CODECEPTION_CODECOVERAGE_DEBUG'])) { 331 | exit; 332 | } 333 | return null; 334 | } 335 | 336 | function __c3_clear() 337 | { 338 | \Codeception\Util\FileSystem::doEmptyDir(C3_CODECOVERAGE_MEDIATE_STORAGE); 339 | } 340 | } 341 | 342 | if (!is_dir(C3_CODECOVERAGE_MEDIATE_STORAGE)) { 343 | if (mkdir(C3_CODECOVERAGE_MEDIATE_STORAGE, 0777, true) === false) { 344 | __c3_error('Failed to create directory "' . C3_CODECOVERAGE_MEDIATE_STORAGE . '"'); 345 | } 346 | } 347 | 348 | // evaluate base path for c3-related files 349 | $path = realpath(C3_CODECOVERAGE_MEDIATE_STORAGE) . DIRECTORY_SEPARATOR . 'codecoverage'; 350 | 351 | $requestedC3Report = (strpos($_SERVER['REQUEST_URI'], 'c3/report') !== false); 352 | 353 | $completeReport = $currentReport = $path . '.serialized'; 354 | if ($requestedC3Report) { 355 | set_time_limit(0); 356 | 357 | $route = ltrim(strrchr(rtrim($_SERVER['REQUEST_URI'], '/'), '/'), '/'); 358 | 359 | if ($route === 'clear') { 360 | __c3_clear(); 361 | return __c3_exit(); 362 | } 363 | 364 | list($codeCoverage, ) = __c3_factory($completeReport); 365 | 366 | switch ($route) { 367 | case 'html': 368 | try { 369 | __c3_send_file(__c3_build_html_report($codeCoverage, $path)); 370 | } catch (Exception $e) { 371 | __c3_error($e->getMessage()); 372 | } 373 | return __c3_exit(); 374 | case 'clover': 375 | try { 376 | __c3_send_file(__c3_build_clover_report($codeCoverage, $path)); 377 | } catch (Exception $e) { 378 | __c3_error($e->getMessage()); 379 | } 380 | return __c3_exit(); 381 | case 'crap4j': 382 | try { 383 | __c3_send_file(__c3_build_crap4j_report($codeCoverage, $path)); 384 | } catch (Exception $e) { 385 | __c3_error($e->getMessage()); 386 | } 387 | return __c3_exit(); 388 | case 'serialized': 389 | try { 390 | __c3_send_file($completeReport); 391 | } catch (Exception $e) { 392 | __c3_error($e->getMessage()); 393 | } 394 | return __c3_exit(); 395 | case 'phpunit': 396 | try { 397 | __c3_send_file(__c3_build_phpunit_report($codeCoverage, $path)); 398 | } catch (Exception $e) { 399 | __c3_error($e->getMessage()); 400 | } 401 | return __c3_exit(); 402 | case 'cobertura': 403 | try { 404 | __c3_send_file(__c3_build_cobertura_report($codeCoverage, $path)); 405 | } catch (Exception $e) { 406 | __c3_error($e->getMessage()); 407 | } 408 | return __c3_exit(); 409 | } 410 | } else { 411 | list($codeCoverage, ) = __c3_factory(null); 412 | __c3_testcounter(); 413 | $codeCoverage->start(C3_CODECOVERAGE_TESTNAME); 414 | if (!array_key_exists('HTTP_X_CODECEPTION_CODECOVERAGE_DEBUG', $_SERVER)) { 415 | register_shutdown_function( 416 | function () use ($codeCoverage, $currentReport) { 417 | $codeCoverage->stop(); 418 | if (!file_exists(dirname($currentReport))) { // verify directory exists 419 | if (!mkdir(dirname($currentReport), 0777, true)) { 420 | __c3_error("Can't write CodeCoverage report into $currentReport"); 421 | } 422 | } 423 | 424 | // This will either lock the existing report for writing and return it along with a file pointer, 425 | // or return a fresh PHP_CodeCoverage object without a file pointer. We'll merge the current request 426 | // into that coverage object, write it to disk, and release the lock. By doing this in the end of 427 | // the request, we avoid this scenario, where Request 2 overwrites the changes from Request 1: 428 | // 429 | // Time -> 430 | // Request 1 [ ] 431 | // Request 2 [ ] 432 | // 433 | // In addition, by locking the file for exclusive writing, we make sure no other request try to 434 | // read/write to the file at the same time as this request (leading to a corrupt file). flock() is a 435 | // blocking call, so it waits until an exclusive lock can be acquired before continuing. 436 | 437 | list($existingCodeCoverage, $file) = __c3_factory($currentReport, true); 438 | $existingCodeCoverage->merge($codeCoverage); 439 | 440 | if ($file === null) { 441 | file_put_contents($currentReport, serialize($existingCodeCoverage), LOCK_EX); 442 | } else { 443 | fseek($file, 0); 444 | fwrite($file, serialize($existingCodeCoverage)); 445 | fflush($file); 446 | flock($file, LOCK_UN); 447 | fclose($file); 448 | } 449 | __c3_testcounter(true); 450 | } 451 | ); 452 | } 453 | } 454 | 455 | // @codeCoverageIgnoreEnd 456 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"codeception/c3", 3 | "description":"CodeCoverage collector for Codeception", 4 | "keywords":["code coverage", "CodeCoverage"], 5 | "homepage":"http://codeception.com/", 6 | "type": "composer-plugin", 7 | "minimum-stability": "dev", 8 | "prefer-stable": true, 9 | "license":"MIT", 10 | "authors":[ 11 | { 12 | "name": "Tiger Seo", 13 | "email": "tiger.seo@gmail.com" 14 | }, 15 | { 16 | "name":"Michael Bodnarchuk", 17 | "email":"davert.php@codegyre.com", 18 | "homepage":"http://codegyre.com" 19 | } 20 | ], 21 | "require": { 22 | "php": ">=5.5.0", 23 | "composer-plugin-api": "^1.0 || ^2.0" 24 | }, 25 | "require-dev": { 26 | "composer/composer": "^1.0 || ^2.0" 27 | }, 28 | "extra": { 29 | "class": "Codeception\\c3\\Installer" 30 | }, 31 | "autoload": { 32 | "psr-4": { 33 | "Codeception\\c3\\": "." 34 | } 35 | } 36 | } 37 | --------------------------------------------------------------------------------