timer = new Benchmark_Timer(); $this->hasTimer = true; } else { $this->hasTimer = false; } $this->suiteResults = array( 'suites' => array(), // array of suites run 'hasTimer' => $this->hasTimer, // availability of PEAR Benchmark_Timer 'totalTests' => 0 // total number of tests run ); $this->currentSession = array( 'errors' => 0, // number of tests with errors (including setup errors) 'failures' => 0, // number of tests which failed 'incomplete' => 0, // number of tests that were not completed correctly 'error' => array(), // Any error encountered outside of suites ); } /** * Returns the suite results * * @access public * @return array Suite results */ public function getSuiteResults() { return $this->suiteResults; } /** * Sets up the container for result details of the current test suite when * each suite is first run * * @param PHPUnit_Framework_TestSuite $suite the suite that is been run */ public function startTestSuite(PHPUnit_Framework_TestSuite $suite) { $this->endCurrentTestSuite(); $this->currentSuite = array( 'suite' => $suite, // the test suite 'tests' => array(), // the tests in the suite 'errors' => 0, // number of tests with errors (including setup errors) 'failures' => 0, // number of tests which failed 'incomplete' => 0, // number of tests that were not completed correctly 'error' => null, // Any error encountered during setup of the test suite ); } /** * Sets up the container for result details of the current test when each * test is first run * * @param PHPUnit_Framework_Test $test the test that is being run */ public function startTest(PHPUnit_Framework_Test $test) { $this->endCurrentTest(); $this->startTestTime = microtime(true); $this->currentTest = array( // the name of the test (without the suite name) 'name' => $this->descriptiveTestName($test), // execution time of the test 'timeElapsed' => 0, // status of the test execution 'status' => TEST_SUCCESS, // user message of test result 'message' => '', // original caught exception thrown by test upon failure/error 'exception' => NULL, // Stacktrace used for exception handling 'trace' => NULL, // a unique ID for this test (used for identification purposes in results) 'uid' => md5(microtime()) ); if($this->hasTimer) $this->timer->start(); } /** * Logs the specified status to the current test, or if no test is currently * run, to the test suite. * @param integer $status Status code * @param string $message Message to log * @param string $exception Exception body related to this message * @param array $trace Stacktrace */ protected function addStatus($status, $message, $exception, $trace) { // Build status body to be saved $statusResult = array( 'status' => $status, 'message' => $message, 'exception' => $exception, 'trace' => $trace ); // Log either to current test or suite record if($this->currentTest) { $this->currentTest = array_merge($this->currentTest, $statusResult); } elseif($this->currentSuite) { $this->currentSuite['error'] = $statusResult; } else { $this->currentSession['error'][] = $statusResult; } } /** * Adds the failure detail to the current test and increases the failure * count for the current suite * * @param PHPUnit_Framework_Test $test current test that is being run * @param PHPUnit_Framework_AssertionFailedError $e PHPUnit error * @param int $time */ public function addFailure(PHPUnit_Framework_Test $test, PHPUnit_Framework_AssertionFailedError $e, $time) { if($this->currentSuite) { $this->currentSuite['failures']++; } else { $this->currentSession['failures']++; } $this->addStatus(TEST_FAILURE, $e->toString(), $this->getTestException($test, $e), $e->getTrace()); } /** * Adds the error detail to the current test and increases the error * count for the current suite * * @param PHPUnit_Framework_Test $test current test that is being run * @param Exception $e PHPUnit error * @param int $time */ public function addError(PHPUnit_Framework_Test $test, Exception $e, $time) { if($this->currentSuite) { $this->currentSuite['errors']++; } else { $this->currentSession['errors']++; } $this->addStatus(TEST_ERROR, $e->getMessage(), $this->getTestException($test, $e), $e->getTrace()); } /** * Adds the test incomplete detail to the current test and increases the incomplete * count for the current suite * * @param PHPUnit_Framework_Test $test current test that is being run * @param Exception $e PHPUnit error */ public function addIncompleteTest(PHPUnit_Framework_Test $test, Exception $e, $time) { if($this->currentSuite) { $this->currentSuite['incomplete']++; } else { $this->currentSession['incomplete']++; } $this->addStatus(TEST_INCOMPLETE, $e->getMessage(), $this->getTestException($test, $e), $e->getTrace()); } /** * Not used * * @param PHPUnit_Framework_Test $test * @param Exception $e * @param int $time */ public function addSkippedTest(PHPUnit_Framework_Test $test, Exception $e, $time) { // not implemented } /** * Cleanly end the current test */ protected function endCurrentTest() { if(!$this->currentTest || !$this->currentSuite) return; // Time the current test $testDuration = microtime(true) - $this->startTestTime; $this->testSpeeds[$this->currentSuite['suite']->getName() . '.' . $this->currentTest['name']] = $testDuration; if($this->hasTimer) { $this->timer->stop(); $this->currentTest['timeElapsed'] = $this->timer->timeElapsed(); } // Save and reset current state array_push($this->currentSuite['tests'], $this->currentTest); $this->currentTest = null; } /** * Upon completion of a test, records the execution time (if available) and adds the test to * the tests performed in the current suite. * * @param PHPUnit_Framework_Test $test Current test that is being run * @param int $time */ public function endTest( PHPUnit_Framework_Test $test, $time) { $this->endCurrentTest(); if(method_exists($test, 'getActualOutput')) { $output = $test->getActualOutput(); if($output) echo "\nOutput:\n$output"; } } /** * Cleanly end the current test suite */ protected function endCurrentTestSuite() { if(!$this->currentSuite) return; // Ensure any current test is ended along with the current suite $this->endCurrentTest(); // Save and reset current state array_push($this->suiteResults['suites'], $this->currentSuite); $this->currentSuite = null; } /** * Upon completion of a test suite adds the suite to the suties performed * * @param PHPUnit_Framework_TestSuite $suite current suite that is being run */ public function endTestSuite( PHPUnit_Framework_TestSuite $suite) { if(strlen($suite->getName())) { $this->endCurrentTestSuite(); } } /** * Risky test. * * @param PHPUnit_Framework_Test $test * @param Exception $e * @param float $time * @since Method available since Release 3.8.0 */ public function addRiskyTest(PHPUnit_Framework_Test $test, Exception $e, $time) { // Stub out to support PHPUnit 3.8 } /** * Tries to get the original exception thrown by the test on failure/error * to enable us to give a bit more detail about the failure/error * * @param PHPUnit_Framework_Test $test current test that is being run * @param Exception $e PHPUnit error * @return array */ private function getTestException(PHPUnit_Framework_Test $test, Exception $e) { // get the name of the testFile from the test $testName = $this->descriptiveTestName($test); $trace = $e->getTrace(); // loop through the exception trace to find the original exception for($i = 0; $i < count($trace); $i++) { if(array_key_exists('file', $trace[$i])) { if(stristr($trace[$i]['file'], $testName.'.php') != false) { return $trace[$i]; } } if(array_key_exists('file:protected', $trace[$i])) { if(stristr($trace[$i]['file:protected'], $testName.'.php') != false) { return $trace[$i]; } } } return array(); } /** * Writes a status message to the output stream in a user readable HTML format * @param string $name Name of the object that generated the error * @param string $message Message of the error * @param array $trace Stacktrace */ protected function writeResultError($name, $message, $trace) { echo "

⊗ ". $this->testNameToPhrase($name) ."

"; echo "
".htmlentities($message, ENT_COMPAT, 'UTF-8')."
"; echo SS_Backtrace::get_rendered_backtrace($trace); echo "
"; } /** * Display error bar if it exists */ public function writeResults() { $passCount = 0; $failCount = 0; $testCount = 0; $incompleteCount = 0; $errorCount = 0; // Includes both suite and test level errors // Ensure that the current suite is cleanly ended. // A suite may not end correctly if there was an error during setUp $this->endCurrentTestSuite(); // Write session errors if($this->currentSession['error']) { $errorCount += $this->currentSession['errors']; $failCount += $this->currentSession['failures']; $incompleteCount += $this->currentSession['incomplete']; foreach($this->currentSession['error'] as $error) { $this->writeResultError( 'SilverStripe\\Control\\Session', $error['message'], $error['trace'] ); } } // Write suite errors foreach($this->suiteResults['suites'] as $suite) { // Report suite error. In the case of fatal non-success messages // These should be reported as errors. Failure/Success relate only // to individual tests directly if($suite['error']) { $errorCount++; $this->writeResultError( $suite['suite']->getName(), $suite['error']['message'], $suite['error']['trace'] ); } // Run through all tests in this suite foreach($suite['tests'] as $test) { $testCount++; switch($test['status']) { case TEST_ERROR: $errorCount++; break; case TEST_INCOMPLETE: $incompleteCount++; break; case TEST_SUCCESS: $passCount++; break; case TEST_FAILURE: $failCount++; break; } // Report test error if ($test['status'] != TEST_SUCCESS) { $this->writeResultError( $test['name'], $test['message'], $test['trace'] ); } } } $result = ($failCount || $errorCount) ? 'fail' : 'pass'; echo "
"; echo "

$testCount tests run: $passCount passes, $failCount failures," . " and $incompleteCount incomplete with $errorCount errors

"; echo "
"; } protected function testNameToPhrase($name) { return ucfirst(preg_replace("/([a-z])([A-Z])/", "$1 $2", $name)); } /** * Get name for this test * * @param PHPUnit_Framework_Test $test * @return string */ protected function descriptiveTestName(PHPUnit_Framework_Test $test) { if ($test instanceof PHPUnit_Framework_TestCase) { $name = $test->toString(); } elseif(method_exists($test, 'getName')) { $name = $test->getName(); } else { $name = get_class($test); } // the name of the test (without the suite name) return preg_replace('(\(.*\))', '', $name); } }