@@ -107,12 +107,34 @@ public function render(FileNode $node, string $file): void
107107 $ template ->setVar (
108108 [
109109 'items ' => $ this ->renderItems ($ node ),
110- 'lines ' => $ this ->renderSourceByLine ($ node ),
110+ 'lines ' => $ this ->renderSourceWithLineCoverage ($ node ),
111111 'legend ' => '<p><span class="success"><strong>Executed</strong></span><span class="danger"><strong>Not Executed</strong></span><span class="warning"><strong>Dead Code</strong></span></p> ' ,
112112 ]
113113 );
114114
115115 $ template ->renderTo ($ file . '.html ' );
116+
117+ if ($ this ->hasBranchCoverage ) {
118+ $ template ->setVar (
119+ [
120+ 'items ' => $ this ->renderItems ($ node ),
121+ 'lines ' => $ this ->renderSourceWithBranchCoverage ($ node ),
122+ 'legend ' => '<p><span class="success"><strong>Fully covered</strong></span><span class="warning"><strong>Partially covered</strong></span><span class="danger"><strong>Not covered</strong></span></p> ' ,
123+ ]
124+ );
125+
126+ $ template ->renderTo ($ file . '_branch.html ' );
127+
128+ $ template ->setVar (
129+ [
130+ 'items ' => $ this ->renderItems ($ node ),
131+ 'lines ' => $ this ->renderSourceWithPathCoverage ($ node ),
132+ 'legend ' => '<p><span class="success"><strong>Fully covered</strong></span><span class="warning"><strong>Partially covered</strong></span><span class="danger"><strong>Not covered</strong></span></p> ' ,
133+ ]
134+ );
135+
136+ $ template ->renderTo ($ file . '_path.html ' );
137+ }
116138 }
117139
118140 private function renderItems (FileNode $ node ): string
@@ -361,8 +383,11 @@ private function renderFunctionOrMethodItem(Template $template, array $item, str
361383 );
362384 }
363385
364- private function renderSourceByLine (FileNode $ node ): string
386+ private function renderSourceWithLineCoverage (FileNode $ node ): string
365387 {
388+ $ linesTemplate = new Template ($ this ->templatePath . 'lines.html.dist ' , '{{ ' , '}} ' );
389+ $ singleLineTemplate = new Template ($ this ->templatePath . 'line.html.dist ' , '{{ ' , '}} ' );
390+
366391 $ coverageData = $ node ->lineCoverageData ();
367392 $ testData = $ node ->testData ();
368393 $ codeLines = $ this ->loadFile ($ node ->pathAsString ());
@@ -378,9 +403,9 @@ private function renderSourceByLine(FileNode $node): string
378403 $ numTests = ($ coverageData [$ i ] ? count ($ coverageData [$ i ]) : 0 );
379404
380405 if ($ coverageData [$ i ] === null ) {
381- $ trClass = ' class=" warning" ' ;
406+ $ trClass = 'warning ' ;
382407 } elseif ($ numTests === 0 ) {
383- $ trClass = ' class=" danger" ' ;
408+ $ trClass = 'danger ' ;
384409 } else {
385410 if ($ numTests > 1 ) {
386411 $ popoverTitle = $ numTests . ' tests cover line ' . $ i ;
@@ -402,7 +427,7 @@ private function renderSourceByLine(FileNode $node): string
402427 }
403428
404429 $ popoverContent .= '</ul> ' ;
405- $ trClass = ' class=" ' . $ lineCss . ' popin" ' ;
430+ $ trClass = $ lineCss . ' popin ' ;
406431 }
407432 }
408433
@@ -416,20 +441,199 @@ private function renderSourceByLine(FileNode $node): string
416441 );
417442 }
418443
419- $ lines .= sprintf (
420- ' <tr%s><td%s><div align="right"><a name="%d"></a><a href="#%d">%d</a></div></td><td class="codeLine">%s</td></tr> ' . "\n" ,
421- $ trClass ,
422- $ popover ,
423- $ i ,
424- $ i ,
425- $ i ,
426- $ line
427- );
444+ $ lines .= $ this ->renderLine ($ singleLineTemplate , $ i , $ line , $ trClass , $ popover );
445+
446+ $ i ++;
447+ }
448+
449+ $ linesTemplate ->setVar (['lines ' => $ lines ]);
450+
451+ return $ linesTemplate ->render ();
452+ }
453+
454+ private function renderSourceWithBranchCoverage (FileNode $ node ): string
455+ {
456+ $ linesTemplate = new Template ($ this ->templatePath . 'lines.html.dist ' , '{{ ' , '}} ' );
457+ $ singleLineTemplate = new Template ($ this ->templatePath . 'line.html.dist ' , '{{ ' , '}} ' );
458+
459+ $ functionCoverageData = $ node ->functionCoverageData ();
460+ $ testData = $ node ->testData ();
461+ $ codeLines = $ this ->loadFile ($ node ->pathAsString ());
462+
463+ $ lineData = [];
464+
465+ foreach (array_keys ($ codeLines ) as $ line ) {
466+ $ lineData [$ line + 1 ] = [
467+ 'includedInBranches ' => 0 ,
468+ 'includedInHitBranches ' => 0 ,
469+ 'tests ' => [],
470+ ];
471+ }
472+
473+ foreach ($ functionCoverageData as $ method ) {
474+ foreach ($ method ['branches ' ] as $ branch ) {
475+ foreach (range ($ branch ['line_start ' ], $ branch ['line_end ' ]) as $ line ) {
476+ if (!isset ($ lineData [$ line ])) { // blank line at end of file is sometimes included here
477+ continue ;
478+ }
479+
480+ $ lineData [$ line ]['includedInBranches ' ]++;
481+
482+ if ($ branch ['hit ' ]) {
483+ $ lineData [$ line ]['includedInHitBranches ' ]++;
484+ $ lineData [$ line ]['tests ' ] = array_merge ($ lineData [$ line ]['tests ' ], $ branch ['hit ' ]);
485+ }
486+ }
487+ }
488+ }
489+
490+ $ lines = '' ;
491+ $ i = 1 ;
492+
493+ foreach ($ codeLines as $ line ) {
494+ $ trClass = '' ;
495+ $ popover = '' ;
496+
497+ if ($ lineData [$ i ]['includedInBranches ' ] > 0 ) {
498+ $ lineCss = 'success ' ;
499+
500+ if ($ lineData [$ i ]['includedInHitBranches ' ] === 0 ) {
501+ $ lineCss = 'danger ' ;
502+ } elseif ($ lineData [$ i ]['includedInHitBranches ' ] !== $ lineData [$ i ]['includedInBranches ' ]) {
503+ $ lineCss = 'warning ' ;
504+ }
505+
506+ $ popoverContent = '<ul> ' ;
507+
508+ if (count ($ lineData [$ i ]['tests ' ]) === 1 ) {
509+ $ popoverTitle = '1 test covers line ' . $ i ;
510+ } else {
511+ $ popoverTitle = count ($ lineData [$ i ]['tests ' ]) . ' tests cover line ' . $ i ;
512+ }
513+ $ popoverTitle .= '. These are covering ' . $ lineData [$ i ]['includedInHitBranches ' ] . ' out of the ' . $ lineData [$ i ]['includedInBranches ' ] . ' code branches. ' ;
514+
515+ foreach ($ lineData [$ i ]['tests ' ] as $ test ) {
516+ $ popoverContent .= $ this ->createPopoverContentForTest ($ test , $ testData [$ test ]);
517+ }
518+
519+ $ popoverContent .= '</ul> ' ;
520+ $ trClass = $ lineCss . ' popin ' ;
521+
522+ $ popover = sprintf (
523+ ' data-title="%s" data-content="%s" data-placement="top" data-html="true" ' ,
524+ $ popoverTitle ,
525+ htmlspecialchars ($ popoverContent , $ this ->htmlSpecialCharsFlags )
526+ );
527+ }
528+
529+ $ lines .= $ this ->renderLine ($ singleLineTemplate , $ i , $ line , $ trClass , $ popover );
428530
429531 $ i ++;
430532 }
431533
432- return $ lines ;
534+ $ linesTemplate ->setVar (['lines ' => $ lines ]);
535+
536+ return $ linesTemplate ->render ();
537+ }
538+
539+ private function renderSourceWithPathCoverage (FileNode $ node ): string
540+ {
541+ $ linesTemplate = new Template ($ this ->templatePath . 'lines.html.dist ' , '{{ ' , '}} ' );
542+ $ singleLineTemplate = new Template ($ this ->templatePath . 'line.html.dist ' , '{{ ' , '}} ' );
543+
544+ $ functionCoverageData = $ node ->functionCoverageData ();
545+ $ testData = $ node ->testData ();
546+ $ codeLines = $ this ->loadFile ($ node ->pathAsString ());
547+
548+ $ lineData = [];
549+
550+ foreach (array_keys ($ codeLines ) as $ line ) {
551+ $ lineData [$ line + 1 ] = [
552+ 'includedInPaths ' => 0 ,
553+ 'includedInHitPaths ' => 0 ,
554+ 'tests ' => [],
555+ ];
556+ }
557+
558+ foreach ($ functionCoverageData as $ method ) {
559+ foreach ($ method ['paths ' ] as $ path ) {
560+ foreach ($ path ['path ' ] as $ branchTaken ) {
561+ foreach (range ($ method ['branches ' ][$ branchTaken ]['line_start ' ], $ method ['branches ' ][$ branchTaken ]['line_end ' ]) as $ line ) {
562+ if (!isset ($ lineData [$ line ])) {
563+ continue ;
564+ }
565+ $ lineData [$ line ]['includedInPaths ' ]++;
566+
567+ if ($ path ['hit ' ]) {
568+ $ lineData [$ line ]['includedInHitPaths ' ]++;
569+ $ lineData [$ line ]['tests ' ] = array_merge ($ lineData [$ line ]['tests ' ], $ path ['hit ' ]);
570+ }
571+ }
572+ }
573+ }
574+ }
575+
576+ $ lines = '' ;
577+ $ i = 1 ;
578+
579+ foreach ($ codeLines as $ line ) {
580+ $ trClass = '' ;
581+ $ popover = '' ;
582+
583+ if ($ lineData [$ i ]['includedInPaths ' ] > 0 ) {
584+ $ lineCss = 'success ' ;
585+
586+ if ($ lineData [$ i ]['includedInHitPaths ' ] === 0 ) {
587+ $ lineCss = 'danger ' ;
588+ } elseif ($ lineData [$ i ]['includedInHitPaths ' ] !== $ lineData [$ i ]['includedInPaths ' ]) {
589+ $ lineCss = 'warning ' ;
590+ }
591+
592+ $ popoverContent = '<ul> ' ;
593+
594+ if (count ($ lineData [$ i ]['tests ' ]) === 1 ) {
595+ $ popoverTitle = '1 test covers line ' . $ i ;
596+ } else {
597+ $ popoverTitle = count ($ lineData [$ i ]['tests ' ]) . ' tests cover line ' . $ i ;
598+ }
599+ $ popoverTitle .= '. These are covering ' . $ lineData [$ i ]['includedInHitPaths ' ] . ' out of the ' . $ lineData [$ i ]['includedInPaths ' ] . ' code paths. ' ;
600+
601+ foreach ($ lineData [$ i ]['tests ' ] as $ test ) {
602+ $ popoverContent .= $ this ->createPopoverContentForTest ($ test , $ testData [$ test ]);
603+ }
604+
605+ $ popoverContent .= '</ul> ' ;
606+ $ trClass = $ lineCss . ' popin ' ;
607+
608+ $ popover = sprintf (
609+ ' data-title="%s" data-content="%s" data-placement="top" data-html="true" ' ,
610+ $ popoverTitle ,
611+ htmlspecialchars ($ popoverContent , $ this ->htmlSpecialCharsFlags )
612+ );
613+ }
614+
615+ $ lines .= $ this ->renderLine ($ singleLineTemplate , $ i , $ line , $ trClass , $ popover );
616+
617+ $ i ++;
618+ }
619+
620+ $ linesTemplate ->setVar (['lines ' => $ lines ]);
621+
622+ return $ linesTemplate ->render ();
623+ }
624+
625+ private function renderLine (Template $ template , int $ lineNumber , string $ lineContent , string $ class , string $ popover ): string
626+ {
627+ $ template ->setVar (
628+ [
629+ 'lineNumber ' => $ lineNumber ,
630+ 'lineContent ' => $ lineContent ,
631+ 'class ' => $ class ,
632+ 'popover ' => $ popover ,
633+ ]
634+ );
635+
636+ return $ template ->render ();
433637 }
434638
435639 /**
0 commit comments