more code coverage reporting

This commit is contained in:
tmont 2009-06-29 01:57:30 +00:00
parent d6faf4533e
commit 6b0d911cd2
6 changed files with 219 additions and 53 deletions

View File

@ -19,6 +19,43 @@
*/ */
class ConsoleTestRunner extends TestRunner { class ConsoleTestRunner extends TestRunner {
/**
* @see setFiles()
* @var array
*/
protected $files;
/**
* Constructor
*
* @author Tommy Montgomery
* @version 1.0
* @since 1.0
*
* @param array $tests
* @param array $listeners
* @param array $options
*/
public function __construct(array $tests = array(), array $listeners = array(), array $options = array()) {
parent::__construct($tests, $listeners, $options);
$this->files = array();
}
/**
* Sets the files to parse for tests
*
* @author Tommy Montgomery
* @version 1.0
* @since 1.0
*
* @param array $files Array of files and/or directories to parse for tests
* @return ConsoleTestRunner
*/
public final function setFiles(array $files) {
$this->files = $files;
return $this;
}
/** /**
* {@inheritdoc} * {@inheritdoc}
* *
@ -36,6 +73,14 @@
if ($bootstrap !== null) { if ($bootstrap !== null) {
require_once $bootstrap; require_once $bootstrap;
} }
//accumulate tests from files
if (!empty($this->files)) {
foreach (TestAccumulator::getTests($this->files, $this->getOption('recursive')) as $test) {
$this->addTest($test);
}
}
} }
/** /**

View File

@ -9,6 +9,70 @@
private function __construct() {} private function __construct() {}
private static function parseCoverageData(array $coverageData) {
$newData = array();
foreach ($coverageData as $file => $data) {
$classes = Util::getClassNamesFromFile($file);
//print_r($classes);
$refClasses = array();
foreach ($classes as $class) {
$refClasses[] = new ReflectionClass($class);
}
$newData[$file] = array();
foreach ($data as $line => $unitsCovered) {
$loc = 1;
$dloc = ($unitsCovered === self::DEAD) ? 1 : 0;
$cloc = ($unitsCovered > 0) ? 1 : 0;
//find the class this line resides in, if any
$class = null;
foreach ($refClasses as $refClass) {
if ($line >= $refClass->getStartLine() && $line <= $refClass->getEndLine()) {
$class = $refClass;
//find the method this line resides in
foreach ($class->getMethods() as $refMethod) {
if ($line >= $refMethod->getStartLine() && $line <= $refMethod->getEndLine()) {
if (isset($newData[$file]['classes'][$class->getName()][$refMethod->getName()])) {
$newData[$file]['classes'][$class->getName()][$refMethod->getName()]['loc'] += $loc;
$newData[$file]['classes'][$class->getName()][$refMethod->getName()]['dloc'] += $dloc;
$newData[$file]['classes'][$class->getName()][$refMethod->getName()]['cloc'] += $cloc;
} else {
$newData[$file]['classes'][$class->getName()][$refMethod->getName()] = array(
'loc' => $loc,
'dloc' => $dloc,
'cloc' => $cloc
);
}
break;
}
}
break;
}
}
//procedural code
if ($class === null) {
if (isset($newData[$file]['procedural'])) {
$newData[$file]['procedural']['loc'] += $loc;
$newData[$file]['procedural']['dloc'] += $dloc;
$newData[$file]['procedural']['cloc'] += $cloc;
} else {
$newData[$file]['procedural'] = array(
'loc' => $loc,
'dloc' => $dloc,
'cloc' => $cloc
);
}
}
}
}
return $newData;
}
public static function createConsoleReport(array $coverageData) { public static function createConsoleReport(array $coverageData) {
$coverageData = CoverageFilter::filter($coverageData); $coverageData = CoverageFilter::filter($coverageData);
fwrite(STDOUT, "\nCode coverage information:\n\n"); fwrite(STDOUT, "\nCode coverage information:\n\n");
@ -47,11 +111,7 @@
fwrite(STDOUT, " Executable: $totloc (" . round($totcloc / $totloc * 100, 2) . "%)\n"); fwrite(STDOUT, " Executable: $totloc (" . round($totcloc / $totloc * 100, 2) . "%)\n");
} }
public static function createHtmlReport($dir, array $coverageData) { public static function createHtmlReport($coverageDir, array $coverageData) {
if (!is_dir($dir)) {
throw new TUnitException($dir . ' is not a directory');
}
$coverageData = CoverageFilter::filter($coverageData); $coverageData = CoverageFilter::filter($coverageData);
$baseDir = array(); $baseDir = array();
@ -72,45 +132,72 @@
$baseDir = implode(DIRECTORY_SEPARATOR, $baseDir) . DIRECTORY_SEPARATOR; $baseDir = implode(DIRECTORY_SEPARATOR, $baseDir) . DIRECTORY_SEPARATOR;
$totalData = array(); $dirData = array();
foreach ($coverageData as $file => $data) { foreach ($coverageData as $file => $data) {
$totalData[$file] = array( self::writeHtmlFile($file, $baseDir, $coverageDir, self::parseCoverageData(array($file => $data)), $data);
'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 //copy css over
$template = dirname(__FILE__) . DIRECTORY_SEPARATOR . self::TEMPLATE_DIR . DIRECTORY_SEPARATOR; $template = dirname(__FILE__) . DIRECTORY_SEPARATOR . self::TEMPLATE_DIR . DIRECTORY_SEPARATOR;
copy($template . 'style.css', $dir . DIRECTORY_SEPARATOR . 'style.css'); copy($template . 'style.css', $coverageDir . DIRECTORY_SEPARATOR . 'style.css');
} }
private static function writeHtmlFile($sourceFile, $baseDir, $coverageDir, array $data) { private static function writeHtmlFile($sourceFile, $baseDir, $coverageDir, array $classData, array $coverageData) {
$lines = file($sourceFile, FILE_IGNORE_NEW_LINES); //summary view
$code = ''; $fileCoverage = '';
$classCoverage = '';
$tloc = 0;
$tdloc = 0;
$tcloc = 0;
foreach ($classData[$sourceFile]['classes'] as $class => $methods) {
$classCoverage = '';
$classLoc = 0;
$classDloc = 0;
$classCloc = 0;
$methodCoverage = '';
$refClass = new ReflectionClass($class);
foreach ($methods as $method => $methodData) {
$tloc += $methodData['loc'];
$tdloc += $methodData['dloc'];
$tcloc += $methodData['cloc'];
$methodStartLine = $refClass->getMethod($method)->getStartLine();
$methodCoverage .= "<tr class=\"method-coverage\"><th>&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#line-$methodStartLine\">$method</a></th>";
$methodCoverage .= "<td>$methodData[cloc] / " . ($methodData['loc'] - $methodData['dloc']) . "</td>";
$methodCoverage .= "<td>" . round($methodData['cloc'] / ($methodData['loc'] - $methodData['dloc']) * 100, 2) . "%</td></tr>\n";
$classLoc += $methodData['loc'];
$classDloc += $methodData['dloc'];
$classCloc += $methodData['cloc'];
}
$classStartLine = $refClass->getStartLine();
$classCoverage .= "<tr class=\"class-coverage\"><th>&nbsp;&nbsp;<a href=\"#line-$classStartLine\">$class</a></th><td>$classCloc / " . ($classLoc - $classDloc) . "</td>";
$classCoverage .= "<td>" . round($classCloc / ($classLoc - $classDloc) * 100, 2) . "%</td></tr>\n";
$classCoverage .= $methodCoverage;
}
$fileCoveragePercent = round($tcloc / max($tloc - $tdloc, 1) * 100, 2);
$fileCoverage = "<tr class=\"file-coverage\"><th>$sourceFile</th><td>$tcloc / " . ($tloc - $tdloc) . "</td><td>$fileCoveragePercent%</td></tr>\n";
$fileCoverage .= $classCoverage;
unset($classCoverage, $methodCoverage, $classData, $refClass);
//code view
$lines = file($sourceFile, FILE_IGNORE_NEW_LINES);
$code = '';
$lineNumbers = ''; $lineNumbers = '';
for ($i = 1, $len = count($lines); $i <= $len; $i++) { for ($i = 1, $len = count($lines); $i <= $len; $i++) {
$lineNumbers .= '<div><a href="#line-' . $i . '">' . $i . '</a></div>'; $lineNumbers .= '<div><a name="#line-' . $i . '" href="#line-' . $i . '">' . $i . '</a></div>';
$code .= '<div'; $code .= '<div';
if (isset($data[$i])) { if (isset($coverageData[$i])) {
$code .= ' class="'; $code .= ' class="';
if ($data[$i] > 0) { if ($coverageData[$i] > 0) {
$code .= 'covered'; $code .= 'covered';
} else if ($data[$i] === self::DEAD) { } else if ($coverageData[$i] === self::DEAD) {
$code .= 'dead'; $code .= 'dead';
} else if ($data[$i] === self::UNUSED) { } else if ($coverageData[$i] === self::UNUSED) {
$code .= 'uncovered'; $code .= 'uncovered';
} }
@ -125,7 +212,6 @@
$code .= htmlentities(str_replace("\t", ' ', $lines[$i - 1]), ENT_QUOTES) ."</div>\n"; $code .= htmlentities(str_replace("\t", ' ', $lines[$i - 1]), ENT_QUOTES) ."</div>\n";
} }
unset($lines); unset($lines);
$fileName = str_replace(array($baseDir, DIRECTORY_SEPARATOR), array('', '_'), $sourceFile); $fileName = str_replace(array($baseDir, DIRECTORY_SEPARATOR), array('', '_'), $sourceFile);
@ -140,21 +226,23 @@
} }
$template = file_get_contents(dirname(__FILE__) . DIRECTORY_SEPARATOR . self::TEMPLATE_DIR . DIRECTORY_SEPARATOR . 'file.html'); $template = file_get_contents(dirname(__FILE__) . DIRECTORY_SEPARATOR . self::TEMPLATE_DIR . DIRECTORY_SEPARATOR . 'file.html');
$template = preg_replace( $template = str_replace(
array( array(
'/\$\{title\}/', '${title}',
'/\$\{file\.name\}/', '${file.link}',
'/\$\{line.numbers\}/', '${file.coverage}',
'/\$\{code\}/', '${line.numbers}',
'/\$\{timestamp\}/', '${code}',
'/\$\{product\.name\}/', '${timestamp}',
'/\$\{product\.version\}/', '${product.name}',
'/\$\{product\.website\}/', '${product.version}',
'/\$\{product\.author\}/' '${product.website}',
'${product.author}'
), ),
array( array(
Product::NAME . ' - Coverage Report', Product::NAME . ' - Coverage Report',
$link . basename($sourceFile), $link . basename($sourceFile),
$fileCoverage,
$lineNumbers, $lineNumbers,
$code, $code,
date('Y-m-d H:i:s'), date('Y-m-d H:i:s'),
@ -169,8 +257,14 @@
return file_put_contents($newFile, $template); return file_put_contents($newFile, $template);
} }
private static function writeHtmlDir(array $data) { private static function writeHtmlDirectories($coverageDir, $baseDir, array $dirData) {
ksort($dirData);
$newDirData = array();
foreach ($dirData as $dir => $data) {
$dir = str_replace($baseDir, '', $dir);
}
} }
} }

View File

@ -13,7 +13,16 @@
<div id="wrapper"> <div id="wrapper">
<div id="header"> <div id="header">
<h1>${file.name}</h1> <h1>${file.link}</h1>
</div>
<div id="summary">
<table>
<tr>
<td></td><th colspan="3">Coverage</th>
</tr>
${file.coverage}
</table>
</div> </div>
<div id="code-wrapper"> <div id="code-wrapper">

View File

@ -4,6 +4,8 @@ html {
body { body {
background-color: #FFFFCC; background-color: #FFFFCC;
color: #000000; color: #000000;
font-family: Verdana, sans-serif;
font-size: 8pt;
} }
#code-wrapper { #code-wrapper {
@ -34,6 +36,13 @@ body {
#code div { #code div {
white-space: pre; white-space: pre;
} }
#summary th {
text-align: left;
}
#footer {
text-align: center;
}
.covered { .covered {
background-color: #66FF66; background-color: #66FF66;

View File

@ -248,6 +248,23 @@
return $closure; return $closure;
} }
public static function getClassNamesFromFile($file) {
$tokens = token_get_all(file_get_contents($file));
$classes = array();
for ($i = 0, $len = count($tokens); $i < $len; $i++) {
if (
is_array($tokens[$i]) && $tokens[$i][0] === T_CLASS &&
isset($tokens[$i + 1]) && is_array($tokens[$i + 1]) && $tokens[$i + 1][0] === T_WHITESPACE &&
isset($tokens[$i + 2]) && is_array($tokens[$i + 2]) && $tokens[$i + 2][0] === T_STRING
) {
$classes[] = $tokens[$i + 2][1];
$i += 2;
}
}
return $classes;
}
} }
?> ?>

View File

@ -71,15 +71,7 @@
$runner = new ConsoleTestRunner(); $runner = new ConsoleTestRunner();
$runner->setOptions($options); $runner->setOptions($options);
//accumulate tests $runner->setFiles($args)
$tests = TestAccumulator::getTests($args, $runner->getOption('recursive'));
if (empty($tests)) {
fwrite(STDERR, 'Found no TestCase subclasses in the given files');
exit(1);
}
$runner->addTests($tests)
->addListener(new ConsoleListener()) ->addListener(new ConsoleListener())
->run(); ->run();