initial html coverage reports

This commit is contained in:
tmont 2009-06-28 04:50:53 +00:00
parent a57c690dc5
commit 3c47c48488
6 changed files with 224 additions and 12 deletions

View File

@ -40,6 +40,16 @@
} }
} }
/**
* {@inheritdoc}
*
* @author Tommy Montgomery
* @since 1.0
* @version 1.0
* @uses getOption()
* @uses CoverageReporter::createConsoleReport()
* @uses CoverageReporter::createHtmlReport()
*/
protected function postRun() { protected function postRun() {
$html = $this->getOption('coverage-html'); $html = $this->getOption('coverage-html');
$console = $this->getOption('coverage-console'); $console = $this->getOption('coverage-console');

View File

@ -66,10 +66,19 @@
throw new LogicException('The class "' . $class . '" is final and cannot be mocked'); throw new LogicException('The class "' . $class . '" is final and cannot be mocked');
} }
$constructor = $refClass->getConstructor();
if ($constructor === null) {
$constructor = '__construct';
$callParent = false;
} else {
$constructor = $constructor->getName();
}
$this->referenceObject = $refClass; $this->referenceObject = $refClass;
$this->methods = array( $this->methods = array(
'default' => array( 'default' => array(
$this->referenceObject->getConstructor()->getName() => array( $constructor => array(
'body' => '', 'body' => '',
'call_parent' => (bool)$callParent 'call_parent' => (bool)$callParent
) )

View File

@ -2,8 +2,10 @@
class CoverageReporter { class CoverageReporter {
const UNUSED = -1; const UNUSED = -1;
const DEAD = -2; const DEAD = -2;
const TEMPLATE_DIR = 'template';
private function __construct() {} private function __construct() {}
@ -45,24 +47,130 @@
fwrite(STDOUT, " Executable: $totloc (" . round($totcloc / $totloc * 100, 2) . "%)\n"); fwrite(STDOUT, " Executable: $totloc (" . round($totcloc / $totloc * 100, 2) . "%)\n");
} }
public static function createXmlReport($file, array $coverageData) {
}
public static function createHtmlReport($dir, array $coverageData) { public static function createHtmlReport($dir, array $coverageData) {
if (!is_dir($dir)) { if (!is_dir($dir)) {
throw new TUnitException($dir . ' is not a directory'); throw new TUnitException($dir . ' is not a directory');
} }
$coverageData = CoverageFilter::filter($coverageData); $coverageData = CoverageFilter::filter($coverageData);
$baseDir = array();
foreach ($coverageData as $file => $data) { foreach ($coverageData as $file => $data) {
foreach ($data as $unitsCovered) { $dirs = explode(DIRECTORY_SEPARATOR, dirname($file));
if (empty($baseDir)) {
$baseDir = $dirs;
} else {
for ($i = 0, $len = count($dirs); $i < $len; $i++) {
if (!isset($baseDir[$i]) || $baseDir[$i] !== $dirs[$i]) {
break;
}
}
$baseDir = array_slice($dirs, 0, $i);
} }
} }
$baseDir = implode(DIRECTORY_SEPARATOR, $baseDir) . DIRECTORY_SEPARATOR;
$totalData = array();
foreach ($coverageData as $file => $data) {
$totalData[$file] = array(
'loc' => 0,
'dloc' => 0,
'cloc' => 0
);
foreach ($data as $line => $unitsCovered) {
$totalData[$file]['loc']++;
if ($unitsCovered > 0) {
$totalData[$file]['cloc']++;
} else if ($unitsCovered === self::DEAD) {
$totalData[$file]['dloc']++;
}
}
self::writeHtmlFile($file, $baseDir, $dir, $data);
}
//copy css over
$template = dirname(__FILE__) . DIRECTORY_SEPARATOR . self::TEMPLATE_DIR . DIRECTORY_SEPARATOR;
copy($template . 'style.css', $dir . DIRECTORY_SEPARATOR . 'style.css');
}
private static function writeHtmlFile($sourceFile, $baseDir, $coverageDir, array $data) {
$lines = file($sourceFile, FILE_IGNORE_NEW_LINES);
$code = '';
$lineNumbers = '';
for ($i = 1, $len = count($lines); $i <= $len; $i++) {
$lineNumbers .= '<div><a href="#line-' . $i . '">' . $i . '</a></div>';
$code .= '<div';
if (isset($data[$i])) {
$code .= ' class="';
if ($data[$i] > 0) {
$code .= 'covered';
} else if ($data[$i] === self::DEAD) {
$code .= 'dead';
} else if ($data[$i] === self::UNUSED) {
$code .= 'uncovered';
}
$code .= '">';
} else {
$code .= '>';
}
if (empty($lines[$i])) {
$lines[$i] = ' ';
}
$code .= htmlentities(str_replace("\t", ' ', $lines[$i - 1]), ENT_QUOTES) ."</div>\n";
}
unset($lines);
$fileName = str_replace(array($baseDir, DIRECTORY_SEPARATOR), array('', '_'), $sourceFile);
$newFile = $coverageDir . DIRECTORY_SEPARATOR . $fileName . '.html';
$link = '<a href="./index.html">' . $baseDir . '</a>';
$dirs = preg_split('@\\' . DIRECTORY_SEPARATOR . '@', str_replace($baseDir, '', dirname($sourceFile) . DIRECTORY_SEPARATOR), -1, PREG_SPLIT_NO_EMPTY);
$path = '';
foreach ($dirs as $dir) {
$path = ltrim($path . '_' . $dir, '_');
$link .= '<a href="./' . $path . '.html">' . $dir . '</a>' . DIRECTORY_SEPARATOR;
}
$template = file_get_contents(dirname(__FILE__) . DIRECTORY_SEPARATOR . self::TEMPLATE_DIR . DIRECTORY_SEPARATOR . 'file.html');
$template = preg_replace(
array(
'/\$\{title\}/',
'/\$\{file\.name\}/',
'/\$\{line.numbers\}/',
'/\$\{code\}/',
'/\$\{timestamp\}/',
'/\$\{product\.name\}/',
'/\$\{product\.version\}/',
'/\$\{product\.website\}/',
'/\$\{product\.author\}/'
),
array(
Product::NAME . ' - Coverage Report',
$link . basename($sourceFile),
$lineNumbers,
$code,
date('Y-m-d H:i:s'),
Product::NAME,
Product::VERSION,
Product::WEBSITE,
Product::AUTHOR
),
$template
);
return file_put_contents($newFile, $template);
}
private static function writeHtmlDir(array $data) {
} }
} }

View File

@ -0,0 +1,39 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
<head>
<title>${title}</title>
<meta http-equiv="content-type" content="text/html; charset=utf-8"/>
<link rel="stylesheet" type="text/css" href="./style.css"/>
</head>
<body>
<div id="wrapper">
<div id="header">
<h1>${file.name}</h1>
</div>
<div id="code-wrapper">
<div id="line-numbers">
${line.numbers}
</div>
<div id="code">
${code}
</div>
</div>
<div id="footer">
<p>
Generated on ${timestamp} by <a href="${product.website}">${product.name} ${product.version}</a>
<br />
&copy; 2009 ${product.author}
</p>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,46 @@
html {
overflow: auto;
}
body {
background-color: #FFFFCC;
color: #000000;
}
#code-wrapper {
background-color: #FFFFFF;
font-family: Consolas, "Courier New", monospace;
font-size: 16px;
border: 2px solid #000000;
}
#line-numbers {
background-color: #CCCCCC;
float: left;
text-align: right;
padding: 0 5px;
border-right: 2px solid #000000;
}
#line-numbers a {
color: #000000;
text-decoration: none;
display: block;
}
#line-numbers div {
white-space: pre;
}
#code {
padding-left: 10px;
overflow: auto;
}
#code div {
white-space: pre;
}
.covered {
background-color: #66FF66;
}
.uncovered {
background-color: #FF6666;
}
.dead {
background-color: #999999;
}

View File

@ -48,7 +48,7 @@
->addSwitch(new CliSwitch('usage', null, false, null, 'Display this help message')) ->addSwitch(new CliSwitch('usage', null, false, null, 'Display this help message'))
->addSwitch(new CliSwitch('recursive', null, false, null, 'Recurse into subdirectories')) ->addSwitch(new CliSwitch('recursive', null, false, null, 'Recurse into subdirectories'))
->addSwitch(new CliSwitch('bootstrap', 'b', false, 'file', 'File to include before tests are run')) ->addSwitch(new CliSwitch('bootstrap', 'b', false, 'file', 'File to include before tests are run'))
->addSwitch(new CliSwitch('coverage-xml', null, false, 'file', 'Generate code coverage report in XML clover format (requires xdebug)')) //->addSwitch(new CliSwitch('coverage-xml', null, false, 'file', 'Generate code coverage report in XML clover format (requires xdebug)'))
->addSwitch(new CliSwitch('coverage-html', null, false, 'dir', 'Generate code coverage report in HTML (requires xdebug, optionally uses ezComponents)')) ->addSwitch(new CliSwitch('coverage-html', null, false, 'dir', 'Generate code coverage report in HTML (requires xdebug, optionally uses ezComponents)'))
->addSwitch(new CliSwitch('coverage-console', null, false, null, 'Generate code coverage report suitable for console viewing')); ->addSwitch(new CliSwitch('coverage-console', null, false, null, 'Generate code coverage report suitable for console viewing'));