|
Moodle
2.2.1
http://www.collinsharper.com
|
00001 <?php 00002 /* 00003 * $Id: HtmlCoverageReporter.php,v 1.3 2010/12/14 17:35:59 moodlerobot Exp $ 00004 * 00005 * Copyright(c) 2004-2006, SpikeSource Inc. All Rights Reserved. 00006 * Licensed under the Open Software License version 2.1 00007 * (See http://www.spikesource.com/license.html) 00008 */ 00009 ?> 00010 <?php 00011 00012 if(!defined("__PHPCOVERAGE_HOME")) { 00013 define("__PHPCOVERAGE_HOME", dirname(dirname(__FILE__))); 00014 } 00015 require_once __PHPCOVERAGE_HOME . "/reporter/CoverageReporter.php"; 00016 require_once __PHPCOVERAGE_HOME . "/parser/PHPParser.php"; 00017 require_once __PHPCOVERAGE_HOME . "/util/Utility.php"; 00018 00026 class HtmlCoverageReporter extends CoverageReporter { 00027 00028 /*{{{ Members */ 00029 00030 private $coverageData; 00031 private $htmlFile; 00032 private $body; 00033 private $header = "html/header.html"; 00034 private $footer = "html/footer.html"; 00035 private $indexHeader = "html/indexheader.html"; 00036 private $indexFooter = "html/indexfooter.html"; 00037 00038 /*}}}*/ 00039 /*{{{ public function __construct() */ 00040 00049 public function __construct( 00050 $heading="Coverage Report", 00051 $style="", 00052 $dir="report" 00053 ) { 00054 parent::__construct($heading, $style, $dir); 00055 } 00056 00057 /*}}}*/ 00058 /*{{{ public function generateReport() */ 00059 00068 public function generateReport(&$data) { 00069 if(!file_exists($this->outputDir)) { 00070 mkdir($this->outputDir); 00071 } 00072 $this->coverageData =& $data; 00073 $this->grandTotalFiles = count($this->coverageData); 00074 $ret = $this->writeIndexFile(); 00075 if($ret === FALSE) { 00076 $this->logger->error("Error occured!!!", __FILE__, __LINE__); 00077 } 00078 $this->logger->debug(print_r($data, true), __FILE__, __LINE__); 00079 } 00080 00081 /*}}}*/ 00082 /*{{{ private function writeIndexFileHeader() */ 00083 00090 private function writeIndexFileHeader() { 00091 $str = false; 00092 $dir = realpath(dirname(__FILE__)); 00093 if($dir !== false) { 00094 $str = file_get_contents($dir . "/" . $this->indexHeader); 00095 if($str == false) { 00096 return $str; 00097 } 00098 $str = str_replace("%%heading%%", $this->heading, $str); 00099 $str = str_replace("%%style%%", $this->style, $str); 00100 } 00101 return $str; 00102 } 00103 00104 /*}}}*/ 00105 /*{{{ private function writeIndexFileFooter() */ 00106 00113 private function writeIndexFileFooter() { 00114 $str = false; 00115 $dir = realpath(dirname(__FILE__)); 00116 if($dir !== false) { 00117 $str = file_get_contents($dir . "/" . $this->indexFooter); 00118 if($str == false) { 00119 return $str; 00120 } 00121 } 00122 return $str; 00123 } 00124 00125 /*}}}*/ 00126 /*{{{ private function createJSDir() */ 00127 00133 private function createJSDir() { 00134 $jsDir = $this->outputDir . "/js"; 00135 if(file_exists($this->outputDir) && !file_exists($jsDir)) { 00136 mkdir($jsDir); 00137 } 00138 $jsSortFile = realpath(dirname(__FILE__)) . "/js/sort_spikesource.js"; 00139 copy($jsSortFile, $jsDir . "/" . "sort_spikesource.js"); 00140 return true; 00141 } 00142 00143 /*}}}*/ 00144 /*{{{ private function createImagesDir() */ 00145 00151 private function createImagesDir() { 00152 $imagesDir = $this->outputDir . "/images"; 00153 if(file_exists($this->outputDir) && !file_exists($imagesDir)) { 00154 mkdir($imagesDir); 00155 } 00156 $imagesSpikeDir = $imagesDir . "/spikesource"; 00157 if(!file_exists($imagesSpikeDir)) { 00158 mkdir($imagesSpikeDir); 00159 } 00160 $imagesArrowUpFile = realpath(dirname(__FILE__)) . "/images/arrow_up.gif"; 00161 $imagesArrowDownFile = realpath(dirname(__FILE__)) . "/images/arrow_down.gif"; 00162 $imagesPHPCoverageLogoFile = realpath(dirname(__FILE__)) . "/images/spikesource/phpcoverage.gif"; 00163 $imagesSpacerFile = realpath(dirname(__FILE__)) . "/images/spacer.gif"; 00164 copy($imagesArrowUpFile, $imagesDir . "/" . "arrow_up.gif"); 00165 copy($imagesArrowDownFile, $imagesDir . "/" . "arrow_down.gif"); 00166 copy($imagesSpacerFile, $imagesDir . "/" . "spacer.gif"); 00167 copy($imagesPHPCoverageLogoFile, $imagesSpikeDir . "/" . "phpcoverage.gif"); 00168 return true; 00169 } 00170 00171 /*}}}*/ 00172 /*{{{ private function createStyleDir() */ 00173 00174 private function createStyleDir() { 00175 if(isset($this->style)) { 00176 $this->style = trim($this->style); 00177 } 00178 if(empty($this->style)) { 00179 $this->style = "spikesource.css"; 00180 } 00181 $styleDir = $this->outputDir . "/css"; 00182 if(file_exists($this->outputDir) && !file_exists($styleDir)) { 00183 mkdir($styleDir); 00184 } 00185 $styleFile = realpath(dirname(__FILE__)) . "/css/" . $this->style; 00186 copy($styleFile, $styleDir . "/" . $this->style); 00187 return true; 00188 } 00189 00190 /*}}}*/ 00191 /*{{{ protected function writeIndexFileTableHead() */ 00192 00199 protected function writeIndexFileTableHead() { 00200 $str = ""; 00201 $str .= '<h1>Details</h1> <table class="spikeDataTable" cellpadding="4" cellspacing="0" border="0" id="table2sort" width="800">'; 00202 $str .= '<thead>'; 00203 // start moodle modification: xhtml links 00204 $str .= '<tr><td class="spikeDataTableHeadLeft" id="sortCell0" rowspan="2" style="white-space:nowrap" width="52%"><a id="sortCellLink0" class="headerlink" href="#" onclick="javascript:sort(0)" title="Sort Ascending">File Name </a></td>'; 00205 // end moodle modification 00206 $str .= '<td colspan="4" class="spikeDataTableHeadCenter">Lines</td>'; 00207 // start moodle modification: xhtml links 00208 $str .= '<td class="spikeDataTableHeadCenterLast" id="sortCell5" rowspan="2" width="16%" style="white-space:nowrap"><a id="sortCellLink5" class="headerlink" href="#" onclick="javascript:sort(5, \'percentage\')" title="Sort Ascending">Code Coverage </a></td>'; 00209 // end moodle modification 00210 $str .= '</tr>'; 00211 00212 // Second row - subheadings 00213 $str .= '<tr>'; 00214 // start moodle modification: xhtml links 00215 $str .= '<td class="spikeDataTableSubHeadCenter" id="sortCell1" style="white-space:nowrap" width="8%"><a id="sortCellLink1" title="Sort Ascending" class="headerlink" href="#" onclick="javascript:sort(1, \'number\')">Total </a></td>'; 00216 $str .= '<td class="spikeDataTableSubHeadCenter" id="sortCell2" style="white-space:nowrap" width="9%"><a id="sortCellLink2" title="Sort Ascending" class="headerlink" href="#" onclick="javascript:sort(2, \'number\')">Covered </a></td>'; 00217 $str .= '<td class="spikeDataTableSubHeadCenter" id="sortCell3" style="white-space:nowrap" width="8%"><a id="sortCellLink3" title="Sort Ascending" class="headerlink" href="#" onclick="javascript:sort(3, \'number\')">Missed </a></td>'; 00218 $str .= '<td class="spikeDataTableSubHeadCenter" id="sortCell4" style="white-space:nowrap" width="10%"><a id="sortCellLink4" title="Sort Ascending" class="headerlink" href="#" onclick="javascript:sort(4, \'number\')">Executable </a></td>'; 00219 // end moodle modification 00220 $str .= '</tr>'; 00221 $str .= '</thead>'; 00222 return $str; 00223 } 00224 00225 /*}}}*/ 00226 /*{{{ protected function writeIndexFileTableRow() */ 00227 00238 protected function writeIndexFileTableRow($fileLink, $realFile, $fileCoverage) { 00239 00240 global $util; 00241 $fileLink = $this->makeRelative($fileLink); 00242 $realFileShort = $util->shortenFilename($realFile); 00243 $str = ""; 00244 00245 $str .= '<tr><td class="spikeDataTableCellLeft">'; 00246 $str .= '<a class="contentlink" href="' . $util->unixifyPath($fileLink) . '" title="' 00247 . $realFile .'">' . $realFileShort. '</a>' . '</td>'; 00248 $str .= '<td class="spikeDataTableCellCenter">' . $fileCoverage['total'] . "</td>"; 00249 $str .= '<td class="spikeDataTableCellCenter">' . $fileCoverage['covered'] . "</td>"; 00250 $str .= '<td class="spikeDataTableCellCenter">' . $fileCoverage['uncovered'] . "</td>"; 00251 $str .= '<td class="spikeDataTableCellCenter">' . ($fileCoverage['covered']+$fileCoverage['uncovered']) . "</td>"; 00252 if($fileCoverage['uncovered'] + $fileCoverage['covered'] == 0) { 00253 // If there are no executable lines, assume coverage to be 100% 00254 $str .= '<td class="spikeDataTableCellCenter">100%</td></tr>'; 00255 } 00256 else { 00257 $str .= '<td class="spikeDataTableCellCenter">' 00258 . round(($fileCoverage['covered']/($fileCoverage['uncovered'] 00259 + $fileCoverage['covered']))*100.0, 2) 00260 . '%</td></tr>'; 00261 } 00262 return $str; 00263 } 00264 00265 /*}}}*/ 00266 /*{{{ protected function writeIndexFileGrandTotalPercentage() */ 00267 00274 protected function writeIndexFileGrandTotalPercentage() { 00275 $str = ""; 00276 00277 $str .= "<br/><h1>" . $this->heading . "</h1><br/>"; 00278 00279 $str .= '<table border="0" cellpadding="0" cellspacing="0" id="contentBox" width="800"> <tr>'; 00280 $str .= '<td align="left" valign="top"><h1>Summary</h1>'; 00281 $str .= '<table class="spikeVerticalTable" cellpadding="4" cellspacing="0" width="800" style="margin-bottom:10px" border="0">'; 00282 // start moodle modification: xhtml 00283 $str .= '<tr>'; 00284 $str .= '<td width="380" class="spikeVerticalTableHead" style="font-size:14px">Overall Code Coverage </td>'; 00285 $str .= '<td class="spikeVerticalTableCell" style="font-size:14px" colspan="2"><strong>' . $this->getGrandCodeCoveragePercentage() . '%</strong></td>'; 00286 // end moodle modification 00287 00288 $str .= '</tr><tr>'; 00289 00290 $str .= '<td class="spikeVerticalTableHead">Total Covered Lines of Code </td>'; 00291 $str .= '<td width="30" class="spikeVerticalTableCell"><span class="emphasis">' . $this->grandTotalCoveredLines.'</span></td>'; 00292 $str .= '<td class="spikeVerticalTableCell"><span class="note">(' . TOTAL_COVERED_LINES_EXPLAIN . ')</span></td>'; 00293 00294 $str .= '</tr><tr>'; 00295 00296 $str .= '<td class="spikeVerticalTableHead">Total Missed Lines of Code </td>'; 00297 $str .= '<td class="spikeVerticalTableCell"><span class="emphasis">' . $this->grandTotalUncoveredLines.'</span></td>'; 00298 $str .= '<td class="spikeVerticalTableCell"><span class="note">(' . TOTAL_UNCOVERED_LINES_EXPLAIN . ')</span></td>'; 00299 00300 $str .= '</tr><tr>'; 00301 00302 $str .= '<td class="spikeVerticalTableHead">Total Lines of Code </td>'; 00303 $str .= '<td class="spikeVerticalTableCell"><span class="emphasis">' . ($this->grandTotalCoveredLines + $this->grandTotalUncoveredLines) .'</span></td>'; 00304 $str .= '<td class="spikeVerticalTableCell"><span class="note">(' . 00305 TOTAL_LINES_OF_CODE_EXPLAIN . ')</span></td>'; 00306 00307 $str .= '</tr><tr>'; 00308 00309 $str .= '<td class="spikeVerticalTableHead" >Total Lines </td>'; 00310 $str .= '<td class="spikeVerticalTableCell"><span class="emphasis">' . $this->grandTotalLines.'</span></td>'; 00311 $str .= '<td class="spikeVerticalTableCell"><span class="note">(' . TOTAL_LINES_EXPLAIN . ')</span></td>'; 00312 00313 $str .= '</tr><tr>'; 00314 00315 $str .= '<td class="spikeVerticalTableHeadLast" >Total Files </td>'; 00316 $str .= '<td class="spikeVerticalTableCellLast"><span class="emphasis">' . $this->grandTotalFiles.'</span></td>'; 00317 $str .= '<td class="spikeVerticalTableCellLast"><span class="note">(' . TOTAL_FILES_EXPLAIN . ')</span></td>'; 00318 00319 $str .= '</tr></table>'; 00320 00321 return $str; 00322 } 00323 00324 /*}}}*/ 00325 /*{{{ protected function writeIndexFile() */ 00326 00333 protected function writeIndexFile() { 00334 global $util; 00335 $str = ""; 00336 $this->createJSDir(); 00337 $this->createImagesDir(); 00338 $this->createStyleDir(); 00339 $this->htmlFile = $this->outputDir . "/index.html"; 00340 $indexFile = fopen($this->htmlFile, "w"); 00341 if(empty($indexFile)) { 00342 $this->logger->error("Cannot open file for writing: $this->htmlFile", 00343 __FILE__, __LINE__); 00344 return false; 00345 } 00346 00347 $strHead = $this->writeIndexFileHeader(); 00348 if($strHead == false) { 00349 return false; 00350 } 00351 $str .= $this->writeIndexFileTableHead(); 00352 $str .= '<tbody>'; 00353 if(!empty($this->coverageData)) { 00354 foreach($this->coverageData as $filename => &$lines) { 00355 $realFile = realpath($filename); 00356 $fileLink = $this->outputDir . $util->unixifyPath($realFile). ".html"; 00357 $fileCoverage = $this->markFile($realFile, $fileLink, $lines); 00358 if(empty($fileCoverage)) { 00359 return false; 00360 } 00361 $this->recordFileCoverageInfo($fileCoverage); 00362 $this->updateGrandTotals($fileCoverage); 00363 00364 $str .= $this->writeIndexFileTableRow($fileLink, $realFile, $fileCoverage); 00365 unset($this->coverageData[$filename]); 00366 } 00367 } 00368 $str .= '</tbody>'; 00369 $str .= "</table></td></tr>"; 00370 00371 $str .= "<tr><td><p align=\"right\" class=\"content\">Report Generated On: " . $util->getTimeStamp() . "<br/>"; 00372 $str .= "Generated using Spike PHPCoverage " . $this->recorder->getVersion() . "</p></td></tr></table>"; 00373 00374 // Get the summary 00375 $strSummary = $this->writeIndexFileGrandTotalPercentage(); 00376 00377 // Merge them - with summary on top 00378 $str = $strHead . $strSummary . $str; 00379 00380 $str .= $this->writeIndexFileFooter(); 00381 fwrite($indexFile, $str); 00382 fclose($indexFile); 00383 return TRUE; 00384 } 00385 00386 /*}}}*/ 00387 /*{{{ private function writePhpFileHeader() */ 00388 00396 private function writePhpFileHeader($filename, $fileLink) { 00397 $fileLink = $this->makeRelative($fileLink); 00398 $str = false; 00399 $dir = realpath(dirname(__FILE__)); 00400 if($dir !== false) { 00401 $str = file_get_contents($dir . "/" . $this->header); 00402 if($str == false) { 00403 return $str; 00404 } 00405 $str = str_replace("%%filename%%", $filename, $str); 00406 // Get the path to parent CSS directory 00407 $relativeCssPath = $this->getRelativeOutputDirPath($fileLink); 00408 $relativeCssPath .= "/css/" . $this->style; 00409 $str = str_replace("%%style%%", $relativeCssPath, $str); 00410 } 00411 return $str; 00412 } 00413 00414 /*}}}*/ 00415 /*{{{ private function writePhpFileFooter() */ 00416 00423 private function writePhpFileFooter() { 00424 $str = false; 00425 $dir = realpath(dirname(__FILE__)); 00426 if($dir !== false) { 00427 $str = file_get_contents($dir . "/" . $this->footer); 00428 if($str == false) { 00429 return $str; 00430 } 00431 } 00432 return $str; 00433 } 00434 00435 /*}}}*/ 00436 /*{{{ protected function markFile() */ 00437 00447 protected function markFile($phpFile, $fileLink, &$coverageLines) { 00448 global $util; 00449 $fileLink = $util->replaceBackslashes($fileLink); 00450 $parentDir = $util->replaceBackslashes(dirname($fileLink)); 00451 if(!file_exists($parentDir)) { 00452 //echo "\nCreating dir: $parentDir\n"; 00453 $util->makeDirRecursive($parentDir, 0755); 00454 } 00455 $writer = fopen($fileLink, "w"); 00456 00457 if(empty($writer)) { 00458 $this->logger->error("Could not open file for writing: $fileLink", 00459 __FILE__, __LINE__); 00460 return false; 00461 } 00462 00463 // Get the header for file 00464 $filestr = $this->writePhpFileHeader(basename($phpFile), $fileLink); 00465 00466 // Add header for table 00467 $filestr .= '<table width="100%" border="0" cellpadding="2" cellspacing="0">'; 00468 $filestr .= $this->writeFileTableHead(); 00469 00470 $lineCnt = $coveredCnt = $uncoveredCnt = 0; 00471 $parser = new PHPParser(); 00472 $parser->parse($phpFile); 00473 $lastLineType = "non-exec"; 00474 $fileLines = array(); 00475 while(($line = $parser->getLine()) !== false) { 00476 if (substr($line, -1) == "\n") { 00477 $line = substr($line, 0, -1); 00478 } 00479 $lineCnt++; 00480 $coverageLineNumbers = array_keys($coverageLines); 00481 if(in_array($lineCnt, $coverageLineNumbers)) { 00482 $lineType = $parser->getLineType(); 00483 if($lineType == LINE_TYPE_EXEC) { 00484 $coveredCnt ++; 00485 $type = "covered"; 00486 } 00487 else if($lineType == LINE_TYPE_CONT) { 00488 // XDebug might return this as covered - when it is 00489 // actually merely a continuation of previous line 00490 if($lastLineType == "covered" || $lastLineType == "covered_cont") { 00491 unset($coverageLines[$lineCnt]); 00492 $type = "covered_cont"; 00493 $coveredCnt ++; 00494 } 00495 else { 00496 $ft = "uncovered_cont"; 00497 for($il = $lineCnt-1 00498 ; $il >=0 00499 && isset($fileLines[$lineCnt-1]["type"]) 00500 && $ft == "uncovered_cont" 00501 ; $il--) { 00502 $ft = $fileLines[$il]["type"]; 00503 $uncoveredCnt --; 00504 $coveredCnt ++; 00505 if($ft == "uncovered") { 00506 $fileLines[$il]["type"] = "covered"; 00507 } else { 00508 $fileLines[$il]["type"] = "covered_cont"; 00509 } 00510 } 00511 $coveredCnt ++; 00512 $type = "covered_cont"; 00513 } 00514 } 00515 else { 00516 $type = "non-exec"; 00517 $coverageLines[$lineCnt] = 0; 00518 } 00519 } 00520 else if($parser->getLineType() == LINE_TYPE_EXEC) { 00521 $uncoveredCnt ++; 00522 $type = "uncovered"; 00523 } 00524 else if($parser->getLineType() == LINE_TYPE_CONT) { 00525 if($lastLineType == "uncovered" || $lastLineType == "uncovered_cont") { 00526 $uncoveredCnt ++; 00527 $type = "uncovered_cont"; 00528 } else if($lastLineType == "covered" || $lastLineType == "covered_cont") { 00529 $coveredCnt ++; 00530 $type = "covered_cont"; 00531 } else { 00532 $type = $lastLineType; 00533 $this->logger->debug("LINE_TYPE_CONT with lastLineType=$lastLineType", 00534 __FILE__, __LINE__); 00535 } 00536 } 00537 else { 00538 $type = "non-exec"; 00539 } 00540 // Save line type 00541 $lastLineType = $type; 00542 //echo $line . "\t[" . $type . "]\n"; 00543 00544 if(!isset($coverageLines[$lineCnt])) { 00545 $coverageLines[$lineCnt] = 0; 00546 } 00547 $fileLines[$lineCnt] = array("type" => $type, "lineCnt" => $lineCnt, "line" => $line, "coverageLines" => $coverageLines[$lineCnt]); 00548 } 00549 $this->logger->debug("File lines: ". print_r($fileLines, true), 00550 __FILE__, __LINE__); 00551 for($i = 1; $i <= count($fileLines); $i++) { 00552 $filestr .= $this->writeFileTableRow($fileLines[$i]["type"], 00553 $fileLines[$i]["lineCnt"], 00554 $fileLines[$i]["line"], 00555 $fileLines[$i]["coverageLines"]); 00556 } 00557 $filestr .= "</table>"; 00558 $filestr .= $this->writePhpFileFooter(); 00559 fwrite($writer, $filestr); 00560 fclose($writer); 00561 return array( 00562 'filename' => $phpFile, 00563 'covered' => $coveredCnt, 00564 'uncovered' => $uncoveredCnt, 00565 'total' => $lineCnt 00566 ); 00567 } 00568 00569 /*}}}*/ 00570 /*{{{ protected function writeFileTableHead() */ 00571 00578 protected function writeFileTableHead() { 00579 $filestr = ""; 00580 00581 // start moodle modification: xhtml + column widths 00582 $filestr .= '<tr><td width="5%"class="coverageDetailsHead" >Line #</td>'; 00583 $filestr .= '<td width="5%" class="coverageDetailsHead">Frequency</td>'; 00584 $filestr .= '<td width="90%" class="coverageDetailsHead">Source Line</td></tr>'; 00585 // end moodle modification 00586 return $filestr; 00587 } 00588 00589 /*}}}*/ 00590 /*{{{ protected function writeFileTableRow() */ 00591 00603 protected function writeFileTableRow($type, $lineCnt, $line, $coverageLineCnt) { 00604 $spanstr = ""; 00605 if($type == "covered" || $type == "covered_cont") { 00606 $spanstr .= '<span class="codeExecuted">'; 00607 } 00608 else if($type == "uncovered" || $type == "uncovered_cont") { 00609 $spanstr .= '<span class="codeMissed">'; 00610 } 00611 else { 00612 $spanstr .= '<span>'; 00613 } 00614 00615 if(empty($coverageLineCnt)) { 00616 $coverageLineCnt = ""; 00617 } 00618 00619 $filestr = '<tr>'; 00620 $filestr .= '<td class="coverageDetails">' . $spanstr; 00621 if($type == "covered_cont" || $type == "uncovered_cont") { 00622 $filestr .= '+'; 00623 } 00624 $filestr .= $lineCnt . '</span></td>'; 00625 if(empty($coverageLineCnt)) { 00626 $coverageLineCnt = " "; 00627 } 00628 $filestr .= '<td class="coverageDetails">' . $spanstr . $coverageLineCnt . '</span></td>'; 00629 $filestr .= '<td class="coverageDetailsCode"><code>' . $spanstr . $this->preserveSpacing($line) . '</span></code></td>'; 00630 $filestr .= "</tr>"; 00631 return $filestr; 00632 } 00633 00634 /*}}}*/ 00635 /*{{{ protected function preserveSpacing() */ 00636 00644 protected function preserveSpacing($string) { 00645 // start moodle modification: xhtml 00646 if (empty($string)) { 00647 return ' '; 00648 } 00649 // end moodle modification 00650 $string = htmlspecialchars($string); 00651 $string = str_replace(" ", " ", $string); 00652 $string = str_replace("\t", " ", $string); 00653 return $string; 00654 } 00655 00656 /*}}}*/ 00657 } 00658 ?>