84% of 1130OPs |
95% of 201Lines |
70% of 140Branches |
1% of 4124Paths |
# | |
---|---|
1 |
<?php |
2 |
|
3 |
namespace mageekguy\atoum\report\fields\runner\coverage; |
4 |
|
5 |
require_once __DIR__ . '/../../../../../constants.php'; |
6 |
|
7 |
use |
8 |
mageekguy\atoum, |
9 |
mageekguy\atoum\report, |
10 |
mageekguy\atoum\template, |
11 |
mageekguy\atoum\exceptions, |
12 |
mageekguy\atoum\cli\prompt, |
13 |
mageekguy\atoum\cli\colorizer |
14 |
; |
15 |
|
16 |
class html extends report\fields\runner\coverage\cli |
17 |
{ |
18 |
const htmlExtensionFile = '.html'; |
19 |
|
20 |
protected $urlPrompt = null; |
21 |
protected $urlColorizer = null; |
22 |
protected $rootUrl = ''; |
23 |
protected $projectName = ''; |
24 |
protected $templatesDirectory = null; |
25 |
protected $destinationDirectory = null; |
26 |
protected $templateParser = null; |
27 |
protected $reflectionClassInjector = null; |
28 |
protected $bootstrapFile = null; |
29 |
|
30 |
public function __construct($projectName, $destinationDirectory)100% |
31 |
{ |
32 |
parent::__construct(); |
33 |
|
34 |
$this |
35 |
->setProjectName($projectName) |
36 |
->setDestinationDirectory($destinationDirectory) |
37 |
->setUrlPrompt() |
38 |
->setUrlColorizer() |
39 |
->setTemplatesDirectory() |
40 |
->setTemplateParser() |
41 |
->setRootUrl('/') |
42 |
; |
43 |
} |
44 |
|
45 |
public function __toString()92% |
46 |
{ |
47 |
$string = ''; |
48 |
|
49 |
if (sizeof($this->coverage) > 0) |
50 |
{ |
51 |
try |
52 |
{ |
53 |
$this->cleanDestinationDirectory(); |
54 |
|
55 |
$this->adapter->copy($this->templatesDirectory . DIRECTORY_SEPARATOR . 'screen.css', $this->destinationDirectory . DIRECTORY_SEPARATOR . 'screen.css'); |
56 |
|
57 |
$classes = $this->coverage->getClasses(); |
58 |
|
59 |
$indexTemplate = $this->templateParser->parseFile($this->templatesDirectory . DIRECTORY_SEPARATOR . 'index.tpl'); |
60 |
$indexTemplate->projectName = $this->projectName; |
61 |
$indexTemplate->rootUrl = $this->rootUrl; |
62 |
|
63 |
$coverageValue = $this->coverage->getValue(); |
64 |
|
65 |
if ($coverageValue === null) |
66 |
{ |
67 |
$indexTemplate->coverageUnavailable->build(); |
68 |
} |
69 |
else |
70 |
{ |
71 |
$indexTemplate->coverageAvailable->build(array('coverageValue' => round($coverageValue * 100, 2))); |
72 |
} |
73 |
|
74 |
$classCoverageTemplates = $indexTemplate->classCoverage; |
75 |
|
76 |
$classCoverageAvailableTemplates = $classCoverageTemplates->classCoverageAvailable; |
77 |
$classCoverageUnavailableTemplates = $classCoverageTemplates->classCoverageUnavailable; |
78 |
|
79 |
ksort($classes, \SORT_STRING); |
80 |
|
81 |
foreach ($classes as $className => $classFile) |
82 |
{ |
83 |
$classCoverageTemplates->className = $className; |
84 |
$classCoverageTemplates->classUrl = str_replace('\\', '/', $className) . self::htmlExtensionFile; |
85 |
|
86 |
$classCoverageValue = $this->coverage->getValueForClass($className); |
87 |
|
88 |
$classCoverageAvailableTemplates->build(array('classCoverageValue' => round($classCoverageValue * 100, 2))); |
89 |
|
90 |
$classCoverageTemplates->build(); |
91 | |
92 |
$classCoverageAvailableTemplates->resetData(); |
93 |
$classCoverageUnavailableTemplates->resetData(); |
94 |
} |
95 |
|
96 |
$this->adapter->file_put_contents($this->destinationDirectory . DIRECTORY_SEPARATOR . 'index.html', (string) $indexTemplate->build()); |
97 |
|
98 |
$classTemplate = $this->templateParser->parseFile($this->templatesDirectory . DIRECTORY_SEPARATOR . 'class.tpl'); |
99 |
|
100 |
$classTemplate->rootUrl = $this->rootUrl; |
101 |
$classTemplate->projectName = $this->projectName; |
102 |
|
103 |
$classCoverageAvailableTemplates = $classTemplate->classCoverageAvailable; |
104 |
$classCoverageUnavailableTemplates = $classTemplate->classCoverageUnavailable; |
105 |
|
106 |
$methodsTemplates = $classTemplate->methods; |
107 |
$methodTemplates = $methodsTemplates->method; |
108 |
|
109 |
$methodCoverageAvailableTemplates = $methodTemplates->methodCoverageAvailable; |
110 |
$methodCoverageUnavailableTemplates = $methodTemplates->methodCoverageUnavailable; |
111 |
|
112 |
$sourceFileTemplates = $classTemplate->sourceFile; |
113 |
|
114 |
$lineTemplates = $sourceFileTemplates->line; |
115 |
$coveredLineTemplates = $sourceFileTemplates->coveredLine; |
116 |
$notCoveredLineTemplates = $sourceFileTemplates->notCoveredLine; |
117 |
|
118 |
foreach ($this->coverage->getMethods() as $className => $methods) |
119 |
{ |
120 |
$classTemplate->className = $className; |
121 | |
122 |
if (substr_count($className, '\\') >= 1) |
123 |
{ |
124 |
$classTemplate->relativeRootUrl = rtrim(str_repeat('../', substr_count($className, '\\')), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; |
125 |
} |
126 |
|
127 |
$classCoverageValue = $this->coverage->getValueForClass($className); |
128 |
|
129 |
if ($classCoverageValue === null) |
130 |
{ |
131 |
$classCoverageUnavailableTemplates->build(); |
132 |
} |
133 |
else |
134 |
{ |
135 |
$classCoverageAvailableTemplates->build(array('classCoverageValue' => round($classCoverageValue * 100, 2))); |
136 |
} |
137 |
|
138 |
$reflectedMethods = array(); |
139 |
|
140 |
foreach (array_filter($this->getReflectionClass($className)->getMethods(), function($reflectedMethod) use ($className) { return $reflectedMethod->isAbstract() === false && $reflectedMethod->getDeclaringClass()->getName() === $className; }) as $reflectedMethod) |
141 |
{ |
142 |
$reflectedMethods[$reflectedMethod->getName()] = $reflectedMethod; |
143 |
} |
144 |
|
145 |
if (sizeof($reflectedMethods) > 0) |
146 |
{ |
147 |
foreach (array_intersect(array_keys($reflectedMethods), array_keys($methods)) as $methodName) |
148 |
{ |
149 |
$methodCoverageValue = $this->coverage->getValueForMethod($className, $methodName); |
150 |
|
151 |
if ($methodCoverageValue === null) |
152 |
{ |
153 |
$methodCoverageUnavailableTemplates->build(array('methodName' => $methodName)); |
154 |
} |
155 |
else |
156 |
{ |
157 |
$methodCoverageAvailableTemplates->build(array( |
158 |
'methodName' => $methodName, |
159 |
'methodCoverageValue' => round($methodCoverageValue * 100, 2) |
160 |
) |
161 |
); |
162 |
} |
163 |
|
164 |
$methodTemplates->build(); |
165 |
|
166 |
$methodCoverageAvailableTemplates->resetData(); |
167 |
$methodCoverageUnavailableTemplates->resetData(); |
168 |
} |
169 |
|
170 |
$methodsTemplates->build(); |
171 |
|
172 |
$methodTemplates->resetData(); |
173 |
} |
174 |
|
175 |
$srcFile = $this->adapter->fopen($classes[$className], 'r'); |
176 |
|
177 |
if ($srcFile !== false) |
178 |
{ |
179 |
$methodLines = array(); |
180 |
|
181 |
foreach ($reflectedMethods as $reflectedMethodName => $reflectedMethod) |
182 |
{ |
183 |
$methodLines[$reflectedMethod->getStartLine()] = $reflectedMethodName; |
184 |
} |
185 |
|
186 |
for ($currentMethod = null, $lineNumber = 1, $line = $this->adapter->fgets($srcFile); $line !== false; $lineNumber++, $line = $this->adapter->fgets($srcFile)) |
187 |
{ |
188 |
if (isset($methodLines[$lineNumber]) === true) |
189 |
{ |
190 |
$currentMethod = $methodLines[$lineNumber]; |
191 |
} |
192 |
|
193 |
switch (true) |
194 |
{ |
195 |
case isset($methods[$currentMethod]) === false || (isset($methods[$currentMethod][$lineNumber]) === false || $methods[$currentMethod][$lineNumber] == -2): |
196 |
$lineTemplateName = 'lineTemplates'; |
197 |
break; |
198 |
|
199 |
case isset($methods[$currentMethod]) === true && isset($methods[$currentMethod][$lineNumber]) === true && $methods[$currentMethod][$lineNumber] == -1: |
200 |
$lineTemplateName = 'notCoveredLineTemplates'; |
201 |
break; |
202 |
|
203 |
default: |
204 |
$lineTemplateName = 'coveredLineTemplates'; |
205 |
} |
206 |
|
207 |
${$lineTemplateName}->lineNumber = $lineNumber; |
208 |
${$lineTemplateName}->code = htmlentities($line, ENT_QUOTES, 'UTF-8'); |
209 |
|
210 |
if (isset($methodLines[$lineNumber]) === true) |
211 |
{ |
212 |
foreach (${$lineTemplateName}->anchor as $anchorTemplate) |
213 |
{ |
214 |
$anchorTemplate->resetData(); |
215 |
$anchorTemplate->method = $currentMethod; |
216 |
$anchorTemplate->build(); |
217 |
} |
218 |
} |
219 |
|
220 |
${$lineTemplateName} |
221 |
->addToParent() |
222 |
->resetData() |
223 |
; |
224 |
} |
225 |
|
226 |
$this->adapter->fclose($srcFile); |
227 |
} |
228 |
|
229 |
$file = $this->destinationDirectory . DIRECTORY_SEPARATOR . str_replace('\\', '/', $className) . self::htmlExtensionFile; |
230 |
|
231 |
$directory = $this->adapter->dirname($file); |
232 |
|
233 |
if ($this->adapter->is_dir($directory) === false) |
234 |
{ |
235 |
$this->adapter->mkdir($directory, 0777, true); |
236 |
} |
237 |
|
238 |
$this->adapter->file_put_contents($file, (string) $classTemplate->build()); |
239 |
|
240 |
$classTemplate->resetData(); |
241 | |
242 |
$classCoverageAvailableTemplates->resetData(); |
243 |
$classCoverageUnavailableTemplates->resetData(); |
244 |
|
245 |
$methodsTemplates->resetData(); |
246 |
|
247 |
$sourceFileTemplates->resetData(); |
248 |
} |
249 |
|
250 |
$string .= $this->urlPrompt . $this->urlColorizer->colorize(sprintf($this->locale->_('Details of code coverage are available at %s.'), $this->rootUrl)) . PHP_EOL; |
251 |
} |
252 |
catch (\exception $exception) |
253 |
{ |
254 |
$string .= $this->urlPrompt . $this->urlColorizer->colorize(sprintf($this->locale->_('Unable to generate code coverage at %s: %s.'), $this->rootUrl, $exception->getMessage())) . PHP_EOL; |
255 |
} |
256 |
} |
257 |
|
258 |
return parent::__toString() . $string; |
259 |
} |
260 |
|
261 |
public function setReflectionClassInjector(\closure $reflectionClassInjector)100% |
262 |
{ |
263 |
$closure = new \reflectionMethod($reflectionClassInjector, '__invoke'); |
264 |
|
265 |
if ($closure->getNumberOfParameters() != 1) |
266 |
{ |
267 |
throw new exceptions\logic\invalidArgument('Reflection class injector must take one argument'); |
268 |
} |
269 |
|
270 |
$this->reflectionClassInjector = $reflectionClassInjector; |
271 | |
272 |
return $this; |
273 |
} |
274 |
|
275 |
public function getReflectionClass($class)100% |
276 |
{ |
277 |
if ($this->reflectionClassInjector === null) |
278 |
{ |
279 |
$reflectionClass = new \reflectionClass($class); |
280 |
} |
281 |
else |
282 |
{ |
283 |
$reflectionClass = $this->reflectionClassInjector->__invoke($class); |
284 |
|
285 |
if ($reflectionClass instanceof \reflectionClass === false) |
286 |
{ |
287 |
throw new exceptions\runtime\unexpectedValue('Reflection class injector must return a \reflectionClass instance'); |
288 |
} |
289 |
} |
290 |
|
291 |
return $reflectionClass; |
292 |
} |
293 |
|
294 |
public function setProjectName($projectName)100% |
295 |
{ |
296 |
$this->projectName = (string) $projectName; |
297 |
|
298 |
return $this; |
299 |
} |
300 |
|
301 |
public function getProjectName() |
302 |
{ |
303 |
return $this->projectName; |
304 |
} |
305 |
|
306 |
public function setDestinationDirectory($path)100% |
307 |
{ |
308 |
$this->destinationDirectory = (string) $path; |
309 |
|
310 |
return $this; |
311 |
} |
312 |
|
313 |
public function getDestinationDirectory()100% |
314 |
{ |
315 |
return $this->destinationDirectory; |
316 |
} |
317 |
|
318 |
public function setUrlPrompt(prompt $prompt = null)100% |
319 |
{ |
320 |
$this->urlPrompt = $prompt ?: new prompt(); |
321 |
|
322 |
return $this; |
323 |
} |
324 |
|
325 |
public function getUrlPrompt()100% |
326 |
{ |
327 |
return $this->urlPrompt; |
328 |
} |
329 |
|
330 |
public function setUrlColorizer(colorizer $colorizer = null)100% |
331 |
{ |
332 |
$this->urlColorizer = $colorizer ?: new colorizer(); |
333 |
|
334 |
return $this; |
335 |
} |
336 |
|
337 |
public function getUrlColorizer()100% |
338 |
{ |
339 |
return $this->urlColorizer; |
340 |
} |
341 |
|
342 |
public function setTemplatesDirectory($path = null)100% |
343 |
{ |
344 |
$this->templatesDirectory = ($path !== null ? (string) $path : atoum\directory . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . 'templates' . DIRECTORY_SEPARATOR . 'coverage'); |
345 |
|
346 |
return $this; |
347 |
} |
348 |
|
349 |
public function getTemplatesDirectory()100% |
350 |
{ |
351 |
return $this->templatesDirectory; |
352 |
} |
353 |
|
354 |
public function setTemplateParser(template\parser $parser = null)100% |
355 |
{ |
356 |
$this->templateParser = $parser ?: new template\parser(); |
357 |
|
358 |
return $this; |
359 |
} |
360 |
|
361 |
public function getTemplateParser() |
362 |
{ |
363 |
return $this->templateParser; |
364 |
} |
365 |
|
366 |
public function setRootUrl($rootUrl)100% |
367 |
{ |
368 |
$this->rootUrl = (string) $rootUrl; |
369 |
|
370 |
return $this; |
371 |
} |
372 |
|
373 |
public function getRootUrl()100% |
374 |
{ |
375 |
return $this->rootUrl; |
376 |
} |
377 |
|
378 |
public function getDestinationDirectoryIterator()100% |
379 |
{ |
380 |
return new \recursiveIteratorIterator(new \recursiveDirectoryIterator($this->destinationDirectory, \filesystemIterator::KEY_AS_PATHNAME | \filesystemIterator::CURRENT_AS_FILEINFO | \filesystemIterator::SKIP_DOTS), \recursiveIteratorIterator::CHILD_FIRST); |
381 |
} |
382 |
|
383 |
public function cleanDestinationDirectory()100% |
384 |
{ |
385 |
try |
386 |
{ |
387 |
foreach ($this->getDestinationDirectoryIterator() as $inode) |
388 |
{ |
389 |
if ($inode->isDir() === false) |
390 |
{ |
391 |
$this->adapter->unlink($inode->getPathname()); |
392 |
} |
393 |
else if (($pathname = $inode->getPathname()) !== $this->destinationDirectory) |
394 |
{ |
395 |
$this->adapter->rmdir($pathname); |
396 |
} |
397 |
} |
398 |
} |
399 |
catch (\exception $exception) {} |
400 |
|
401 |
return $this; |
402 |
} |
403 |
} |