<?php /** * Base include file for SimpleTest * @package SimpleTest * @subpackage UnitTester * @version $Id$ */ /**#@+ * Includes SimpleTest files and defined the root constant * for dependent libraries. */ require_once(dirname(__FILE__) . '/invoker.php'); require_once(dirname(__FILE__) . '/errors.php'); require_once(dirname(__FILE__) . '/compatibility.php'); require_once(dirname(__FILE__) . '/scorer.php'); require_once(dirname(__FILE__) . '/expectation.php'); require_once(dirname(__FILE__) . '/dumper.php'); require_once(dirname(__FILE__) . '/simpletest.php'); if (version_compare(phpversion(), '5') >= 0) { require_once(dirname(__FILE__) . '/exceptions.php'); require_once(dirname(__FILE__) . '/reflection_php5.php'); } else { require_once(dirname(__FILE__) . '/reflection_php4.php'); } if (! defined('SIMPLE_TEST')) { /** * @ignore */ define('SIMPLE_TEST', dirname(__FILE__) . '/'); } /**#@-*/ /** * Basic test case. This is the smallest unit of a test * suite. It searches for * all methods that start with the the string "test" and * runs them. Working test cases extend this class. * @package SimpleTest * @subpackage UnitTester */ class SimpleTestCase { protected $_label = false; protected $_reporter; protected $_observers; /** * Sets up the test with no display. * @param string $label If no test name is given then * the class name is used. * @access public */ function SimpleTestCase($label = false) { if ($label) { $this->_label = $label; } } /** * Accessor for the test name for subclasses. * @return string Name of the test. * @access public */ function getLabel() { return $this->_label ? $this->_label : get_class($this); } /** * Used to invoke the single tests. * @return SimpleInvoker Individual test runner. * @access public */ function createInvoker() { $invoker = new SimpleErrorTrappingInvoker(new SimpleInvoker($this)); if (version_compare(phpversion(), '5') >= 0) { $invoker = new SimpleExceptionTrappingInvoker($invoker); } return $invoker; } /** * Uses reflection to run every method within itself * starting with the string "test" unless a method * is specified. * @param SimpleReporter $reporter Current test reporter. * @access public */ function run($reporter) { SimpleTest::setCurrent($this); $this->_reporter = $reporter; $this->_reporter->paintCaseStart($this->getLabel()); foreach ($this->getTests() as $method) { if ($this->_reporter->shouldInvoke($this->getLabel(), $method)) { $invoker = $this->_reporter->createInvoker($this->createInvoker()); $invoker->before($method); $invoker->invoke($method); $invoker->after($method); } } $this->_reporter->paintCaseEnd($this->getLabel()); unset($this->_reporter); return $reporter->getStatus(); } /** * Gets a list of test names. Normally that will * be all internal methods that start with the * name "test". This method should be overridden * if you want a different rule. * @return array List of test names. * @access public */ function getTests() { $methods = array(); foreach (get_class_methods(get_class($this)) as $method) { if ($this->_isTest($method)) { $methods[] = $method; } } return $methods; } /** * Tests to see if the method is a test that should * be run. Currently any method that starts with 'test' * is a candidate unless it is the constructor. * @param string $method Method name to try. * @return boolean True if test method. * @access protected */ function _isTest($method) { if (strtolower(substr($method, 0, 4)) == 'test') { return ! SimpleTestCompatibility::isA($this, strtolower($method)); } return false; } /** * Announces the start of the test. * @param string $method Test method just started. * @access public */ function before($method) { $this->_reporter->paintMethodStart($method); $this->_observers = array(); } /** * Sets up unit test wide variables at the start * of each test method. To be overridden in * actual user test cases. * @access public */ function setUp() { } /** * Clears the data set in the setUp() method call. * To be overridden by the user in actual user test cases. * @access public */ function tearDown() { } /** * Announces the end of the test. Includes private clean up. * @param string $method Test method just finished. * @access public */ function after($method) { for ($i = 0; $i < count($this->_observers); $i++) { $this->_observers[$i]->atTestEnd($method); } $this->_reporter->paintMethodEnd($method); } /** * Sets up an observer for the test end. * @param object $observer Must have atTestEnd() * method. * @access public */ function tell($observer) { $this->_observers[] = $observer; } /** * Sends a pass event with a message. * @param string $message Message to send. * @access public */ function pass($message = "Pass") { if (! isset($this->_reporter)) { trigger_error('Can only make assertions within test methods'); } $this->_reporter->paintPass( $message . $this->getAssertionLine()); return true; } /** * Sends a fail event with a message. * @param string $message Message to send. * @access public */ function fail($message = "Fail") { if (! isset($this->_reporter)) { trigger_error('Can only make assertions within test methods'); } $this->_reporter->paintFail( $message . $this->getAssertionLine()); return false; } /** * Formats a PHP error and dispatches it to the * reporter. * @param integer $severity PHP error code. * @param string $message Text of error. * @param string $file File error occoured in. * @param integer $line Line number of error. * @access public */ function error($severity, $message, $file, $line) { if (! isset($this->_reporter)) { trigger_error('Can only make assertions within test methods'); } $this->_reporter->paintError( "Unexpected PHP error [$message] severity [$severity] in [$file] line [$line]"); } /** * Formats an exception and dispatches it to the * reporter. * @param Exception $exception Object thrown. * @access public */ function exception($exception) { $this->_reporter->paintError( 'Unexpected exception of type [' . get_class($exception) . '] with message ['. $exception->getMessage() . '] in ['. $exception->getFile() . '] line [' . $exception->getLine() . '] stack [' . $exception->getTraceAsString() .']'); } /** * Sends a user defined event to the test reporter. * This is for small scale extension where * both the test case and either the reporter or * display are subclassed. * @param string $type Type of event. * @param mixed $payload Object or message to deliver. * @access public */ function signal($type, $payload) { if (! isset($this->_reporter)) { trigger_error('Can only make assertions within test methods'); } $this->_reporter->paintSignal($type, $payload); } /** * Cancels any outstanding errors. * @access public */ function swallowErrors() { $queue = &SimpleErrorQueue::instance(); $queue->clear(); } /** * Runs an expectation directly, for extending the * tests with new expectation classes. * @param SimpleExpectation $expectation Expectation subclass. * @param mixed $compare Value to compare. * @param string $message Message to display. * @return boolean True on pass * @access public */ function assert($expectation, $compare, $message = '%s') { return $this->assertTrue( $expectation->test($compare), sprintf($message, $expectation->overlayMessage($compare))); } /** * @deprecated */ function assertExpectation($expectation, $compare, $message = '%s') { return $this->assert($expectation, $compare, $message); } /** * Called from within the test methods to register * passes and failures. * @param boolean $result Pass on true. * @param string $message Message to display describing * the test state. * @return boolean True on pass * @access public */ function assertTrue($result, $message = false) { if (! $message) { $message = 'True assertion got ' . ($result ? 'True' : 'False'); } if ($result) { return $this->pass($message); } else { return $this->fail($message); } } /** * Will be true on false and vice versa. False * is the PHP definition of false, so that null, * empty strings, zero and an empty array all count * as false. * @param boolean $result Pass on false. * @param string $message Message to display. * @return boolean True on pass * @access public */ function assertFalse($result, $message = false) { if (! $message) { $message = 'False assertion got ' . ($result ? 'True' : 'False'); } return $this->assertTrue(! $result, $message); } /** * Uses a stack trace to find the line of an assertion. * @param string $format String formatting. * @param array $stack Stack frames top most first. Only * needed if not using the PHP * backtrace function. * @return string Line number of first assert* * method embedded in format string. * @access public */ function getAssertionLine($stack = false) { if ($stack === false) { $stack = SimpleTestCompatibility::getStackTrace(); } return SimpleDumper::getFormattedAssertionLine($stack); } /** * Sends a formatted dump of a variable to the * test suite for those emergency debugging * situations. * @param mixed $variable Variable to display. * @param string $message Message to display. * @return mixed The original variable. * @access public */ function dump($variable, $message = false) { $formatted = SimpleDumper::dump($variable); if ($message) { $formatted = $message . "\n" . $formatted; } $this->_reporter->paintFormattedMessage($formatted); return $variable; } /** * Dispatches a text message straight to the * test suite. Useful for status bar displays. * @param string $message Message to show. * @access public */ function sendMessage($message) { $this->_reporter->PaintMessage($message); } /** * Accessor for the number of subtests. * @return integer Number of test cases. * @access public * @static */ static function getSize() { return 1; } } /** * This is a composite test class for combining * test cases and other RunnableTest classes into * a group test. * @package SimpleTest * @subpackage UnitTester */ class GroupTest { protected $_label; protected $_test_cases; protected $_old_track_errors; protected $_xdebug_is_enabled; /** * Sets the name of the test suite. * @param string $label Name sent at the start and end * of the test. * @access public */ function GroupTest($label = false) { $this->_label = $label ? $label : get_class($this); $this->_test_cases = array(); $this->_old_track_errors = ini_get('track_errors'); $this->_xdebug_is_enabled = function_exists('xdebug_is_enabled') ? xdebug_is_enabled() : false; } /** * Accessor for the test name for subclasses. * @return string Name of the test. * @access public */ function getLabel() { return $this->_label; } function setLabel($value) { $this->_label = $value; } /** * Adds a test into the suite. Can be either a group * test or some other unit test. * @param SimpleTestCase $test_case Suite or individual test * case implementing the * runnable test interface. * @access public */ function addTestCase($test_case) { $this->_test_cases[] = $test_case; } /** * Adds a test into the suite by class name. The class will * be instantiated as needed. * @param SimpleTestCase $test_case Suite or individual test * case implementing the * runnable test interface. * @access public */ function addTestClass($class) { if ($this->_getBaseTestCase($class) == 'grouptest') { $this->_test_cases[] = new $class(); } else { $this->_test_cases[] = $class; } } /** * Builds a group test from a library of test cases. * The new group is composed into this one. * @param string $test_file File name of library with * test case classes. * @access public */ function addTestFile($test_file) { $existing_classes = get_declared_classes(); if ($error = $this->_requireWithError($test_file)) { $this->addTestCase(new BadGroupTest($test_file, $error)); return; } $classes = $this->_selectRunnableTests($existing_classes, get_declared_classes()); if (count($classes) == 0) { $this->addTestCase(new BadGroupTest($test_file, "No runnable test cases in [$test_file]")); return; } $group = $this->_createGroupFromClasses($test_file, $classes); $this->addTestCase($group); } /** * Requires a source file recording any syntax errors. * @param string $file File name to require in. * @return string/boolean An error message on failure or false * if no errors. * @access private */ function _requireWithError($file) { $this->_enableErrorReporting(); include_once($file); $error = isset($php_errormsg) ? $php_errormsg : false; $this->_disableErrorReporting(); $self_inflicted_errors = array( 'Assigning the return value of new by reference is deprecated', 'var: Deprecated. Please use the public/private/protected modifiers'); if (in_array($error, $self_inflicted_errors)) { return false; } return $error; } /** * Sets up detection of parse errors. Note that XDebug * interferes with this and has to be disabled. This is * to make sure the correct error code is returned * from unattended scripts. * @access private */ function _enableErrorReporting() { if ($this->_xdebug_is_enabled) { xdebug_disable(); } ini_set('track_errors', true); } /** * Resets detection of parse errors to their old values. * This is to make sure the correct error code is returned * from unattended scripts. * @access private */ function _disableErrorReporting() { ini_set('track_errors', $this->_old_track_errors); if ($this->_xdebug_is_enabled) { xdebug_enable(); } } /** * Calculates the incoming test cases from a before * and after list of loaded classes. Skips abstract * classes. * @param array $existing_classes Classes before require(). * @param array $new_classes Classes after require(). * @return array New classes which are test * cases that shouldn't be ignored. * @access private */ function _selectRunnableTests($existing_classes, $new_classes) { $classes = array(); foreach ($new_classes as $class) { if (in_array($class, $existing_classes)) { continue; } if ($this->_getBaseTestCase($class)) { $reflection = new SimpleReflection($class); if ($reflection->isAbstract()) { SimpleTest::ignore($class); } $classes[] = $class; } } return $classes; } /** * Builds a group test from a class list. * @param string $title Title of new group. * @param array $classes Test classes. * @return GroupTest Group loaded with the new * test cases. * @access private */ function &_createGroupFromClasses($title, $classes) { SimpleTest::ignoreParentsIfIgnored($classes); $group = new GroupTest($title); foreach ($classes as $class) { if (! SimpleTest::isIgnored($class)) { $group->addTestClass($class); } } return $group; } /** * Test to see if a class is derived from the * SimpleTestCase class. * @param string $class Class name. * @access private */ function _getBaseTestCase($class) { while ($class = get_parent_class($class)) { $class = strtolower($class); if ($class == "simpletestcase" || $class == "grouptest") { return $class; } } return false; } /** * Delegates to a visiting collector to add test * files. * @param string $path Path to scan from. * @param SimpleCollector $collector Directory scanner. * @access public */ function collect($path, $collector) { $collector->collect($this, $path); } /** * Invokes run() on all of the held test cases, instantiating * them if necessary. * @param SimpleReporter $reporter Current test reporter. * @access public */ function run($reporter) { $reporter->paintGroupStart($this->getLabel(), $this->getSize()); for ($i = 0, $count = count($this->_test_cases); $i < $count; $i++) { if (is_string($this->_test_cases[$i])) { $class = $this->_test_cases[$i]; $test = new $class(); $test->run($reporter); } else { $this->_test_cases[$i]->run($reporter); } } $reporter->paintGroupEnd($this->getLabel()); return $reporter->getStatus(); } /** * Number of contained test cases. * @return integer Total count of cases in the group. * @access public */ function getSize() { $count = 0; foreach ($this->_test_cases as $case) { if (is_string($case)) { $count++; } else { $count += $case->getSize(); } } return $count; } } /** * This is a failing group test for when a test suite hasn't * loaded properly. * @package SimpleTest * @subpackage UnitTester */ class BadGroupTest { protected $_label; protected $_error; /** * Sets the name of the test suite and error message. * @param string $label Name sent at the start and end * of the test. * @access public */ function BadGroupTest($label, $error) { $this->_label = $label; $this->_error = $error; } /** * Accessor for the test name for subclasses. * @return string Name of the test. * @access public */ function getLabel() { return $this->_label; } /** * Sends a single error to the reporter. * @param SimpleReporter $reporter Current test reporter. * @access public */ function run($reporter) { $reporter->paintGroupStart($this->getLabel(), $this->getSize()); $reporter->paintFail('Bad GroupTest [' . $this->getLabel() . '] with error [' . $this->_error . ']'); $reporter->paintGroupEnd($this->getLabel()); return $reporter->getStatus(); } /** * Number of contained test cases. Always zero. * @return integer Total count of cases in the group. * @access public */ function getSize() { return 0; } } ?>