* * @license http://opensource.org/licenses/bsd-license.php BSD * * @version $Id$ * */ class Solar_Test extends Solar_Base { /** * Exit code for premature termination from error or exception. */ const EXIT_TERM = 0; /** * Exit code for a failed test. */ const EXIT_FAIL = 101; /** * Exit code for an incomplete test. */ const EXIT_TODO = 102; /** * Exit code for a skipped test. */ const EXIT_SKIP = 103; /** * Exit code for a successful test. */ const EXIT_PASS = 104; /** * * A running count of how many times an assert*() method is called. * * @var int * */ protected $_assert_count = 0; /** * * Variable dumper for debug output. * * @var Solar_Debug_Var * */ protected $_debug; /** * * Post-construction tasks to complete object construction. * * @return void * */ protected function _postConstruct() { parent::_postConstruct(); $this->_debug = Solar::factory('Solar_Debug_Var'); } /** * * Teardown after the entire unit test. * * @return void * */ public function __destruct() { } /** * * Runs before each test method; used for preparing state. * * @return void * */ public function preTest() { } /** * * Runs after each test method; used for restoring state. * * @return void * */ public function postTest() { } /** * * Returns the number of assertions made by this test. * * @return int * */ public function getAssertCount() { return $this->_assert_count; } /** * * Resets the assertion counter to zero. * * @return void * */ public function resetAssertCount() { $this->_assert_count = 0; } /** * * Asserts that a variable is boolean true. * * @param mixed $actual The variable to test. * * @return bool The assertion result. * */ public function assertTrue($actual) { $this->_assert_count ++; if ($actual !== true) { $this->fail( 'Expected true, actually not-true', array( 'actual' => $this->_export($actual), ) ); } else { return true; } } /** * * Asserts that a variable is not boolean true. * * @param mixed $actual The variable to test. * * @return bool The assertion result. * */ public function assertNotTrue($actual) { $this->_assert_count ++; if ($actual === true) { $this->fail( 'Expected not-true, actually true', array( 'actual' => $this->_export($actual), ) ); } else { return true; } } /** * * Asserts that a variable is boolean false. * * @param mixed $actual The variable to test. * * @return bool The assertion result. * */ public function assertFalse($actual) { $this->_assert_count ++; if ($actual !== false) { $this->fail( 'Expected false, actually not-false', array( 'actual' => $this->_export($actual), ) ); } else { return true; } } /** * * Asserts that a variable is not boolean false. * * @param mixed $actual The variable to test. * * @return bool The assertion result. * */ public function assertNotFalse($actual) { $this->_assert_count ++; if ($actual === false) { $this->fail( 'Expected not-false, actually false', array( 'actual' => $this->_export($actual), ) ); } else { return true; } } /** * * Asserts that a variable is PHP null. * * @param mixed $actual The variable to test. * * @return bool The assertion result. * */ public function assertNull($actual) { $this->_assert_count ++; if ($actual !== null) { $this->fail( 'Expected null, actually not-null', array( 'actual' => $this->_export($actual), ) ); } else { return true; } } /** * * Asserts that a variable is not PHP null. * * @param mixed $actual The variable to test. * * @return bool The assertion result. * */ public function assertNotNull($actual) { $this->_assert_count ++; if ($actual === null) { $this->fail( 'Expected not-null, actually null', array( 'actual' => $this->_export($actual), ) ); } else { return true; } } /** * * Asserts that a object is an instance of a class. * * @param object $actual The object to test. * * @param string $expect The expected class name. * * @return bool The assertion result. * */ public function assertInstance($actual, $expect) { $this->_assert_count ++; if (! is_object($actual)) { $this->fail( 'Expected object, actually ' . gettype($actual), array( 'actual' => $this->_export($actual), ) ); } if (! class_exists($expect, false)) { $this->fail( "Expected class '$expect' not loaded for comparison" ); } if (!($actual instanceof $expect)) { $this->fail( "Expected instance of class '$expect', actually '" . get_class($actual) . "'" ); } return true; } /** * * Asserts that a object is not an instance of a class. * * @param object $actual The object to test. * * @param string $expect The non-expected class name. * * @return bool The assertion result. * */ public function assertNotInstance($actual, $expect) { $this->_assert_count ++; if (! is_object($actual)) { $this->fail( "Expected object, actually " . gettype($actual), array( 'actual' => $this->_export($actual), ) ); } if (! class_exists($expect, false)) { $this->fail( "Expected class '$expect' not loaded for comparison" ); } if ($actual instanceof $expect) { $this->fail( "Expected instance not-of class '$expect', actually is" ); } return true; } /** * * Asserts that two variables have the same type and value. * * When used on objects, asserts the two variables are * references to the same object. * * @param mixed $actual The variable to test. * * @param mixed $expect The expected value. * * @return bool The assertion result. * */ public function assertSame($actual, $expect) { $this->_assert_count ++; if (is_array($actual)) { $this->_ksort($actual); } if (is_array($expect)) { $this->_ksort($expect); } if ($actual !== $expect) { $this->fail( 'Expected same, actually not-same', array( 'actual' => $this->_export($actual), 'expect' => $this->_export($expect), ) ); } else { return true; } } /** * * Asserts that two variables are not the same type and value. * * When used on objects, asserts the two variables are not * references to the same object. * * @param mixed $actual The variable to test. * * @param mixed $expect The non-expected result. * * @return bool The assertion result. * */ public function assertNotSame($actual, $expect) { $this->_assert_count ++; if (is_array($actual)) { $this->_ksort($actual); } if (is_array($expect)) { $this->_ksort($expect); } if ($actual === $expect) { $this->fail( 'Expected not-same, actually same', array( 'actual' => $this->_export($actual), 'expect' => $this->_export($expect), ) ); } else { return true; } } /** * * Asserts that two variables are equal; type is not strict. * * @param mixed $actual The variable to test. * * @param mixed $expect The expected value. * * @return bool The assertion result. * */ public function assertEquals($actual, $expect) { $this->_assert_count ++; if (is_array($actual)) { $this->_ksort($actual); } if (is_array($expect)) { $this->_ksort($expect); } if ($actual != $expect) { $this->fail( 'Expected equals, actually not-equals', array( 'actual' => $this->_export($actual), 'expect' => $this->_export($expect), ) ); } else { return true; } } /** * * Asserts that two variables are not equal; type is not strict. * * @param mixed $actual The variable to test. * * @param mixed $expect The expected value. * * @return bool The assertion result. * */ public function assertNotEquals($actual, $expect) { $this->_assert_count ++; if (is_array($actual)) { $this->_ksort($actual); } if (is_array($expect)) { $this->_ksort($expect); } if ($actual == $expect) { $this->fail( 'Expected not-equals, actually equals', array( 'actual' => $this->_export($actual), 'expect' => $this->_export($expect), ) ); } else { return true; } } /** * * Asserts that a value matches a regular expression pattern * using [[php::preg_match() | ]]. * * @param mixed $actual The variable to test. * * @param mixed $expect The regular expression pattern. * * @return bool The assertion result. * */ public function assertRegex($actual, $expect) { $this->_assert_count ++; if (! preg_match($expect, $actual)) { $this->fail( 'Expected pattern match, actually not a match', array( 'actual' => $this->_export($actual), 'expect' => $this->_export($expect), ) ); } else { return true; } } /** * * Asserts that a value does not match a regular expression pattern * using [[php::preg_match() | ]]. * * @param mixed $actual The variable to test. * * @param mixed $expect The regular expression pattern. * * @return bool The assertion result. * */ public function assertNotRegex($actual, $expect) { $this->_assert_count ++; if (preg_match($expect, $actual)) { $this->fail( 'Expected no pattern match, actually matches', array( 'actual' => $this->_export($actual), 'expect' => $this->_export($expect), ) ); } else { return true; } } /** * * Asserts that an object property meets criteria. * * The object property may be public, protected, or private. * * @param object $object The object to test. * * @param string $property The property to inspect. * * @param string $test The Solar_Test_Assert method to call. * * @param mixed $expect The expected result from the test method. * * @return bool The assertion result. * */ public function assertProperty($object, $property, $test, $expect = null) { $this->_assert_count ++; if (! is_object($object)) { $this->fail("Expected object, actually " . gettype($object)); } // introspect the object and look for the property $class = get_class($object); $found = false; $reflect = new ReflectionObject($object); foreach ($reflect->getProperties() as $prop) { // $val is a ReflectionProperty object $name = $prop->getName(); if ($name != $property) { // skip it, not the one we're looking for continue; } // found the requested property $found = true; $copy = (array) $object; // get the actual value. the null-char // trick for accessing protected and private // properties comes from Mike Naberezny. if ($prop->isPublic()) { $actual = $copy[$name]; } elseif ($prop->isProtected()) { $actual = $copy["\0*\0$name"]; } else { $actual = $copy["\0$class\0$name"]; } // done break; } // did we find $object->$property? if (! $found) { $this->fail( "Did not find expected property '$property' " . "in object of class '$class'" ); } // test the property value $method = 'assert' . ucfirst($test); return $this->$method($actual, $expect); } /** * * Prints diagnostic output. * * @param mixed $spec The diagnostic output. If a string, prints line-by- * line; otherwise, prints a var_export() of the value line-by-line. * * @param string $label The label for the diagnostic output, if any. * * @return void * */ public function diag($spec, $label = null) { // print the label if any if ($label) { $this->diag($label); } // print the diagnostic output if ($spec instanceof Exception) { $text = $spec->__toString(); $this->diag($text); } elseif (is_string($spec)) { $lines = explode(PHP_EOL, $spec); foreach ($lines as $line) { echo "# " . $line . PHP_EOL; } } else { $dump = var_export($spec, true); $this->diag($dump); } } /** * * Throws an exception indicating a failed test. * * @param string $text The failed-test message. * * @param array $info Additional info for the exception. * * @return void * */ public function fail($text = null, $info = null) { $info['exit'] = Solar_Test::EXIT_FAIL; throw Solar::factory('Solar_Test_Exception_Fail', array( 'class' => get_class($this), 'code' => 'ERR_FAIL', 'text' => ($text ? $text : $this->locale('ERR_FAIL')), 'info' => $info, )); } /** * * Throws an exception indicating an incomplete test. * * @param string $text The incomplete-test message. * * @param array $info Additional info for the exception. * * @return void * */ public function todo($text = null, $info = null) { $info['exit'] = Solar_Test::EXIT_TODO; throw Solar::factory('Solar_Test_Exception_Todo', array( 'class' => get_class($this), 'code' => 'ERR_TODO', 'text' => ($text ? $text : $this->locale('ERR_TODO')), 'info' => $info, )); } /** * * Throws an exception indicating a skipped test. * * @param string $text The skipped-test message. * * @param array $info Additional info for the exception. * * @return void * */ public function skip($text = null, $info = null) { $info['exit'] = Solar_Test::EXIT_SKIP; throw Solar::factory('Solar_Test_Exception_Skip', array( 'class' => get_class($this), 'code' => 'ERR_SKIP', 'text' => ($text ? $text : $this->locale('ERR_SKIP')), 'info' => $info, )); } /** * * Error handler for this test; throws a test failure. * * @param int $code The PHP error level code. * * @param string $text The PHP error string. * * @param string $file The file where the error occurred. * * @param int $line The line number in that file. * * @return void * */ public function error($code, $text, $file, $line) { // if using @ to suppress error reporting, don't fail. if (ini_get('error_reporting') == 0) { return; } // figure out the error level $type = array( E_STRICT => 'PHP Strict', E_WARNING => 'PHP Warning', E_NOTICE => 'PHP Notice', E_USER_ERROR => 'PHP User Error', E_USER_WARNING => 'PHP User Warning', E_USER_NOTICE => 'PHP User Notice', ); if (empty($type[$code])) { $level = 'Unknown PHP Error'; } else { $level = $type[$code]; } // throw a failure $this->fail("$level: $text", array( 'file' => $file, 'line' => $line, )); } /** * * Returns the output from Solar_Debug_Var::fetch() for a variable. * * @param mixed $var The variable dump. * * @return string * */ protected function _export($var) { return trim($this->_debug->fetch($var)); } /** * * Recursively [[php::ksort() | ]] an array. * * Used so that order of array elements does not affect equality. * * @param array $array The array to sort. * * @return void * */ protected function _ksort(&$array) { ksort($array); foreach($array as $key => $val) { if (is_array($val)) { $this->_ksort($array[$key]); } } } }