className = $class->getName(); $this->classFile = new ogoFile($class->getFilename()); $targetName = substr($this->className, 0, - strlen(self::testClassSuffix)); if ($this->phpAdapter->class_exists($targetName, true) === false) { trigger_error('Unable to find class \'' . $targetName . '\'', E_USER_ERROR); } else { $this->targetName = $targetName; $targetClass = new reflectionClass($this->targetName); $this->targetFile = new ogoFile($targetClass->getFilename()); foreach ($class->getMethods() as $method) { $methodName = $method->getName(); if (substr($methodName, 0, strlen(self::testMethodPrefix)) === self::testMethodPrefix) { if ($method->isProtected() === true) { $this->methods[] = $methodName; } else { trigger_error('Test method \'' . $this->className . '::' . $methodName . '() must be protected', E_USER_ERROR); } } } if ($score === null) { $score = new ogoUnitTestScore($this); } $this->setUnitTestScore($score); $this->testAsserter = new ogoTestAsserter($this->phpAdapter); } } public function __get($property) { if ($property === 'assert') { return $this->testAsserter; } else { trigger_error('Property\'' . $property . '\' is unknown in \'' . $this->className . '\'', E_USER_ERROR); } } public function getClassName() { return $this->className; } public function getClassFile() { return $this->classFile; } public function getTargetName() { return $this->targetName; } public function getTargetFile() { return $this->targetFile; } public function getTargetClassInstance() { $arguments = func_get_args(); $targetClass = new reflectionClass($this->targetName); if ($targetClass->isInstantiable() === false) { trigger_error('Unable to instanciate target class \'' . $this->targetName . '\'', E_USER_ERROR); } else { return $targetClass->newInstanceArgs($arguments); } } public function getUnitTestScore() { return $this->score; } public function setUnitTestScore(ogoUnitTestScore $score) { $this->score = $score->setUnitTest($this); return $this; } protected function getCurrentMethod() { return $this->currentMethod; } protected function assert($mixed) { return $this->testAsserter->set($mixed); } protected function beforeRun() {} public function run(array $methods = null) { $this->score->reset(); if ($methods === null) { $methods = $this->methods; } else { $undefinedMethods = array_diff($methods, $this->methods); if (sizeof($undefinedMethods) > 0) { $methods = array(); trigger_error('Test methods \'' . join(', ', $undefinedMethods) . '\' are undefined in test class \'' . $this->className . '\'', E_USER_ERROR); } } if (sizeof($methods) > 0) { $this->parseIniFile(); $this->beforeRun(); foreach ($methods as $this->currentMethod) { if ($this->fork() == true) { return true; } } $this->currentMethod = null; $this->afterRun(); } return false; } protected function afterRun() {} protected function beforeMethod() {} protected function afterMethod() {} public function count() { return sizeof($this->methods); } public function errorHandler($errno, $errstr, $file, $line, $context) { if (error_reporting() !== 0) { $backtraces = array(); foreach (array_reverse(debug_backtrace()) as $level => $backtrace) { if (isset($backtrace['class']) === true && $backtrace['class'] === __CLASS__ && isset($backtrace['function']) === true && $backtrace['function'] === 'errorHandler') { break; } else { if (isset($backtrace['file']) === false) { $backtrace['file'] = 'unknown'; } if (isset($backtrace['line']) === false) { $backtrace['line'] = 'unknown'; } if (isset($backtrace['function']) == false) { $backtrace['function'] = 'unknown'; } else if (isset($backtrace['class']) === true) { $backtrace['function'] = $backtrace['class'] . '::' . $backtrace['function']; } $backtrace['function'] .= '()'; $backtraces[] = '[' . ($level + 1) . '] ' . $backtrace['file'] . '(' . $backtrace['line'] . '): ' . $backtrace['function']; } } $backtraces = array_reverse($backtraces); if (self::isUserError($errno) === false || $this->isTargetError(array_slice(debug_backtrace(), 1)) === false) { $this->score->addExternalError($this->currentMethod, trim($errstr) . ' in file ' . $file . ' on line ' . $line . "\n" . join("\n", $backtraces)); } else { $error = array( 'class' => $this->className, 'method' => $this->currentMethod, 'code' => $errno, 'file' => $file, 'line' => $line, 'message' => $errstr, 'backtraces' => $backtraces ); $this->score->addClassError($error); $this->errors[] = $error; } } return true; } protected function skipIfMethodFailed($method, $message = null) { if (in_array($method, $this->methods) === false) { trigger_error('Test method \'' . $method . '\' does not exist in test class \'' . $this->className . '\'', E_USER_ERROR); } else { foreach ($this->score->getFails() as $fail) { if ($fail['method'] === $method) { if ($message === null) { $backtrace = $this->getBacktrace(); $message = '\'' . $this->className . '::' . $backtrace['method'] . '()\' skiped because method \'' . $this->className . '::' . $method . '()\' has failed'; } throw new ogoUnitTestSkipMethodException($message); } } } } protected function depedency($boolean, $message) { if ($boolean == false) { throw new ogoUnitTestDepedencyException($message); } } protected function pass() { $this->score->addPass($this->getBacktrace()); return true; } protected function fail($reason) { $fail = $this->getBacktrace(); $fail['reason'] = $reason; $this->score->addFail($fail); return false; } protected function true($var) { return ($var == true ? $this->pass() : $this->fail(ogoDebug::dumpAsString($var) . ' is not true')); } protected function false($var) { return ($var == false ? $this->pass() : $this->fail(ogoDebug::dumpAsString($var) . ' is not false')); } protected function isArray($var) { return (is_array($var) == true ? $this->pass() : $this->fail(ogoDebug::dumpAsString($var) . ' is not an array')); } protected function null($var) { return ($var === null ? $this->pass() : $this->fail(ogoDebug::dumpAsString($var) . ' is not null')); } protected function notNull($var) { return ($var !== null ? $this->pass() : $this->fail(ogoDebug::dumpAsString($var) . ' is null')); } protected function instance($var, $type) { return ($var instanceof $type ? $this->pass() : $this->fail(ogoDebug::dumpAsString($var) . ' is not an instance of type \'' . $type . '\'')); } protected function notInstance($var, $type) { return ($var instanceof $type === false ? $this->pass() : $this->fail(ogoDebug::dumpAsString($var) . ' is an instance of type \'' . $type . '\'')); } protected function equal($var1, $var2) { return ($var1 == $var2 ? $this->pass() : $this->fail(ogoDebug::dumpAsString($var1) . ' is not equal to ' . ogoDebug::dumpAsString($var2))); } protected function notEqual($var1, $var2) { return ($var1 != $var2 ? $this->pass() : $this->fail(ogoDebug::dumpAsString($var1) . ' is equal to ' . ogoDebug::dumpAsString($var2))); } protected function greaterThan($var1, $var2) { return ($var1 > $var2 ? $this->pass() : $this->fail(ogoDebug::dumpAsString($var1) . ' is not greater than ' . ogoDebug::dumpAsString($var2))); } protected function greaterThanOrEqualTo($var1, $var2) { return ($var1 >= $var2 ? $this->pass() : $this->fail(ogoDebug::dumpAsString($var1) . ' is not greater than or equal to ' . ogoDebug::dumpAsString($var2))); } protected function lessThan($var1, $var2) { return ($var1 < $var2 ? $this->pass() : $this->fail(ogoDebug::dumpAsString($var1) . ' is not less than ' . ogoDebug::dumpAsString($var2))); } protected function lessThanOrEqualTo($var1, $var2) { return ($var1 <= $var2 ? $this->pass() : $this->fail(ogoDebug::dumpAsString($var1) . ' is not less than or equal to ' . ogoDebug::dumpAsString($var2))); } protected function sizeof($var, $sizeof) { $sizeofVar = sizeof($var); return ($sizeofVar === $sizeof ? $this->pass() : $this->fail('size ' . $sizeofVar . ' is not equal to ' . $sizeof)); } protected function notSizeof($var, $sizeof) { $sizeofVar = sizeof($var); return ($sizeofVar !== $sizeof ? $this->pass() : $this->fail('size ' . $sizeofVar . ' is equal to ' . $sizeof)); } protected function identical($var1, $var2) { return ($var1 === $var2 ? $this->pass() : $this->fail(ogoDebug::dumpAsString($var1) . ' is not identical to ' . ogoDebug::dumpAsString($var2))); } protected function notIdentical($var1, $var2) { return ($var1 !== $var2 ? $this->pass() : $this->fail(ogoDebug::dumpAsString($var1) . ' is identical to ' . ogoDebug::dumpAsString($var2))); } protected function reference(& $var1, & $var2) { if (is_object($var1) === true && is_object($var2) === true) { return ($var1 === $var2 ? $this->pass() : $this->fail(ogoDebug::dumpAsString($var1) . ' is not a reference to ' . ogoDebug::dumpAsString($var2))); } else { $var1Backup = $var1; $var1 = uniqid(mt_rand()); $isReference = ($var1 === $var2); $var1 = $var1Backup; return ($isReference === true ? $this->pass() : $this->fail(ogoDebug::dumpAsString($var1) . ' is not a reference to ' . ogoDebug::dumpAsString($var2))); } } protected function notReference(& $var1, & $var2) { if (is_object($var1) == true && is_object($var2) == true) { return ($var1 !== $var2 ? $this->pass() : $this->fail(ogoDebug::dumpAsString($var1) . ' is a reference to ' . ogoDebug::dumpAsString($var2))); } else { $var1Backup = $var1; $var1 = uniqid(mt_rand()); $isNotReference = ($var1 !== $var2); $var1 = $var1Backup; return ($isNotReference === true ? $this->pass() : $this->fail(ogoDebug::dumpAsString($var1) . ' is a reference to ' . ogoDebug::dumpAsString($var2))); } } protected function pattern($var, $pattern, $failMessage = null) { if ($failMessage === null) { $failMessage = 'Pattern ' . $pattern . ' does not match ' . ogoDebug::dumpAsString($var); } return (preg_match($pattern, $var) == 1 ? $this->pass() : $this->fail($failMessage)); } protected function notPattern($var, $pattern) { return (preg_match($pattern, $var) == 0 ? $this->pass() : $this->fail('Pattern ' . $pattern . ' matches ' . ogoDebug::dumpAsString($var))); } protected function error($errorCode = null, $errorMessage = null, $errorFile = null, $errorLine = null) { $assert = $this->errorExists($errorCode, $errorMessage, $errorFile, $errorLine, $key); if ($key !== null) { unset($this->errors[$key]); } return ($assert === true ? $this->pass() : $this->fail('error does not exist')); } protected function noError($errorCode = null, $errorMessage = null, $errorFile = null, $errorLine = null) { return ($this->errorExists($errorCode, $errorMessage, $errorFile, $errorLine) === false ? $this->pass() : $this->fail('error exists')); } protected function defined($constant) { return (defined($constant) === true ? $this->pass() : $this->fail('Constant ' . $constant . ' is undefined')); } protected function resource($resource) { return (is_resource($resource) === true ? $this->pass() : $this->fail(ogoDebug::dumpAsString($resource) . ' is not a resource')); } protected function inArray($value, array $array) { return (in_array($value, $array) === true ? $this->pass() : $this->fail(ogoDebug::dumpAsString($value) . ' is not in ' . ogoDebug::dumpAsString($array))); } protected function definedWithValue($constant, $value) { return (defined($constant) === true && constant($constant) == $value ? $this->pass() : $this->fail('Constant ' . $constant . ' has not value ' . ogoDebug::dumpAsString($value))); } protected function getBacktrace() { $backtrace = array(); $debugBacktraces = $this->phpAdapter->debug_backtrace(); foreach ($debugBacktraces as $key => $backtrace) { if (isset($backtrace['object']) === true && isset($backtrace['function']) === true && $backtrace['object'] === $this && substr($backtrace['function'], 0, 4) === 'test') { $backtrace['assertion'] = $debugBacktraces[$key - 1]['function']; $backtrace['file'] = $debugBacktraces[$key - 1]['file']; $backtrace['line'] = $debugBacktraces[$key - 1]['line']; $backtrace['method'] = $debugBacktraces[$key]['function']; $backtrace['class'] = $debugBacktraces[$key]['class']; break; } } return $backtrace; } protected function saveScore() { $score = serialize($this->score); $shm = $this->phpAdapter->shmop_open($this->phpAdapter->ftok($this->getClassFile()->getRealPath(), 'a'), 'c', 0700, strlen($score)); if ($shm !== false) { $this->phpAdapter->shmop_write($shm, $score, 0); $this->phpAdapter->shmop_close($shm); } return $this; } protected function loadScore() { $shm = @$this->phpAdapter->shmop_open($this->phpAdapter->ftok($this->getClassFile()->getRealPath(), 'a'), 'w', 0, 0); if ($shm !== false) { $shmData = $this->phpAdapter->shmop_read($shm, 0, $this->phpAdapter->shmop_size($shm)); $this->phpAdapter->shmop_delete($shm); $this->phpAdapter->shmop_close($shm); $score = unserialize($shmData); if ($score instanceof ogoUnitTestScore === false) { trigger_error('Shared memory segment does not contain a ogoUnitTestScore instance: ' . $shmData, E_USER_ERROR); } else { $this->score = $score; } } return $this; } protected static function isUserError($errno) { switch ($errno) { case E_USER_ERROR: case E_USER_WARNING: case E_USER_NOTICE: return true; default: return false; } } private function errorExists($errorCode = null, $errorMessage = null, $errorFile = null, $errorLine = null, & $key = null) { $key = null; $code = $errorCode === null; $message = $errorMessage === null; $file = $errorFile === null; $line = $errorLine === null; if ($code === true && $message === true && $file === true && $line === true) { if (sizeof($this->errors) <= 0) { return false; } else { $key = key($this->errors); return true; } } else { foreach ($this->errors as $key => $error) { if ($errorCode !== null) { $code = $error['code'] == $errorCode; } if ($errorMessage !== null) { $message = $error['message'] == $errorMessage; } if ($errorFile !== null) { $file = $error['file'] == $errorFile; } if ($errorLine !== null) { $line = $error['line'] == $errorLine; } if ($code === true && $message === true && $file === true && $line === true) { return true; } } $key = null; return false; } } private function parseIniFile() { $this->iniFile = new ogoIniFile($this->classFile->getDirectory() . '/' . $this->className . '.' . self::iniFileExtension); if ($this->iniFile->exists() === true) { $this->iniFile->parse(); } } private function fork() { $pid = $this->phpAdapter->pcntl_fork(); if ($pid < 0) { trigger_error('Unable to fork to execute test method \'' . $this->currentMethod . '\''); } else if ($pid > 0) { return $this->readTestMethodScore($pid); } else { return $this->executeTestMethod(); } } private function readTestMethodScore($pid) { $status = null; $this->phpAdapter->invoke('pcntl_wait', array(& $status)); $this->loadScore(); if ($this->phpAdapter->pcntl_wexitstatus($status) != 0) { $message = ''; $errorLogFile = $this->getErrorLogFile(); if ($errorLogFile->isReadable() === true) { $errorLogFile->read($message)->delete(); } $this->score->addExternalError($this->currentMethod, trim($message)); } return false; } private function executeTestMethod() { ogoTestAsserter::$unitTestScore = $this->score; $errorLogFile = $this->getErrorLogFile(); if ($errorLogFile->exists() === true) { $errorLogFile->delete(); } $this->phpAdapter->ini_set('error_log', $errorLogFile->getPath()); $this->phpAdapter->ini_set('display_errors', '0'); $this->beforeMethod(); set_error_handler(array($this, 'errorHandler')); $classCodeCoverage = $this->score->getClassCodeCoverage($this->targetName); if ($classCodeCoverage !== null) { $this->phpAdapter->xdebug_start_code_coverage(); } $memoryUsage = $this->phpAdapter->memory_get_usage(); $startTime = $this->phpAdapter->microtime(true); try { $this->{$this->currentMethod}(); } catch (ogoUnitTestDepedencyException $exception) { $this->score->addFailedDepedencies($exception->getMessage()); } catch (ogoUnitTestSkipMethodException $exception) { $this->score->addSkipedMethod($exception->getMessage()); } catch (exception $exception) { $this->score->addException($this->currentMethod, $exception); } $this->score->addDuration($this->currentMethod, $this->phpAdapter->microtime(true) - $startTime); $this->score->addMemoryUsage($this->currentMethod, $this->phpAdapter->memory_get_usage() - $memoryUsage); $this->score->addMemoryPeak($this->currentMethod, $this->phpAdapter->memory_get_peak_usage()); restore_error_handler(); if ($classCodeCoverage !== null) { $codeCoverages = $this->phpAdapter->xdebug_get_code_coverage(); $this->phpAdapter->xdebug_stop_code_coverage(); while ($classCodeCoverage !== null) { $classPath = $classCodeCoverage->getClassPath(); if (isset($codeCoverages[$classPath]) === true) { foreach ($codeCoverages[$classPath] as $line => $call) { $classCodeCoverage->addCodeCoverage($line, $call); } } $classCodeCoverage = $classCodeCoverage->getParentClassCodeCoverage(); } } $this->afterMethod(); $this->saveScore(); return true; } private function isTargetError(array $debugBacktraces) { $targetName = $this->getTargetName(); foreach ($debugBacktraces as $key => $debugBacktrace) { if (isset($debugBacktrace['object']) === true && $debugBacktrace['object'] === $this) { foreach (array_reverse(array_slice($debugBacktraces, 0, $key - 1)) as $debugBacktrace) { if (isset($debugBacktrace['class']) === true && ($debugBacktrace['class'] === $targetName || is_subclass_of($targetName, $debugBacktrace['class']) === true)) { return true; } } } } return false; } private function getErrorLogFile() { $temporaryDirectory = new ogoTemporaryDirectory(); return new ogoFile($temporaryDirectory . DIRECTORY_SEPARATOR . $this->className . '.' . $this->currentMethod . '.log'); } } class ogoUnitTestException extends exception {} class ogoUnitTestSkipMethodException extends ogoUnitTestException {} class ogoUnitTestDepedencyException extends ogoUnitTestException {} ?>