added initial test coverage and filtering stuff

This commit is contained in:
tmont 2009-06-28 00:02:24 +00:00
parent 586215680e
commit 54f54d9794
8 changed files with 307 additions and 63 deletions

View File

@ -16,5 +16,7 @@
Autoloader::loadClassMapFromFile(dirname(__FILE__) . '/manifest.php'); Autoloader::loadClassMapFromFile(dirname(__FILE__) . '/manifest.php');
spl_autoload_register('Autoloader::autoload'); spl_autoload_register('Autoloader::autoload');
CoverageFilter::addDefaultFilters();
?> ?>

View File

@ -189,34 +189,55 @@
return $this; return $this;
} }
/**
* Adds tests in bulk
*
* @author Tommy Montgomery
* @version 1.0
* @since 1.0
* @uses addTest()
*
* @param array $tests
* @return TestRunner
*/
public final function addTests(array $tests) {
foreach ($tests as $test) {
$this->addTest($test);
}
return $this;
}
/**
* Gets the value of an option
*
* @author Tommy Montgomery
* @version 1.0
* @since 1.0
*
* @param string $option
* @throws InvalidArgumentException
* @return mixed
*/
public final function getOption($option) {
if (!array_key_exists($option, $this->options)) {
throw new InvalidArgumentException('Option "' . $option . '" does not exist');
}
return $this->options[$option];
}
/** /**
* Parses options * Parses options
* *
* @author Tommy Montgomery * @author Tommy Montgomery
* @version 1.0 * @version 1.0
* @since 1.0 * @since 1.0
* @uses getAllowableOptions()
* *
* @param array $unparsed The unparsed options * @param array $unparsed The unparsed options
* @throws {@link InvalidOptionException}
* @return array The parsed options * @return array The parsed options
*/ */
protected final function parseOptions(array $unparsed) { protected abstract function parseOptions(array $unparsed);
$allowedOptions = $this->getAllowableOptions();
$options = array_fill_keys(array_keys($allowedOptions), null);
foreach ($unparsed as $option => $value) {
if (!isset($allowedOptions[$option])) {
throw new InvalidOptionException($option);
}
if (gettype($value) !== $allowedOptions[$option]) {
throw new InvalidOptionException($option, 'Expected a value of type ' . $allowedOptions[$option] . ', got ' . gettype($value));
}
$options[$option] = $value;
}
return $options;
}
/** /**
* Runs the tests and returns the results * Runs the tests and returns the results
@ -263,6 +284,17 @@
} }
} }
/**
* Called before runTests()
*
* @author Tommy Montgomery
* @version 1.0
* @since 1.0
*/
protected function preRun() {
}
/** /**
* Runs all tests and publishes the results * Runs all tests and publishes the results
* *
@ -274,17 +306,34 @@
* @uses TestListener::afterTestRunner() * @uses TestListener::afterTestRunner()
*/ */
public final function run() { public final function run() {
$this->preRun();
foreach ($this->listeners as $listener) { foreach ($this->listeners as $listener) {
$listener->beforeTestRunner($this); $listener->beforeTestRunner($this);
} }
$this->startTime = microtime(true); $this->startTime = microtime(true);
$this->publishResults($this->runTests()); $results = $this->runTests();
$this->endTime = microtime(true); $this->endTime = microtime(true);
$this->publishResults($results);
foreach ($this->listeners as $listener) { foreach ($this->listeners as $listener) {
$listener->afterTestRunner($this); $listener->afterTestRunner($this);
} }
$this->postRun();
}
/**
* Called after runTests()
*
* @author Tommy Montgomery
* @version 1.0
* @since 1.0
*/
protected function postRun() {
} }
/** /**
@ -315,17 +364,6 @@
return Util::countTests($this->tests); return Util::countTests($this->tests);
} }
/**
* Gets all allowable options for this test runner
*
* @author Tommy Montgomery
* @version 1.0
* @since 1.0
*
* @return array
*/
protected abstract function getAllowableOptions();
} }
?> ?>

View File

@ -20,22 +20,124 @@
class ConsoleTestRunner extends TestRunner { class ConsoleTestRunner extends TestRunner {
/** /**
* Gets allowable options * {@inheritdoc}
*
* Currently supported options:
* - recursive (boolean)
* - bootstrap (string)
* *
* @author Tommy Montgomery * @author Tommy Montgomery
* @version 1.0
* @since 1.0 * @since 1.0
* * @version 1.0
* @uses getOption()
*/
protected function preRun() {
parent::preRun();
if ($this->getOption('coverage-html') !== null || $this->getOption('coverage-console') !== null/* || $this->getOption('coverage-xml')*/) {
xdebug_start_code_coverage(XDEBUG_CC_UNUSED | XDEBUG_CC_DEAD_CODE);
}
$bootstrap = $this->getOption('bootstrap');
if ($bootstrap !== null) {
require_once $bootstrap;
}
}
protected function postRun() {
$html = $this->getOption('coverage-html');
$console = $this->getOption('coverage-console');
if ($html !== null || $console !== null) {
$coverage = xdebug_get_code_coverage();
xdebug_stop_code_coverage();
if ($console !== null) {
CoverageReporter::createConsoleReport($coverage);
}
if ($html !== null) {
CoverageReporter::createHtmlReport($html, $coverage);
}
unset($coverage);
}
}
/**
* Parses test runner options
*
* @author Tommy Montgomery
* @since 1.0
* @version 1.0
* @uses validateOptions()
* @uses getAllowableOptions()
*
* @param array $unparsed Unparsed options
* @return array The parsed options
*/
protected function parseOptions(array $unparsed) {
$options = $this->getAllowableOptions();
foreach ($options as $name => $default) {
if (array_key_exists($name, $unparsed)) {
$options[$name] = $unparsed[$name];
}
}
$this->validateOptions($options);
return $options;
}
/**
* Validates the options
*
* @author Tommy Montgomery
* @since 1.0
* @version 1.0
*
* @param array $options
* @throws {@link InvalidOptionException}
*/
protected function validateOptions(array $options) {
if ($options['bootstrap'] !== null && !is_file($options['bootstrap'])) {
throw new InvalidOptionException('bootstrap', 'Bootstrap must be a file');
}
if (!is_bool($options['recursive'])) {
throw new InvalidOptionException('recursive', 'recursive must be a boolean');
}
if (/*$options['coverage-xml'] !== null || */$options['coverage-html'] !== null || $options['coverage-console'] !== null) {
if (!extension_loaded('xdebug')) {
throw new InvalidOptionException('coverage-(xml|html|console)', 'xdebug extension is not loaded');
}
/*if ($options['coverage-xml'] !== null) {
$dir = dirname($options['coverage-xml']);
if (!is_dir($dir)) {
if (!mkdir($dir, 0777, true)) {
throw new TUnitException('Could not create directory: ' . $dir);
}
}
}*/
if ($options['coverage-html'] !== null) {
//create coverage directory if needed
if (!is_dir($options['coverage-html'])) {
if (!mkdir($options['coverage-html'], 0777, true)) {
throw new TUnitException('Could not create directory: ' . $options['coverage-html']);
}
}
}
}
}
/**
* Gets allowable options and default values
*
* @author Tommy Montgomery
* @since 1.0
* @version 1.0
*
* @return array * @return array
*/ */
protected function getAllowableOptions() { public function getAllowableOptions() {
return array( return array(
'recursive' => 'boolean', 'bootstrap' => null,
'bootstrap' => 'string' 'recursive' => false,
//'coverage-xml' => null,
'coverage-html' => null,
'coverage-console' => false
); );
} }

View File

@ -0,0 +1,54 @@
<?php
final class CoverageFilter {
private static $files = array();
private static $directories = array();
private function __construct() {}
public static function addFile($file) {
self::$files[] = $file;
}
public static function addDirectory($dir) {
self::$directories[] = $dir;
}
public static function clear() {
self::$files = array();
self::$directories = array();
}
public static function getDirectories() {
return self::$dirs;
}
public static function getFiles() {
return self::$files;
}
public static function addDefaultFilters() {
self::$directories[] = dirname(dirname(dirname(__FILE__)));
}
public static function filter(array $data) {
foreach ($data as $file => $arr) {
if (in_array($file, self::$files)) {
unset($data[$file]);
} else {
foreach (self::$directories as $dir) {
if (strpos($file, $dir) === 0) {
unset($data[$file]);
break;
}
}
}
}
return $data;
}
}
?>

View File

@ -0,0 +1,49 @@
<?php
class CoverageReporter {
const UNUSED = -1;
const DEAD = -2;
private function __construct() {}
public static function createConsoleReport(array $coverageData) {
$coverageData = CoverageFilter::filter($coverageData);
echo "\nCode coverage information:\n\n";
//print_r($coverageData);
foreach ($coverageData as $file => $data) {
fwrite(STDOUT, $file . "\n");
$loc = 0;
$dloc = 0;
$cloc = 0;
foreach ($data as $line => $unitsCovered) {
$loc++;
if ($unitsCovered > 0) {
$cloc++;
} else if ($unitsCovered === self::DEAD) {
$dloc++;
}
}
fwrite(STDOUT, " Covered: $cloc\n");
fwrite(STDOUT, " Dead: $dloc\n");
fwrite(STDOUT, " Executable: $loc (" . round($cloc / $loc * 100, 2) . "%)\n");
}
}
public static function createXmlReport($file, array $coverageData) {
}
public static function createHtmlReport($dir, array $coverageData) {
if (!is_dir($dir)) {
throw new TUnitException($dir . ' is not a directory');
}
}
}
?>

View File

@ -3,7 +3,7 @@
/** /**
* Autoload manifest * Autoload manifest
* *
* Autogenerated by manifester.php on 2009-06-19 20:50:38 * Autogenerated by manifester.php on 2009-06-27 16:39:48
* *
* @package TUnit * @package TUnit
* @version 0.4.0 * @version 0.4.0
@ -20,6 +20,8 @@
'ConsoleListener' => 'TUnit/framework/listeners/ConsoleListener.php', 'ConsoleListener' => 'TUnit/framework/listeners/ConsoleListener.php',
'ConsoleTestRunner' => 'TUnit/framework/TestRunners.php', 'ConsoleTestRunner' => 'TUnit/framework/TestRunners.php',
'Constraint' => 'TUnit/framework/constraints/Constraint.php', 'Constraint' => 'TUnit/framework/constraints/Constraint.php',
'CoverageFilter' => 'TUnit/framework/reporting/CoverageFilter.php',
'CoverageReporter' => 'TUnit/framework/reporting/CoverageReporter.php',
'DefaultConstraint' => 'TUnit/framework/constraints/DefaultConstraint.php', 'DefaultConstraint' => 'TUnit/framework/constraints/DefaultConstraint.php',
'EmptyConstraint' => 'TUnit/framework/constraints/SimpleConstraints.php', 'EmptyConstraint' => 'TUnit/framework/constraints/SimpleConstraints.php',
'EqualConstraint' => 'TUnit/framework/constraints/EqualConstraint.php', 'EqualConstraint' => 'TUnit/framework/constraints/EqualConstraint.php',
@ -47,6 +49,7 @@
'Product' => 'TUnit/util/Product.php', 'Product' => 'TUnit/util/Product.php',
'RecursivePhpFileIterator' => 'TUnit/util/PhpFileIterator.php', 'RecursivePhpFileIterator' => 'TUnit/util/PhpFileIterator.php',
'RecursiveTestIterator' => 'TUnit/util/RecursiveTestIterator.php', 'RecursiveTestIterator' => 'TUnit/util/RecursiveTestIterator.php',
'RecursivelyCountable' => 'TUnit/framework/test/Testable.php',
'SimpleConstraint' => 'TUnit/framework/constraints/SimpleConstraints.php', 'SimpleConstraint' => 'TUnit/framework/constraints/SimpleConstraints.php',
'SingleTestResult' => 'TUnit/framework/result/SingleTestResult.php', 'SingleTestResult' => 'TUnit/framework/result/SingleTestResult.php',
'TUnitException' => 'TUnit/framework/exceptions/Exceptions.php', 'TUnitException' => 'TUnit/framework/exceptions/Exceptions.php',

View File

@ -108,6 +108,8 @@
$ref = new ReflectionClass($className); $ref = new ReflectionClass($className);
if ($ref->isSubClassOf('TestCase')) { if ($ref->isSubClassOf('TestCase')) {
$tests[] = $ref->newInstance($className); $tests[] = $ref->newInstance($className);
//add to filter
CoverageFilter::addFile($file);
} }
unset($ref); unset($ref);

View File

@ -43,11 +43,14 @@
global $switches; global $switches;
$switches = new CliSwitchCollection(); $switches = new CliSwitchCollection();
$switches->addSwitch(new CliSwitch(null, null, true, '<files>', 'Files and/or directories to parse for test cases')) $switches->addSwitch(new CliSwitch(null, null, true, '<files>', 'Files and/or directories to parse for test cases'))
->addSwitch(new CliSwitch('help', 'h', false, null, 'Display this help message (also --usage)')) ->addSwitch(new CliSwitch('help', 'h', false, null, 'Display this help message (also --usage)'))
->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-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'));
array_shift($argv); array_shift($argv);
$args = Cli::parseArgs($argv, $switches); $args = Cli::parseArgs($argv, $switches);
@ -65,30 +68,21 @@
exit(1); exit(1);
} }
$runner = new ConsoleTestRunner();
$runner->setOptions($options);
//accumulate tests //accumulate tests
$tests = TestAccumulator::getTests($args, isset($options['recursive'])); $tests = TestAccumulator::getTests($args, $runner->getOption('recursive'));
if (empty($tests)) { if (empty($tests)) {
fwrite(STDERR, 'Found no TestCase subclasses in the given files'); fwrite(STDERR, 'Found no TestCase subclasses in the given files');
exit(1); exit(1);
} }
if (isset($options['bootstrap'])) { $runner->addTests($tests)
if (!is_file($options['bootstrap'])) { ->addListener(new ConsoleListener())
fwrite(STDERR, 'Bootstrap file given (' . $options['bootstrap'] . ') is not a file'); ->run();
exit(1);
}
require_once $options['bootstrap'];
}
$runner = new ConsoleTestRunner(
array(new TestSuite('Main Test Suite', $tests)),
array(new ConsoleListener()),
$options
);
$runner->run();
exit(0); exit(0);
?> ?>