c3.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378
  1. <?php
  2. // @codingStandardsIgnoreFile
  3. // @codeCoverageIgnoreStart
  4. /**
  5. * C3 - Codeception Code Coverage
  6. *
  7. * @author tiger
  8. */
  9. // $_SERVER['HTTP_X_CODECEPTION_CODECOVERAGE_DEBUG'] = 1;
  10. if (isset($_COOKIE['CODECEPTION_CODECOVERAGE'])) {
  11. $cookie = json_decode($_COOKIE['CODECEPTION_CODECOVERAGE'], true);
  12. // fix for improperly encoded JSON in Code Coverage cookie with WebDriver.
  13. // @see https://github.com/Codeception/Codeception/issues/874
  14. if (!is_array($cookie)) {
  15. $cookie = json_decode($cookie, true);
  16. }
  17. if ($cookie) {
  18. foreach ($cookie as $key => $value) {
  19. $_SERVER["HTTP_X_CODECEPTION_" . strtoupper($key)] = $value;
  20. }
  21. }
  22. }
  23. if (!array_key_exists('HTTP_X_CODECEPTION_CODECOVERAGE', $_SERVER)) {
  24. return;
  25. }
  26. if (!function_exists('__c3_error')) {
  27. function __c3_error($message)
  28. {
  29. $errorLogFile = defined('C3_CODECOVERAGE_ERROR_LOG_FILE') ?
  30. C3_CODECOVERAGE_ERROR_LOG_FILE :
  31. C3_CODECOVERAGE_MEDIATE_STORAGE . DIRECTORY_SEPARATOR . 'error.txt';
  32. if (is_writable($errorLogFile)) {
  33. file_put_contents($errorLogFile, $message);
  34. } else {
  35. $message = "Could not write error to log file ($errorLogFile), original message: $message";
  36. }
  37. if (!headers_sent()) {
  38. header('X-Codeception-CodeCoverage-Error: ' . str_replace("\n", ' ', $message), true, 500);
  39. }
  40. setcookie('CODECEPTION_CODECOVERAGE_ERROR', $message);
  41. }
  42. }
  43. // phpunit codecoverage shimming
  44. if (!class_exists('PHP_CodeCoverage') and class_exists('SebastianBergmann\CodeCoverage\CodeCoverage')) {
  45. class_alias('SebastianBergmann\CodeCoverage\CodeCoverage', 'PHP_CodeCoverage');
  46. class_alias('SebastianBergmann\CodeCoverage\Report\Text', 'PHP_CodeCoverage_Report_Text');
  47. class_alias('SebastianBergmann\CodeCoverage\Report\PHP', 'PHP_CodeCoverage_Report_PHP');
  48. class_alias('SebastianBergmann\CodeCoverage\Report\Clover', 'PHP_CodeCoverage_Report_Clover');
  49. class_alias('SebastianBergmann\CodeCoverage\Report\Crap4j', 'PHP_CodeCoverage_Report_Crap4j');
  50. class_alias('SebastianBergmann\CodeCoverage\Report\Html\Facade', 'PHP_CodeCoverage_Report_HTML');
  51. class_alias('SebastianBergmann\CodeCoverage\Report\Xml\Facade', 'PHP_CodeCoverage_Report_XML');
  52. class_alias('SebastianBergmann\CodeCoverage\Exception', 'PHP_CodeCoverage_Exception');
  53. }
  54. // phpunit version
  55. if (!class_exists('PHPUnit_Runner_Version') && class_exists('PHPUnit\Runner\Version')) {
  56. class_alias('PHPUnit\Runner\Version', 'PHPUnit_Runner_Version');
  57. }
  58. // Autoload Codeception classes
  59. if (!class_exists('\\Codeception\\Codecept')) {
  60. if (file_exists(__DIR__ . '/codecept.phar')) {
  61. require_once 'phar://' . __DIR__ . '/codecept.phar/autoload.php';
  62. } elseif (stream_resolve_include_path(__DIR__ . '/vendor/autoload.php')) {
  63. require_once __DIR__ . '/vendor/autoload.php';
  64. // Required to load some methods only available at codeception/autoload.php
  65. if (stream_resolve_include_path(__DIR__ . '/vendor/codeception/codeception/autoload.php')) {
  66. require_once __DIR__ . '/vendor/codeception/codeception/autoload.php';
  67. }
  68. } elseif (stream_resolve_include_path('Codeception/autoload.php')) {
  69. require_once 'Codeception/autoload.php';
  70. } else {
  71. __c3_error('Codeception is not loaded. Please check that either PHAR or Composer package can be used');
  72. }
  73. }
  74. // Load Codeception Config
  75. $config_dist_file = realpath(__DIR__) . DIRECTORY_SEPARATOR . 'codeception.dist.yml';
  76. $config_file = realpath(__DIR__) . DIRECTORY_SEPARATOR . 'codeception.yml';
  77. if (isset($_SERVER['HTTP_X_CODECEPTION_CODECOVERAGE_CONFIG'])) {
  78. $config_file = realpath(__DIR__) . DIRECTORY_SEPARATOR . $_SERVER['HTTP_X_CODECEPTION_CODECOVERAGE_CONFIG'];
  79. }
  80. if (file_exists($config_file)) {
  81. // Use codeception.yml for configuration.
  82. } elseif (file_exists($config_dist_file)) {
  83. // Use codeception.dist.yml for configuration.
  84. $config_file = $config_dist_file;
  85. } else {
  86. __c3_error(sprintf("Codeception config file '%s' not found", $config_file));
  87. }
  88. try {
  89. \Codeception\Configuration::config($config_file);
  90. } catch (\Exception $e) {
  91. __c3_error($e->getMessage());
  92. }
  93. if (!defined('C3_CODECOVERAGE_MEDIATE_STORAGE')) {
  94. // workaround for 'zend_mm_heap corrupted' problem
  95. gc_disable();
  96. $memoryLimit = ini_get('memory_limit');
  97. $requiredMemory = '384M';
  98. if ((substr($memoryLimit, -1) === 'M' && (int)$memoryLimit < (int)$requiredMemory)
  99. || (substr($memoryLimit, -1) === 'K' && (int)$memoryLimit < (int)$requiredMemory * 1024)
  100. || (ctype_digit($memoryLimit) && (int)$memoryLimit < (int)$requiredMemory * 1024 * 1024)
  101. ) {
  102. ini_set('memory_limit', $requiredMemory);
  103. }
  104. define('C3_CODECOVERAGE_MEDIATE_STORAGE', Codeception\Configuration::logDir() . 'c3tmp');
  105. define('C3_CODECOVERAGE_PROJECT_ROOT', Codeception\Configuration::projectDir());
  106. define('C3_CODECOVERAGE_TESTNAME', $_SERVER['HTTP_X_CODECEPTION_CODECOVERAGE']);
  107. function __c3_build_html_report(PHP_CodeCoverage $codeCoverage, $path)
  108. {
  109. $writer = new PHP_CodeCoverage_Report_HTML();
  110. $writer->process($codeCoverage, $path . 'html');
  111. if (file_exists($path . '.tar')) {
  112. unlink($path . '.tar');
  113. }
  114. $phar = new PharData($path . '.tar');
  115. $phar->setSignatureAlgorithm(Phar::SHA1);
  116. $files = $phar->buildFromDirectory($path . 'html');
  117. array_map('unlink', $files);
  118. if (in_array('GZ', Phar::getSupportedCompression())) {
  119. if (file_exists($path . '.tar.gz')) {
  120. unlink($path . '.tar.gz');
  121. }
  122. $phar->compress(\Phar::GZ);
  123. // close the file so that we can rename it
  124. unset($phar);
  125. unlink($path . '.tar');
  126. rename($path . '.tar.gz', $path . '.tar');
  127. }
  128. return $path . '.tar';
  129. }
  130. function __c3_build_clover_report(PHP_CodeCoverage $codeCoverage, $path)
  131. {
  132. $writer = new PHP_CodeCoverage_Report_Clover();
  133. $writer->process($codeCoverage, $path . '.clover.xml');
  134. return $path . '.clover.xml';
  135. }
  136. function __c3_build_crap4j_report(PHP_CodeCoverage $codeCoverage, $path)
  137. {
  138. $writer = new PHP_CodeCoverage_Report_Crap4j();
  139. $writer->process($codeCoverage, $path . '.crap4j.xml');
  140. return $path . '.crap4j.xml';
  141. }
  142. function __c3_build_phpunit_report(PHP_CodeCoverage $codeCoverage, $path)
  143. {
  144. $writer = new PHP_CodeCoverage_Report_XML(\PHPUnit_Runner_Version::id());
  145. $writer->process($codeCoverage, $path . 'phpunit');
  146. if (file_exists($path . '.tar')) {
  147. unlink($path . '.tar');
  148. }
  149. $phar = new PharData($path . '.tar');
  150. $phar->setSignatureAlgorithm(Phar::SHA1);
  151. $files = $phar->buildFromDirectory($path . 'phpunit');
  152. array_map('unlink', $files);
  153. if (in_array('GZ', Phar::getSupportedCompression())) {
  154. if (file_exists($path . '.tar.gz')) {
  155. unlink($path . '.tar.gz');
  156. }
  157. $phar->compress(\Phar::GZ);
  158. // close the file so that we can rename it
  159. unset($phar);
  160. unlink($path . '.tar');
  161. rename($path . '.tar.gz', $path . '.tar');
  162. }
  163. return $path . '.tar';
  164. }
  165. function __c3_send_file($filename)
  166. {
  167. if (!headers_sent()) {
  168. readfile($filename);
  169. }
  170. return __c3_exit();
  171. }
  172. /**
  173. * @param $filename
  174. * @param bool $lock Lock the file for writing?
  175. * @return [null|PHP_CodeCoverage|\SebastianBergmann\CodeCoverage\CodeCoverage, resource]
  176. */
  177. function __c3_factory($filename, $lock=false)
  178. {
  179. $file = null;
  180. if ($filename !== null && is_readable($filename)) {
  181. if ($lock) {
  182. $file = fopen($filename, 'r+');
  183. if (flock($file, LOCK_EX)) {
  184. $phpCoverage = unserialize(stream_get_contents($file));
  185. } else {
  186. __c3_error("Failed to acquire write-lock for $filename");
  187. }
  188. } else {
  189. $phpCoverage = unserialize(file_get_contents($filename));
  190. }
  191. return array($phpCoverage, $file);
  192. } else {
  193. $phpCoverage = new PHP_CodeCoverage();
  194. }
  195. if (isset($_SERVER['HTTP_X_CODECEPTION_CODECOVERAGE_SUITE'])) {
  196. $suite = $_SERVER['HTTP_X_CODECEPTION_CODECOVERAGE_SUITE'];
  197. try {
  198. $settings = \Codeception\Configuration::suiteSettings($suite, \Codeception\Configuration::config());
  199. } catch (Exception $e) {
  200. __c3_error($e->getMessage());
  201. }
  202. } else {
  203. $settings = \Codeception\Configuration::config();
  204. }
  205. try {
  206. \Codeception\Coverage\Filter::setup($phpCoverage)
  207. ->whiteList($settings)
  208. ->blackList($settings);
  209. } catch (Exception $e) {
  210. __c3_error($e->getMessage());
  211. }
  212. return array($phpCoverage, $file);
  213. }
  214. function __c3_exit()
  215. {
  216. if (!isset($_SERVER['HTTP_X_CODECEPTION_CODECOVERAGE_DEBUG'])) {
  217. exit;
  218. }
  219. return null;
  220. }
  221. function __c3_clear()
  222. {
  223. \Codeception\Util\FileSystem::doEmptyDir(C3_CODECOVERAGE_MEDIATE_STORAGE);
  224. }
  225. }
  226. if (!is_dir(C3_CODECOVERAGE_MEDIATE_STORAGE)) {
  227. if (mkdir(C3_CODECOVERAGE_MEDIATE_STORAGE, 0777, true) === false) {
  228. __c3_error('Failed to create directory "' . C3_CODECOVERAGE_MEDIATE_STORAGE . '"');
  229. }
  230. }
  231. // evaluate base path for c3-related files
  232. $path = realpath(C3_CODECOVERAGE_MEDIATE_STORAGE) . DIRECTORY_SEPARATOR . 'codecoverage';
  233. $requested_c3_report = (strpos($_SERVER['REQUEST_URI'], 'c3/report') !== false);
  234. $complete_report = $current_report = $path . '.serialized';
  235. if ($requested_c3_report) {
  236. set_time_limit(0);
  237. $route = ltrim(strrchr($_SERVER['REQUEST_URI'], '/'), '/');
  238. if ($route === 'clear') {
  239. __c3_clear();
  240. return __c3_exit();
  241. }
  242. list($codeCoverage, ) = __c3_factory($complete_report);
  243. switch ($route) {
  244. case 'html':
  245. try {
  246. __c3_send_file(__c3_build_html_report($codeCoverage, $path));
  247. } catch (Exception $e) {
  248. __c3_error($e->getMessage());
  249. }
  250. return __c3_exit();
  251. case 'clover':
  252. try {
  253. __c3_send_file(__c3_build_clover_report($codeCoverage, $path));
  254. } catch (Exception $e) {
  255. __c3_error($e->getMessage());
  256. }
  257. return __c3_exit();
  258. case 'crap4j':
  259. try {
  260. __c3_send_file(__c3_build_crap4j_report($codeCoverage, $path));
  261. } catch (Exception $e) {
  262. __c3_error($e->getMessage());
  263. }
  264. return __c3_exit();
  265. case 'serialized':
  266. try {
  267. __c3_send_file($complete_report);
  268. } catch (Exception $e) {
  269. __c3_error($e->getMessage());
  270. }
  271. return __c3_exit();
  272. case 'phpunit':
  273. try {
  274. __c3_send_file(__c3_build_phpunit_report($codeCoverage, $path));
  275. } catch (Exception $e) {
  276. __c3_error($e->getMessage());
  277. }
  278. return __c3_exit();
  279. }
  280. } else {
  281. list($codeCoverage, ) = __c3_factory(null);
  282. $codeCoverage->start(C3_CODECOVERAGE_TESTNAME);
  283. if (!array_key_exists('HTTP_X_CODECEPTION_CODECOVERAGE_DEBUG', $_SERVER)) {
  284. register_shutdown_function(
  285. function () use ($codeCoverage, $current_report) {
  286. $codeCoverage->stop();
  287. if (!file_exists(dirname($current_report))) { // verify directory exists
  288. if (!mkdir(dirname($current_report), 0777, true)) {
  289. __c3_error("Can't write CodeCoverage report into $current_report");
  290. }
  291. }
  292. // This will either lock the existing report for writing and return it along with a file pointer,
  293. // or return a fresh PHP_CodeCoverage object without a file pointer. We'll merge the current request
  294. // into that coverage object, write it to disk, and release the lock. By doing this in the end of
  295. // the request, we avoid this scenario, where Request 2 overwrites the changes from Request 1:
  296. //
  297. // Time ->
  298. // Request 1 [ <read> <write> ]
  299. // Request 2 [ <read> <write> ]
  300. //
  301. // In addition, by locking the file for exclusive writing, we make sure no other request try to
  302. // read/write to the file at the same time as this request (leading to a corrupt file). flock() is a
  303. // blocking call, so it waits until an exclusive lock can be acquired before continuing.
  304. list($existingCodeCoverage, $file) = __c3_factory($current_report, true);
  305. $existingCodeCoverage->merge($codeCoverage);
  306. if ($file === null) {
  307. file_put_contents($current_report, serialize($existingCodeCoverage), LOCK_EX);
  308. } else {
  309. fseek($file, 0);
  310. fwrite($file, serialize($existingCodeCoverage));
  311. fflush($file);
  312. flock($file, LOCK_UN);
  313. fclose($file);
  314. }
  315. }
  316. );
  317. }
  318. }
  319. // @codeCoverageIgnoreEnd