* @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 .= 'Lines | ';
// 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 .= '' . $realFileShort. '' . ' | ';
$str .= '' . $fileCoverage['total'] . " | ";
$str .= '' . $fileCoverage['covered'] . " | ";
$str .= '' . $fileCoverage['uncovered'] . " | ";
$str .= '' . ($fileCoverage['covered']+$fileCoverage['uncovered']) . " | ";
if($fileCoverage['uncovered'] + $fileCoverage['covered'] == 0) {
// If there are no executable lines, assume coverage to be 100%
$str .= '100% |
';
}
else {
$str .= ''
. round(($fileCoverage['covered']/($fileCoverage['uncovered']
+ $fileCoverage['covered']))*100.0, 2)
. '% | ';
}
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 .= ' ';
$str .= 'Summary';
$str .= '';
// start moodle modification: xhtml
$str .= '';
$str .= 'Overall Code Coverage | ';
$str .= '' . $this->getGrandCodeCoveragePercentage() . '% | ';
// end moodle modification
$str .= ' ';
$str .= 'Total Covered Lines of Code | ';
$str .= '' . $this->grandTotalCoveredLines.' | ';
$str .= '(' . TOTAL_COVERED_LINES_EXPLAIN . ') | ';
$str .= ' ';
$str .= 'Total Missed Lines of Code | ';
$str .= '' . $this->grandTotalUncoveredLines.' | ';
$str .= '(' . TOTAL_UNCOVERED_LINES_EXPLAIN . ') | ';
$str .= ' ';
$str .= 'Total Lines of Code | ';
$str .= '' . ($this->grandTotalCoveredLines + $this->grandTotalUncoveredLines) .' | ';
$str .= '(' .
TOTAL_LINES_OF_CODE_EXPLAIN . ') | ';
$str .= ' ';
$str .= 'Total Lines | ';
$str .= '' . $this->grandTotalLines.' | ';
$str .= '(' . TOTAL_LINES_EXPLAIN . ') | ';
$str .= ' ';
$str .= 'Total Files | ';
$str .= '' . $this->grandTotalFiles.' | ';
$str .= '(' . TOTAL_FILES_EXPLAIN . ') | ';
$str .= ' ';
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 .= ' |
';
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 .= "
";
$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;
}
/*}}}*/
}
?>