* @version $Revision$ * @package SpikePHPCoverage_Reporter */ class HtmlCoverageReporter extends CoverageReporter { /*{{{ Members */ private $coverageData; private $htmlFile; private $body; private $header = "html/header.html"; private $footer = "html/footer.html"; private $indexHeader = "html/indexheader.html"; private $indexFooter = "html/indexfooter.html"; /*}}}*/ /*{{{ public function __construct() */ /** * Constructor method (PHP5 only) * * @param $heading Heading of the report (shown as title) * @param $style Name of the stylesheet file * @param $dir Directory where the report files should be dumped * @access public */ public function __construct( $heading="Coverage Report", $style="", $dir="report" ) { parent::__construct($heading, $style, $dir); } /*}}}*/ /*{{{ public function generateReport() */ /** * Implementaion of generateReport abstract function. * This is the only function that will be called * by the instrumentor. * * @param &$data Reference to Coverage Data * @access public */ public function generateReport(&$data) { if(!file_exists($this->outputDir)) { mkdir($this->outputDir); } $this->coverageData =& $data; $this->grandTotalFiles = count($this->coverageData); $ret = $this->writeIndexFile(); if($ret === FALSE) { $this->logger->error("Error occured!!!", __FILE__, __LINE__); } $this->logger->debug(print_r($data, true), __FILE__, __LINE__); } /*}}}*/ /*{{{ private function writeIndexFileHeader() */ /** * Write the index file header to a string * * @return string String containing HTML code for the index file header * @access private */ private function writeIndexFileHeader() { $str = false; $dir = realpath(dirname(__FILE__)); if($dir !== false) { $str = file_get_contents($dir . "/" . $this->indexHeader); if($str == false) { return $str; } $str = str_replace("%%heading%%", $this->heading, $str); $str = str_replace("%%style%%", $this->style, $str); } return $str; } /*}}}*/ /*{{{ private function writeIndexFileFooter() */ /** * Write the index file footer to a string * * @return string String containing HTML code for the index file footer. * @access private */ private function writeIndexFileFooter() { $str = false; $dir = realpath(dirname(__FILE__)); if($dir !== false) { $str = file_get_contents($dir . "/" . $this->indexFooter); if($str == false) { return $str; } } return $str; } /*}}}*/ /*{{{ private function createJSDir() */ /** * Create a directory for storing Javascript for the report * * @access private */ private function createJSDir() { $jsDir = $this->outputDir . "/js"; if(file_exists($this->outputDir) && !file_exists($jsDir)) { mkdir($jsDir); } $jsSortFile = realpath(dirname(__FILE__)) . "/js/sort_spikesource.js"; copy($jsSortFile, $jsDir . "/" . "sort_spikesource.js"); return true; } /*}}}*/ /*{{{ private function createImagesDir() */ /** * Create a directory for storing images for the report * * @access private */ private function createImagesDir() { $imagesDir = $this->outputDir . "/images"; if(file_exists($this->outputDir) && !file_exists($imagesDir)) { mkdir($imagesDir); } $imagesSpikeDir = $imagesDir . "/spikesource"; if(!file_exists($imagesSpikeDir)) { mkdir($imagesSpikeDir); } $imagesArrowUpFile = realpath(dirname(__FILE__)) . "/images/arrow_up.gif"; $imagesArrowDownFile = realpath(dirname(__FILE__)) . "/images/arrow_down.gif"; $imagesPHPCoverageLogoFile = realpath(dirname(__FILE__)) . "/images/spikesource/phpcoverage.gif"; $imagesSpacerFile = realpath(dirname(__FILE__)) . "/images/spacer.gif"; copy($imagesArrowUpFile, $imagesDir . "/" . "arrow_up.gif"); copy($imagesArrowDownFile, $imagesDir . "/" . "arrow_down.gif"); copy($imagesSpacerFile, $imagesDir . "/" . "spacer.gif"); copy($imagesPHPCoverageLogoFile, $imagesSpikeDir . "/" . "phpcoverage.gif"); return true; } /*}}}*/ /*{{{ private function createStyleDir() */ private function createStyleDir() { if(isset($this->style)) { $this->style = trim($this->style); } if(empty($this->style)) { $this->style = "spikesource.css"; } $styleDir = $this->outputDir . "/css"; if(file_exists($this->outputDir) && !file_exists($styleDir)) { mkdir($styleDir); } $styleFile = realpath(dirname(__FILE__)) . "/css/" . $this->style; copy($styleFile, $styleDir . "/" . $this->style); return true; } /*}}}*/ /*{{{ protected function writeIndexFileTableHead() */ /** * Writes the table heading for index.html * * @return string Table heading row code * @access protected */ protected function writeIndexFileTableHead() { $str = ""; $str .= '

Details

'; $str .= ''; // start moodle modification: xhtml links $str .= ''; // end moodle modification $str .= ''; // start moodle modification: xhtml links $str .= ''; // end moodle modification $str .= ''; // Second row - subheadings $str .= ''; // start moodle modification: xhtml links $str .= ''; $str .= ''; $str .= ''; $str .= ''; // end moodle modification $str .= ''; $str .= ''; return $str; } /*}}}*/ /*{{{ protected function writeIndexFileTableRow() */ /** * Writes one row in the index.html table to display filename * and coverage recording. * * @param $fileLink link to html details file. * @param $realFile path to real PHP file. * @param $fileCoverage Coverage recording for that file. * @return string HTML code for a single row. * @access protected */ protected function writeIndexFileTableRow($fileLink, $realFile, $fileCoverage) { global $util; $fileLink = $this->makeRelative($fileLink); $realFileShort = $util->shortenFilename($realFile); $str = ""; $str .= ''; $str .= '"; $str .= '"; $str .= '"; $str .= '"; if($fileCoverage['uncovered'] + $fileCoverage['covered'] == 0) { // If there are no executable lines, assume coverage to be 100% $str .= ''; } else { $str .= ''; } return $str; } /*}}}*/ /*{{{ protected function writeIndexFileGrandTotalPercentage() */ /** * Writes the grand total for coverage recordings on the index.html * * @return string HTML code for grand total row * @access protected */ protected function writeIndexFileGrandTotalPercentage() { $str = ""; $str .= "

" . $this->heading . "


"; $str .= '
File Name LinesCode Coverage
Total Covered Missed Executable
'; $str .= '' . $realFileShort. '' . '' . $fileCoverage['total'] . "' . $fileCoverage['covered'] . "' . $fileCoverage['uncovered'] . "' . ($fileCoverage['covered']+$fileCoverage['uncovered']) . "100%
' . round(($fileCoverage['covered']/($fileCoverage['uncovered'] + $fileCoverage['covered']))*100.0, 2) . '%
'; $str .= ''; if(!empty($this->coverageData)) { foreach($this->coverageData as $filename => &$lines) { $realFile = realpath($filename); $fileLink = $this->outputDir . $util->unixifyPath($realFile). ".html"; $fileCoverage = $this->markFile($realFile, $fileLink, $lines); if(empty($fileCoverage)) { return false; } $this->recordFileCoverageInfo($fileCoverage); $this->updateGrandTotals($fileCoverage); $str .= $this->writeIndexFileTableRow($fileLink, $realFile, $fileCoverage); unset($this->coverageData[$filename]); } } $str .= ''; $str .= "

Summary

'; $str .= ''; // start moodle modification: xhtml $str .= ''; $str .= ''; $str .= ''; // end moodle modification $str .= ''; $str .= ''; $str .= ''; $str .= ''; $str .= ''; $str .= ''; $str .= ''; $str .= ''; $str .= ''; $str .= ''; $str .= ''; $str .= ''; $str .= ''; $str .= ''; $str .= ''; $str .= ''; $str .= ''; $str .= ''; $str .= ''; $str .= ''; $str .= '
Overall Code Coverage ' . $this->getGrandCodeCoveragePercentage() . '%
Total Covered Lines of Code ' . $this->grandTotalCoveredLines.'(' . TOTAL_COVERED_LINES_EXPLAIN . ')
Total Missed Lines of Code ' . $this->grandTotalUncoveredLines.'(' . TOTAL_UNCOVERED_LINES_EXPLAIN . ')
Total Lines of Code ' . ($this->grandTotalCoveredLines + $this->grandTotalUncoveredLines) .'(' . TOTAL_LINES_OF_CODE_EXPLAIN . ')
Total Lines ' . $this->grandTotalLines.'(' . TOTAL_LINES_EXPLAIN . ')
Total Files ' . $this->grandTotalFiles.'(' . TOTAL_FILES_EXPLAIN . ')
'; return $str; } /*}}}*/ /*{{{ protected function writeIndexFile() */ /** * Writes index.html file from all coverage recordings. * * @return boolean FALSE on failure * @access protected */ protected function writeIndexFile() { global $util; $str = ""; $this->createJSDir(); $this->createImagesDir(); $this->createStyleDir(); $this->htmlFile = $this->outputDir . "/index.html"; $indexFile = fopen($this->htmlFile, "w"); if(empty($indexFile)) { $this->logger->error("Cannot open file for writing: $this->htmlFile", __FILE__, __LINE__); return false; } $strHead = $this->writeIndexFileHeader(); if($strHead == false) { return false; } $str .= $this->writeIndexFileTableHead(); $str .= '
"; $str .= "

Report Generated On: " . $util->getTimeStamp() . "
"; $str .= "Generated using Spike PHPCoverage " . $this->recorder->getVersion() . "

"; // Get the summary $strSummary = $this->writeIndexFileGrandTotalPercentage(); // Merge them - with summary on top $str = $strHead . $strSummary . $str; $str .= $this->writeIndexFileFooter(); fwrite($indexFile, $str); fclose($indexFile); return TRUE; } /*}}}*/ /*{{{ private function writePhpFileHeader() */ /** * Write the header for the source file with mark-up * * @param $filename Name of the php file * @return string String containing the HTML for PHP file header * @access private */ private function writePhpFileHeader($filename, $fileLink) { $fileLink = $this->makeRelative($fileLink); $str = false; $dir = realpath(dirname(__FILE__)); if($dir !== false) { $str = file_get_contents($dir . "/" . $this->header); if($str == false) { return $str; } $str = str_replace("%%filename%%", $filename, $str); // Get the path to parent CSS directory $relativeCssPath = $this->getRelativeOutputDirPath($fileLink); $relativeCssPath .= "/css/" . $this->style; $str = str_replace("%%style%%", $relativeCssPath, $str); } return $str; } /*}}}*/ /*{{{ private function writePhpFileFooter() */ /** * Write the footer for the source file with mark-up * * @return string String containing the HTML for PHP file footer * @access private */ private function writePhpFileFooter() { $str = false; $dir = realpath(dirname(__FILE__)); if($dir !== false) { $str = file_get_contents($dir . "/" . $this->footer); if($str == false) { return $str; } } return $str; } /*}}}*/ /*{{{ protected function markFile() */ /** * Mark a source code file based on the coverage data gathered * * @param $phpFile Name of the actual source file * @param $fileLink Link to the html mark-up file for the $phpFile * @param &$coverageLines Coverage recording for $phpFile * @return boolean FALSE on failure * @access protected */ protected function markFile($phpFile, $fileLink, &$coverageLines) { global $util; $fileLink = $util->replaceBackslashes($fileLink); $parentDir = $util->replaceBackslashes(dirname($fileLink)); if(!file_exists($parentDir)) { //echo "\nCreating dir: $parentDir\n"; $util->makeDirRecursive($parentDir, 0755); } $writer = fopen($fileLink, "w"); if(empty($writer)) { $this->logger->error("Could not open file for writing: $fileLink", __FILE__, __LINE__); return false; } // Get the header for file $filestr = $this->writePhpFileHeader(basename($phpFile), $fileLink); // Add header for table $filestr .= ''; $filestr .= $this->writeFileTableHead(); $lineCnt = $coveredCnt = $uncoveredCnt = 0; $parser = new PHPParser(); $parser->parse($phpFile); $lastLineType = "non-exec"; $fileLines = array(); while(($line = $parser->getLine()) !== false) { if (substr($line, -1) == "\n") { $line = substr($line, 0, -1); } $lineCnt++; $coverageLineNumbers = array_keys($coverageLines); if(in_array($lineCnt, $coverageLineNumbers)) { $lineType = $parser->getLineType(); if($lineType == LINE_TYPE_EXEC) { $coveredCnt ++; $type = "covered"; } else if($lineType == LINE_TYPE_CONT) { // XDebug might return this as covered - when it is // actually merely a continuation of previous line if($lastLineType == "covered" || $lastLineType == "covered_cont") { unset($coverageLines[$lineCnt]); $type = "covered_cont"; $coveredCnt ++; } else { $ft = "uncovered_cont"; for($il = $lineCnt-1 ; $il >=0 && isset($fileLines[$lineCnt-1]["type"]) && $ft == "uncovered_cont" ; $il--) { $ft = $fileLines[$il]["type"]; $uncoveredCnt --; $coveredCnt ++; if($ft == "uncovered") { $fileLines[$il]["type"] = "covered"; } else { $fileLines[$il]["type"] = "covered_cont"; } } $coveredCnt ++; $type = "covered_cont"; } } else { $type = "non-exec"; $coverageLines[$lineCnt] = 0; } } else if($parser->getLineType() == LINE_TYPE_EXEC) { $uncoveredCnt ++; $type = "uncovered"; } else if($parser->getLineType() == LINE_TYPE_CONT) { if($lastLineType == "uncovered" || $lastLineType == "uncovered_cont") { $uncoveredCnt ++; $type = "uncovered_cont"; } else if($lastLineType == "covered" || $lastLineType == "covered_cont") { $coveredCnt ++; $type = "covered_cont"; } else { $type = $lastLineType; $this->logger->debug("LINE_TYPE_CONT with lastLineType=$lastLineType", __FILE__, __LINE__); } } else { $type = "non-exec"; } // Save line type $lastLineType = $type; //echo $line . "\t[" . $type . "]\n"; if(!isset($coverageLines[$lineCnt])) { $coverageLines[$lineCnt] = 0; } $fileLines[$lineCnt] = array("type" => $type, "lineCnt" => $lineCnt, "line" => $line, "coverageLines" => $coverageLines[$lineCnt]); } $this->logger->debug("File lines: ". print_r($fileLines, true), __FILE__, __LINE__); for($i = 1; $i <= count($fileLines); $i++) { $filestr .= $this->writeFileTableRow($fileLines[$i]["type"], $fileLines[$i]["lineCnt"], $fileLines[$i]["line"], $fileLines[$i]["coverageLines"]); } $filestr .= "
"; $filestr .= $this->writePhpFileFooter(); fwrite($writer, $filestr); fclose($writer); return array( 'filename' => $phpFile, 'covered' => $coveredCnt, 'uncovered' => $uncoveredCnt, 'total' => $lineCnt ); } /*}}}*/ /*{{{ protected function writeFileTableHead() */ /** * Writes table heading for file details table. * * @return string HTML string representing one table row. * @access protected */ protected function writeFileTableHead() { $filestr = ""; // start moodle modification: xhtml + column widths $filestr .= 'Line #'; $filestr .= 'Frequency'; $filestr .= 'Source Line'; // end moodle modification return $filestr; } /*}}}*/ /*{{{ protected function writeFileTableRow() */ /** * Write a line for file details table. * * @param $color Text color * @param $bgcolor Row bgcolor * @param $lineCnt Line number * @param $line The source code line * @param $coverageLineCnt Number of time the line was executed. * @return string HTML code for a table row. * @access protected */ protected function writeFileTableRow($type, $lineCnt, $line, $coverageLineCnt) { $spanstr = ""; if($type == "covered" || $type == "covered_cont") { $spanstr .= ''; } else if($type == "uncovered" || $type == "uncovered_cont") { $spanstr .= ''; } else { $spanstr .= ''; } if(empty($coverageLineCnt)) { $coverageLineCnt = ""; } $filestr = ''; $filestr .= '' . $spanstr; if($type == "covered_cont" || $type == "uncovered_cont") { $filestr .= '+'; } $filestr .= $lineCnt . ''; if(empty($coverageLineCnt)) { $coverageLineCnt = " "; } $filestr .= '' . $spanstr . $coverageLineCnt . ''; $filestr .= '' . $spanstr . $this->preserveSpacing($line) . ''; $filestr .= ""; return $filestr; } /*}}}*/ /*{{{ protected function preserveSpacing() */ /** * Changes all tabs and spaces with HTML non-breakable spaces. * * @param $string String containing spaces and tabs. * @return string HTML string with replacements. * @access protected */ protected function preserveSpacing($string) { // start moodle modification: xhtml if (empty($string)) { return ' '; } // end moodle modification $string = htmlspecialchars($string); $string = str_replace(" ", " ", $string); $string = str_replace("\t", "    ", $string); return $string; } /*}}}*/ } ?>