1
|
|
|
<?php |
2
|
|
|
namespace Codeception\Coverage\Subscriber; |
3
|
|
|
|
4
|
|
|
use Codeception\Configuration; |
5
|
|
|
use Codeception\Coverage\SuiteSubscriber; |
6
|
|
|
use Codeception\Event\StepEvent; |
7
|
|
|
use Codeception\Event\SuiteEvent; |
8
|
|
|
use Codeception\Event\TestEvent; |
9
|
|
|
use Codeception\Events; |
10
|
|
|
use Codeception\Exception\ModuleException; |
11
|
|
|
use Codeception\Exception\RemoteException; |
12
|
|
|
|
13
|
|
|
/** |
14
|
|
|
* When collecting code coverage data from local server HTTP requests are sent to c3.php file. |
15
|
|
|
* Coverage Collection is started by sending cookies/headers. |
16
|
|
|
* Result is taken from the local file and merged with local code coverage results. |
17
|
|
|
* |
18
|
|
|
* Class LocalServer |
19
|
|
|
* @package Codeception\Coverage\Subscriber |
20
|
|
|
*/ |
21
|
|
|
class LocalServer extends SuiteSubscriber |
22
|
|
|
{ |
23
|
|
|
// headers |
24
|
|
|
const COVERAGE_HEADER = 'X-Codeception-CodeCoverage'; |
25
|
|
|
const COVERAGE_HEADER_ERROR = 'X-Codeception-CodeCoverage-Error'; |
26
|
|
|
const COVERAGE_HEADER_CONFIG = 'X-Codeception-CodeCoverage-Config'; |
27
|
|
|
const COVERAGE_HEADER_SUITE = 'X-Codeception-CodeCoverage-Suite'; |
28
|
|
|
|
29
|
|
|
// cookie names |
30
|
|
|
const COVERAGE_COOKIE = 'CODECEPTION_CODECOVERAGE'; |
31
|
|
|
const COVERAGE_COOKIE_ERROR = 'CODECEPTION_CODECOVERAGE_ERROR'; |
32
|
|
|
|
33
|
|
|
protected $suiteName; |
34
|
|
|
protected $c3Access = [ |
35
|
|
|
'http' => [ |
36
|
|
|
'method' => "GET", |
37
|
|
|
'header' => '' |
38
|
|
|
] |
39
|
|
|
]; |
40
|
|
|
|
41
|
|
|
/** |
42
|
|
|
* @var \Codeception\Lib\Interfaces\Web |
43
|
|
|
*/ |
44
|
|
|
protected $module; |
45
|
|
|
|
46
|
|
|
public static $events = [ |
47
|
|
|
Events::SUITE_BEFORE => 'beforeSuite', |
48
|
|
|
Events::TEST_BEFORE => 'beforeTest', |
49
|
|
|
Events::STEP_AFTER => 'afterStep', |
50
|
|
|
Events::SUITE_AFTER => 'afterSuite', |
51
|
|
|
]; |
52
|
|
|
|
53
|
|
|
protected function isEnabled() |
54
|
|
|
{ |
55
|
|
|
return $this->module && !$this->settings['remote'] && $this->settings['enabled']; |
56
|
|
|
} |
57
|
|
|
|
58
|
|
|
public function beforeSuite(SuiteEvent $e) |
59
|
|
|
{ |
60
|
|
|
$this->module = $this->getServerConnectionModule($e->getSuite()->getModules()); |
|
|
|
|
61
|
|
|
$this->applySettings($e->getSettings()); |
62
|
|
|
if (!$this->isEnabled()) { |
63
|
|
|
return; |
64
|
|
|
} |
65
|
|
|
|
66
|
|
|
$this->suiteName = $e->getSuite()->getBaseName(); |
67
|
|
|
|
68
|
|
|
if ($this->settings['remote_config']) { |
69
|
|
|
$this->addC3AccessHeader(self::COVERAGE_HEADER_CONFIG, $this->settings['remote_config']); |
70
|
|
|
} |
71
|
|
|
|
72
|
|
|
$knock = $this->c3Request('clear'); |
73
|
|
|
if ($knock === false) { |
74
|
|
|
throw new RemoteException( |
75
|
|
|
' |
76
|
|
|
CodeCoverage Error. |
77
|
|
|
Check the file "c3.php" is included in your application. |
78
|
|
|
We tried to access "/c3/report/clear" but this URI was not accessible. |
79
|
|
|
You can review actual error messages in c3tmp dir. |
80
|
|
|
' |
81
|
|
|
); |
82
|
|
|
} |
83
|
|
|
} |
84
|
|
|
|
85
|
|
|
public function beforeTest(TestEvent $e) |
86
|
|
|
{ |
87
|
|
|
if (!$this->isEnabled()) { |
88
|
|
|
return; |
89
|
|
|
} |
90
|
|
|
$this->startCoverageCollection($e->getTest()->getName()); |
91
|
|
|
} |
92
|
|
|
|
93
|
|
|
public function afterStep(StepEvent $e) |
|
|
|
|
94
|
|
|
{ |
95
|
|
|
if (!$this->isEnabled()) { |
96
|
|
|
return; |
97
|
|
|
} |
98
|
|
|
$this->fetchErrors(); |
99
|
|
|
} |
100
|
|
|
|
101
|
|
|
public function afterSuite(SuiteEvent $e) |
102
|
|
|
{ |
103
|
|
|
if (!$this->isEnabled()) { |
104
|
|
|
return; |
105
|
|
|
} |
106
|
|
|
$coverageFile = Configuration::outputDir() . 'c3tmp/codecoverage.serialized'; |
107
|
|
|
|
108
|
|
|
$retries = 5; |
109
|
|
|
while (!file_exists($coverageFile) && --$retries >= 0) { |
110
|
|
|
usleep(0.5 * 1000000); // 0.5 sec |
111
|
|
|
} |
112
|
|
|
|
113
|
|
|
if (!file_exists($coverageFile)) { |
114
|
|
|
if (file_exists(Configuration::outputDir() . 'c3tmp/error.txt')) { |
115
|
|
|
throw new \RuntimeException(file_get_contents(Configuration::outputDir() . 'c3tmp/error.txt')); |
116
|
|
|
} |
117
|
|
|
return; |
118
|
|
|
} |
119
|
|
|
|
120
|
|
|
$contents = file_get_contents($coverageFile); |
121
|
|
|
$coverage = @unserialize($contents); |
122
|
|
|
if ($coverage === false) { |
123
|
|
|
return; |
124
|
|
|
} |
125
|
|
|
$this->mergeToPrint($coverage); |
126
|
|
|
} |
127
|
|
|
|
128
|
|
|
protected function c3Request($action) |
129
|
|
|
{ |
130
|
|
|
$this->addC3AccessHeader(self::COVERAGE_HEADER, 'remote-access'); |
131
|
|
|
$context = stream_context_create($this->c3Access); |
132
|
|
|
$c3Url = $this->settings['c3_url'] ? $this->settings['c3_url'] : $this->module->_getUrl(); |
|
|
|
|
133
|
|
|
$contents = file_get_contents($c3Url . '/c3/report/' . $action, false, $context); |
134
|
|
|
|
135
|
|
|
$okHeaders = array_filter( |
136
|
|
|
$http_response_header, |
137
|
|
|
function ($h) { |
138
|
|
|
return preg_match('~^HTTP(.*?)\s200~', $h); |
139
|
|
|
} |
140
|
|
|
); |
141
|
|
|
if (empty($okHeaders)) { |
142
|
|
|
throw new RemoteException("Request was not successful. See response header: " . $http_response_header[0]); |
143
|
|
|
} |
144
|
|
|
if ($contents === false) { |
145
|
|
|
$this->getRemoteError($http_response_header); |
146
|
|
|
} |
147
|
|
|
return $contents; |
148
|
|
|
} |
149
|
|
|
|
150
|
|
|
protected function startCoverageCollection($testName) |
151
|
|
|
{ |
152
|
|
|
$value = [ |
153
|
|
|
'CodeCoverage' => $testName, |
154
|
|
|
'CodeCoverage_Suite' => $this->suiteName, |
155
|
|
|
'CodeCoverage_Config' => $this->settings['remote_config'] |
156
|
|
|
]; |
157
|
|
|
$value = json_encode($value); |
158
|
|
|
|
159
|
|
|
if ($this->module instanceof \Codeception\Module\WebDriver) { |
160
|
|
|
$this->module->amOnPage('/'); |
161
|
|
|
} |
162
|
|
|
|
163
|
|
|
$c3Url = parse_url($this->settings['c3_url'] ? $this->settings['c3_url'] : $this->module->_getUrl()); |
|
|
|
|
164
|
|
|
|
165
|
|
|
// we need to separate coverage cookies by host; we can't separate cookies by port. |
166
|
|
|
$c3Host = isset($c3Url['host']) ? $c3Url['host'] : 'localhost'; |
167
|
|
|
|
168
|
|
|
$this->module->setCookie(self::COVERAGE_COOKIE, $value, ['domain' => $c3Host]); |
169
|
|
|
|
170
|
|
|
// putting in configuration ensures the cookie is used for all sessions of a MultiSession test |
171
|
|
|
|
172
|
|
|
$cookies = $this->module->_getConfig('cookies'); |
173
|
|
|
if (!$cookies || !is_array($cookies)) { |
174
|
|
|
$cookies = []; |
175
|
|
|
} |
176
|
|
|
|
177
|
|
|
$found = false; |
178
|
|
|
foreach ($cookies as &$cookie) { |
179
|
|
View Code Duplication |
if (!is_array($cookie) || !array_key_exists('Name', $cookie) || !array_key_exists('Value', $cookie)) { |
|
|
|
|
180
|
|
|
// \Codeception\Lib\InnerBrowser will complain about this |
181
|
|
|
continue; |
182
|
|
|
} |
183
|
|
|
if ($cookie['Name'] === self::COVERAGE_COOKIE) { |
184
|
|
|
$found = true; |
185
|
|
|
$cookie['Value'] = $value; |
186
|
|
|
break; |
187
|
|
|
} |
188
|
|
|
} |
189
|
|
|
if (!$found) { |
190
|
|
|
$cookies[] = [ |
191
|
|
|
'Name' => self::COVERAGE_COOKIE, |
192
|
|
|
'Value' => $value |
193
|
|
|
]; |
194
|
|
|
} |
195
|
|
|
|
196
|
|
|
$this->module->_setConfig(['cookies' => $cookies]); |
197
|
|
|
} |
198
|
|
|
|
199
|
|
|
protected function fetchErrors() |
200
|
|
|
{ |
201
|
|
|
try { |
202
|
|
|
$error = $this->module->grabCookie(self::COVERAGE_COOKIE_ERROR); |
203
|
|
|
} catch (ModuleException $e) { |
204
|
|
|
// when a new session is started we can't get cookies because there is no |
205
|
|
|
// current page, but there can be no code coverage error either |
206
|
|
|
$error = null; |
207
|
|
|
} |
208
|
|
|
if (!empty($error)) { |
209
|
|
|
$this->module->resetCookie(self::COVERAGE_COOKIE_ERROR); |
210
|
|
|
throw new RemoteException($error); |
211
|
|
|
} |
212
|
|
|
} |
213
|
|
|
|
214
|
|
|
protected function getRemoteError($headers) |
215
|
|
|
{ |
216
|
|
|
foreach ($headers as $header) { |
217
|
|
|
if (strpos($header, self::COVERAGE_HEADER_ERROR) === 0) { |
218
|
|
|
throw new RemoteException($header); |
219
|
|
|
} |
220
|
|
|
} |
221
|
|
|
} |
222
|
|
|
|
223
|
|
|
protected function addC3AccessHeader($header, $value) |
224
|
|
|
{ |
225
|
|
|
$headerString = "$header: $value\r\n"; |
226
|
|
|
if (strpos($this->c3Access['http']['header'], $headerString) === false) { |
227
|
|
|
$this->c3Access['http']['header'] .= $headerString; |
228
|
|
|
} |
229
|
|
|
} |
230
|
|
|
|
231
|
|
|
protected function applySettings($settings) |
232
|
|
|
{ |
233
|
|
|
parent::applySettings($settings); |
234
|
|
|
if (isset($settings['coverage']['remote_context_options'])) { |
235
|
|
|
$this->c3Access = array_replace_recursive($this->c3Access, $settings['coverage']['remote_context_options']); |
236
|
|
|
} |
237
|
|
|
} |
238
|
|
|
} |
239
|
|
|
|
Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.
For example, imagine you have a variable
$accountId
that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to theid
property of an instance of theAccount
class. This class holds a proper account, so the id value must no longer be false.Either this assignment is in error or a type check should be added for that assignment.