PageRenderTime 27ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 0ms

/src/Codeception/Coverage/Subscriber/LocalServer.php

http://github.com/Codeception/Codeception
PHP | 296 lines | 237 code | 31 blank | 28 comment | 27 complexity | 1dccbba2066d4a17195905b0d5e4b286 MD5 | raw file
  1. <?php
  2. namespace Codeception\Coverage\Subscriber;
  3. use Codeception\Configuration;
  4. use Codeception\Coverage\SuiteSubscriber;
  5. use Codeception\Event\StepEvent;
  6. use Codeception\Event\SuiteEvent;
  7. use Codeception\Event\TestEvent;
  8. use Codeception\Events;
  9. use Codeception\Exception\ModuleException;
  10. use Codeception\Exception\RemoteException;
  11. use Facebook\WebDriver\Exception\NoSuchAlertException;
  12. /**
  13. * When collecting code coverage data from local server HTTP requests are sent to c3.php file.
  14. * Coverage Collection is started by sending cookies/headers.
  15. * Result is taken from the local file and merged with local code coverage results.
  16. *
  17. * Class LocalServer
  18. * @package Codeception\Coverage\Subscriber
  19. */
  20. class LocalServer extends SuiteSubscriber
  21. {
  22. // headers
  23. const COVERAGE_HEADER = 'X-Codeception-CodeCoverage';
  24. const COVERAGE_HEADER_ERROR = 'X-Codeception-CodeCoverage-Error';
  25. const COVERAGE_HEADER_CONFIG = 'X-Codeception-CodeCoverage-Config';
  26. const COVERAGE_HEADER_SUITE = 'X-Codeception-CodeCoverage-Suite';
  27. // cookie names
  28. const COVERAGE_COOKIE = 'CODECEPTION_CODECOVERAGE';
  29. const COVERAGE_COOKIE_ERROR = 'CODECEPTION_CODECOVERAGE_ERROR';
  30. protected $suiteName;
  31. protected $c3Access = [
  32. 'http' => [
  33. 'method' => "GET",
  34. 'header' => ''
  35. ]
  36. ];
  37. /**
  38. * @var \Codeception\Lib\Interfaces\Web
  39. */
  40. protected $module;
  41. public static $events = [
  42. Events::SUITE_BEFORE => 'beforeSuite',
  43. Events::TEST_BEFORE => 'beforeTest',
  44. Events::STEP_AFTER => 'afterStep',
  45. Events::SUITE_AFTER => 'afterSuite',
  46. ];
  47. protected function isEnabled()
  48. {
  49. return $this->module && !$this->settings['remote'] && $this->settings['enabled'];
  50. }
  51. public function beforeSuite(SuiteEvent $e)
  52. {
  53. $this->module = $this->getServerConnectionModule($e->getSuite()->getModules());
  54. $this->applySettings($e->getSettings());
  55. if (!$this->isEnabled()) {
  56. return;
  57. }
  58. $this->suiteName = $e->getSuite()->getBaseName();
  59. if ($this->settings['remote_config']) {
  60. $this->addC3AccessHeader(self::COVERAGE_HEADER_CONFIG, $this->settings['remote_config']);
  61. $knock = $this->c3Request('clear');
  62. if ($knock === false) {
  63. throw new RemoteException(
  64. '
  65. CodeCoverage Error.
  66. Check the file "c3.php" is included in your application.
  67. We tried to access "/c3/report/clear" but this URI was not accessible.
  68. You can review actual error messages in c3tmp dir.
  69. '
  70. );
  71. }
  72. }
  73. }
  74. public function beforeTest(TestEvent $e)
  75. {
  76. if (!$this->isEnabled()) {
  77. return;
  78. }
  79. $this->startCoverageCollection($e->getTest()->getName());
  80. }
  81. public function afterStep(StepEvent $e)
  82. {
  83. if (!$this->isEnabled()) {
  84. return;
  85. }
  86. $this->fetchErrors();
  87. }
  88. public function afterSuite(SuiteEvent $e)
  89. {
  90. if (!$this->isEnabled()) {
  91. return;
  92. }
  93. $coverageFile = Configuration::outputDir() . 'c3tmp/codecoverage.serialized';
  94. $retries = 5;
  95. while (!file_exists($coverageFile) && --$retries >= 0) {
  96. usleep(0.5 * 1000000); // 0.5 sec
  97. }
  98. if (!file_exists($coverageFile)) {
  99. if (file_exists(Configuration::outputDir() . 'c3tmp/error.txt')) {
  100. throw new \RuntimeException(file_get_contents(Configuration::outputDir() . 'c3tmp/error.txt'));
  101. }
  102. return;
  103. }
  104. $contents = file_get_contents($coverageFile);
  105. $coverage = @unserialize($contents);
  106. if ($coverage === false) {
  107. return;
  108. }
  109. $this->preProcessCoverage($coverage)
  110. ->mergeToPrint($coverage);
  111. }
  112. /**
  113. * Allows Translating Remote Paths To Local (IE: When Using Docker)
  114. *
  115. * @param \SebastianBergmann\CodeCoverage\CodeCoverage $coverage
  116. * @return $this
  117. */
  118. protected function preProcessCoverage($coverage)
  119. {
  120. //Only Process If Work Directory Set
  121. if ($this->settings['work_dir'] === null) {
  122. return $this;
  123. }
  124. $workDir = rtrim($this->settings['work_dir'], '/\\') . DIRECTORY_SEPARATOR;
  125. $projectDir = Configuration::projectDir();
  126. $data = $coverage->getData(true); //We only want covered files, not all whitelisted ones.
  127. codecept_debug("Replacing all instances of {$workDir} with {$projectDir}");
  128. foreach ($data as $path => $datum) {
  129. unset($data[$path]);
  130. $path = str_replace($workDir, $projectDir, $path);
  131. $data[$path] = $datum;
  132. }
  133. $coverage->setData($data);
  134. return $this;
  135. }
  136. protected function c3Request($action)
  137. {
  138. $this->addC3AccessHeader(self::COVERAGE_HEADER, 'remote-access');
  139. $context = stream_context_create($this->c3Access);
  140. $c3Url = $this->settings['c3_url'] ? $this->settings['c3_url'] : $this->module->_getUrl();
  141. $contents = file_get_contents($c3Url . '/c3/report/' . $action, false, $context);
  142. $okHeaders = array_filter(
  143. $http_response_header,
  144. function ($h) {
  145. return preg_match('~^HTTP(.*?)\s200~', $h);
  146. }
  147. );
  148. if (empty($okHeaders)) {
  149. throw new RemoteException("Request was not successful. See response header: " . $http_response_header[0]);
  150. }
  151. if ($contents === false) {
  152. $this->getRemoteError($http_response_header);
  153. }
  154. return $contents;
  155. }
  156. protected function startCoverageCollection($testName)
  157. {
  158. $value = [
  159. 'CodeCoverage' => $testName,
  160. 'CodeCoverage_Suite' => $this->suiteName,
  161. 'CodeCoverage_Config' => $this->settings['remote_config']
  162. ];
  163. $value = json_encode($value);
  164. if ($this->module instanceof \Codeception\Module\WebDriver) {
  165. $this->module->amOnPage('/');
  166. }
  167. $cookieDomain = isset($this->settings['cookie_domain']) ? $this->settings['cookie_domain'] : null;
  168. if (!$cookieDomain) {
  169. $c3Url = parse_url($this->settings['c3_url'] ? $this->settings['c3_url'] : $this->module->_getUrl());
  170. // we need to separate coverage cookies by host; we can't separate cookies by port.
  171. $cookieDomain = isset($c3Url['host']) ? $c3Url['host'] : 'localhost';
  172. }
  173. $cookieParams = [];
  174. if ($cookieDomain !== 'localhost') {
  175. $cookieParams['domain'] = $cookieDomain;
  176. }
  177. $this->module->setCookie(self::COVERAGE_COOKIE, $value, $cookieParams);
  178. // putting in configuration ensures the cookie is used for all sessions of a MultiSession test
  179. $cookies = $this->module->_getConfig('cookies');
  180. if (!$cookies || !is_array($cookies)) {
  181. $cookies = [];
  182. }
  183. $found = false;
  184. foreach ($cookies as &$cookie) {
  185. if (!is_array($cookie) || !array_key_exists('Name', $cookie) || !array_key_exists('Value', $cookie)) {
  186. // \Codeception\Lib\InnerBrowser will complain about this
  187. continue;
  188. }
  189. if ($cookie['Name'] === self::COVERAGE_COOKIE) {
  190. $found = true;
  191. $cookie['Value'] = $value;
  192. break;
  193. }
  194. }
  195. unset($cookie);
  196. if (!$found) {
  197. $cookies[] = [
  198. 'Name' => self::COVERAGE_COOKIE,
  199. 'Value' => $value
  200. ];
  201. }
  202. $this->module->_setConfig(['cookies' => $cookies]);
  203. }
  204. protected function fetchErrors()
  205. {
  206. // Calling grabCookie() while an alert is present dismisses the alert
  207. // @see https://github.com/Codeception/Codeception/issues/1485
  208. if ($this->module instanceof \Codeception\Module\WebDriver) {
  209. try {
  210. $alert = $this->module->webDriver->switchTo()->alert();
  211. $alert->getText();
  212. // If this succeeds an alert is present, abort
  213. return;
  214. } catch (NoSuchAlertException $e) {
  215. // No alert present, continue
  216. }
  217. }
  218. try {
  219. $error = $this->module->grabCookie(self::COVERAGE_COOKIE_ERROR);
  220. } catch (ModuleException $e) {
  221. // when a new session is started we can't get cookies because there is no
  222. // current page, but there can be no code coverage error either
  223. $error = null;
  224. }
  225. if (!empty($error)) {
  226. $this->module->resetCookie(self::COVERAGE_COOKIE_ERROR);
  227. throw new RemoteException($error);
  228. }
  229. }
  230. protected function getRemoteError($headers)
  231. {
  232. foreach ($headers as $header) {
  233. if (strpos($header, self::COVERAGE_HEADER_ERROR) === 0) {
  234. throw new RemoteException($header);
  235. }
  236. }
  237. }
  238. protected function addC3AccessHeader($header, $value)
  239. {
  240. $headerString = "$header: $value\r\n";
  241. if (strpos($this->c3Access['http']['header'], $headerString) === false) {
  242. $this->c3Access['http']['header'] .= $headerString;
  243. }
  244. }
  245. protected function applySettings($settings)
  246. {
  247. parent::applySettings($settings);
  248. if (isset($settings['coverage']['remote_context_options'])) {
  249. $this->c3Access = array_replace_recursive($this->c3Access, $settings['coverage']['remote_context_options']);
  250. }
  251. }
  252. }