PageRenderTime 32ms CodeModel.GetById 18ms RepoModel.GetById 1ms app.codeStats 0ms

/dev/SapphireTestReporter.php

http://github.com/silverstripe/sapphire
PHP | 470 lines | 222 code | 46 blank | 202 comment | 32 complexity | 055d0a54543f8201baff306fc62af8df MD5 | raw file
Possible License(s): BSD-3-Clause, MIT, CC-BY-3.0, GPL-2.0, AGPL-1.0, LGPL-2.1
  1. <?php
  2. /**#@+
  3. * @var int
  4. */
  5. /**
  6. * Failure test status constant
  7. */
  8. define('TEST_FAILURE', -1);
  9. /**
  10. * Error test status constant
  11. */
  12. define('TEST_ERROR', 0);
  13. /**
  14. * Success test status constant
  15. */
  16. define('TEST_SUCCESS', 1);
  17. /**
  18. * Incomplete test status constant
  19. */
  20. define('TEST_INCOMPLETE', 2);
  21. /**#@-*/
  22. /**
  23. * Gathers details about PHPUnit2 test suites as they
  24. * are been executed. This does not actually format any output
  25. * but simply gathers extended information about the overall
  26. * results of all suites & their tests for use elsewhere.
  27. *
  28. * Changelog:
  29. * 0.6 First created [David Spurr]
  30. * 0.7 Added fix to getTestException provided [Glen Ogilvie]
  31. *
  32. * @package framework
  33. * @subpackage testing
  34. *
  35. * @version 0.7 2006-03-12
  36. * @author David Spurr
  37. */
  38. class SapphireTestReporter implements PHPUnit_Framework_TestListener {
  39. /**
  40. * Holds array of suites and total number of tests run
  41. * @var array
  42. */
  43. protected $suiteResults;
  44. /**
  45. * Holds data of current suite that is been run
  46. * @var array
  47. */
  48. protected $currentSuite;
  49. /**
  50. * Holds data of current test that is been run
  51. * @var array
  52. */
  53. protected $currentTest;
  54. /**
  55. * Whether PEAR Benchmark_Timer is available for timing
  56. * @var boolean
  57. */
  58. protected $hasTimer;
  59. /**
  60. * Holds the PEAR Benchmark_Timer object
  61. *
  62. * @var Benchmark_Timer
  63. */
  64. protected $timer;
  65. /**
  66. * @var int
  67. */
  68. protected $startTestTime;
  69. /**
  70. * An array of all the test speeds
  71. *
  72. * @var array
  73. */
  74. protected $testSpeeds = array();
  75. /**
  76. * Errors not belonging to a test or suite
  77. *
  78. * @var array
  79. */
  80. protected $currentSession = array();
  81. /**
  82. * Constructor, checks to see availability of PEAR Benchmark_Timer and
  83. * sets up basic properties
  84. */
  85. public function __construct() {
  86. @include_once 'Benchmark/Timer.php';
  87. if(class_exists('Benchmark_Timer')) {
  88. $this->timer = new Benchmark_Timer();
  89. $this->hasTimer = true;
  90. } else {
  91. $this->hasTimer = false;
  92. }
  93. $this->suiteResults = array(
  94. 'suites' => array(), // array of suites run
  95. 'hasTimer' => $this->hasTimer, // availability of PEAR Benchmark_Timer
  96. 'totalTests' => 0 // total number of tests run
  97. );
  98. $this->currentSession = array(
  99. 'errors' => 0, // number of tests with errors (including setup errors)
  100. 'failures' => 0, // number of tests which failed
  101. 'incomplete' => 0, // number of tests that were not completed correctly
  102. 'error' => array(), // Any error encountered outside of suites
  103. );
  104. }
  105. /**
  106. * Returns the suite results
  107. *
  108. * @access public
  109. * @return array Suite results
  110. */
  111. public function getSuiteResults() {
  112. return $this->suiteResults;
  113. }
  114. /**
  115. * Sets up the container for result details of the current test suite when
  116. * each suite is first run
  117. *
  118. * @param PHPUnit_Framework_TestSuite $suite the suite that is been run
  119. */
  120. public function startTestSuite(PHPUnit_Framework_TestSuite $suite) {
  121. $this->endCurrentTestSuite();
  122. $this->currentSuite = array(
  123. 'suite' => $suite, // the test suite
  124. 'tests' => array(), // the tests in the suite
  125. 'errors' => 0, // number of tests with errors (including setup errors)
  126. 'failures' => 0, // number of tests which failed
  127. 'incomplete' => 0, // number of tests that were not completed correctly
  128. 'error' => null, // Any error encountered during setup of the test suite
  129. );
  130. }
  131. /**
  132. * Sets up the container for result details of the current test when each
  133. * test is first run
  134. *
  135. * @param PHPUnit_Framework_Test $test the test that is being run
  136. */
  137. public function startTest(PHPUnit_Framework_Test $test) {
  138. $this->endCurrentTest();
  139. $this->startTestTime = microtime(true);
  140. $this->currentTest = array(
  141. // the name of the test (without the suite name)
  142. 'name' => $this->descriptiveTestName($test),
  143. // execution time of the test
  144. 'timeElapsed' => 0,
  145. // status of the test execution
  146. 'status' => TEST_SUCCESS,
  147. // user message of test result
  148. 'message' => '',
  149. // original caught exception thrown by test upon failure/error
  150. 'exception' => NULL,
  151. // Stacktrace used for exception handling
  152. 'trace' => NULL,
  153. // a unique ID for this test (used for identification purposes in results)
  154. 'uid' => md5(microtime())
  155. );
  156. if($this->hasTimer) $this->timer->start();
  157. }
  158. /**
  159. * Logs the specified status to the current test, or if no test is currently
  160. * run, to the test suite.
  161. * @param integer $status Status code
  162. * @param string $message Message to log
  163. * @param string $exception Exception body related to this message
  164. * @param array $trace Stacktrace
  165. */
  166. protected function addStatus($status, $message, $exception, $trace) {
  167. // Build status body to be saved
  168. $statusResult = array(
  169. 'status' => $status,
  170. 'message' => $message,
  171. 'exception' => $exception,
  172. 'trace' => $trace
  173. );
  174. // Log either to current test or suite record
  175. if($this->currentTest) {
  176. $this->currentTest = array_merge($this->currentTest, $statusResult);
  177. } elseif($this->currentSuite) {
  178. $this->currentSuite['error'] = $statusResult;
  179. } else {
  180. $this->currentSession['error'][] = $statusResult;
  181. }
  182. }
  183. /**
  184. * Adds the failure detail to the current test and increases the failure
  185. * count for the current suite
  186. *
  187. * @param PHPUnit_Framework_Test $test current test that is being run
  188. * @param PHPUnit_Framework_AssertionFailedError $e PHPUnit error
  189. * @param int $time
  190. */
  191. public function addFailure(PHPUnit_Framework_Test $test, PHPUnit_Framework_AssertionFailedError $e, $time) {
  192. if($this->currentSuite) {
  193. $this->currentSuite['failures']++;
  194. } else {
  195. $this->currentSession['failures']++;
  196. }
  197. $this->addStatus(TEST_FAILURE, $e->toString(), $this->getTestException($test, $e), $e->getTrace());
  198. }
  199. /**
  200. * Adds the error detail to the current test and increases the error
  201. * count for the current suite
  202. *
  203. * @param PHPUnit_Framework_Test $test current test that is being run
  204. * @param Exception $e PHPUnit error
  205. * @param int $time
  206. */
  207. public function addError(PHPUnit_Framework_Test $test, Exception $e, $time) {
  208. if($this->currentSuite) {
  209. $this->currentSuite['errors']++;
  210. } else {
  211. $this->currentSession['errors']++;
  212. }
  213. $this->addStatus(TEST_ERROR, $e->getMessage(), $this->getTestException($test, $e), $e->getTrace());
  214. }
  215. /**
  216. * Adds the test incomplete detail to the current test and increases the incomplete
  217. * count for the current suite
  218. *
  219. * @param PHPUnit_Framework_Test $test current test that is being run
  220. * @param Exception $e PHPUnit error
  221. */
  222. public function addIncompleteTest(PHPUnit_Framework_Test $test, Exception $e, $time) {
  223. if($this->currentSuite) {
  224. $this->currentSuite['incomplete']++;
  225. } else {
  226. $this->currentSession['incomplete']++;
  227. }
  228. $this->addStatus(TEST_INCOMPLETE, $e->getMessage(), $this->getTestException($test, $e), $e->getTrace());
  229. }
  230. /**
  231. * Not used
  232. *
  233. * @param PHPUnit_Framework_Test $test
  234. * @param Exception $e
  235. * @param int $time
  236. */
  237. public function addSkippedTest(PHPUnit_Framework_Test $test, Exception $e, $time) {
  238. // not implemented
  239. }
  240. /**
  241. * Cleanly end the current test
  242. */
  243. protected function endCurrentTest() {
  244. if(!$this->currentTest || !$this->currentSuite) return;
  245. // Time the current test
  246. $testDuration = microtime(true) - $this->startTestTime;
  247. $this->testSpeeds[$this->currentSuite['suite']->getName() . '.' . $this->currentTest['name']] = $testDuration;
  248. if($this->hasTimer) {
  249. $this->timer->stop();
  250. $this->currentTest['timeElapsed'] = $this->timer->timeElapsed();
  251. }
  252. // Save and reset current state
  253. array_push($this->currentSuite['tests'], $this->currentTest);
  254. $this->currentTest = null;
  255. }
  256. /**
  257. * Upon completion of a test, records the execution time (if available) and adds the test to
  258. * the tests performed in the current suite.
  259. *
  260. * @param PHPUnit_Framework_Test $test Current test that is being run
  261. * @param int $time
  262. */
  263. public function endTest( PHPUnit_Framework_Test $test, $time) {
  264. $this->endCurrentTest();
  265. if(method_exists($test, 'getActualOutput')) {
  266. $output = $test->getActualOutput();
  267. if($output) echo "\nOutput:\n$output";
  268. }
  269. }
  270. /**
  271. * Cleanly end the current test suite
  272. */
  273. protected function endCurrentTestSuite() {
  274. if(!$this->currentSuite) return;
  275. // Ensure any current test is ended along with the current suite
  276. $this->endCurrentTest();
  277. // Save and reset current state
  278. array_push($this->suiteResults['suites'], $this->currentSuite);
  279. $this->currentSuite = null;
  280. }
  281. /**
  282. * Upon completion of a test suite adds the suite to the suties performed
  283. *
  284. * @param PHPUnit_Framework_TestSuite $suite current suite that is being run
  285. */
  286. public function endTestSuite( PHPUnit_Framework_TestSuite $suite) {
  287. if(strlen($suite->getName())) {
  288. $this->endCurrentTestSuite();
  289. }
  290. }
  291. /**
  292. * Risky test.
  293. *
  294. * @param PHPUnit_Framework_Test $test
  295. * @param Exception $e
  296. * @param float $time
  297. * @since Method available since Release 3.8.0
  298. */
  299. public function addRiskyTest(PHPUnit_Framework_Test $test, Exception $e, $time) {
  300. // Stub out to support PHPUnit 3.8
  301. }
  302. /**
  303. * Tries to get the original exception thrown by the test on failure/error
  304. * to enable us to give a bit more detail about the failure/error
  305. *
  306. * @param PHPUnit_Framework_Test $test current test that is being run
  307. * @param Exception $e PHPUnit error
  308. * @return array
  309. */
  310. private function getTestException(PHPUnit_Framework_Test $test, Exception $e) {
  311. // get the name of the testFile from the test
  312. $testName = $this->descriptiveTestName($test);
  313. $trace = $e->getTrace();
  314. // loop through the exception trace to find the original exception
  315. for($i = 0; $i < count($trace); $i++) {
  316. if(array_key_exists('file', $trace[$i])) {
  317. if(stristr($trace[$i]['file'], $testName.'.php') != false) {
  318. return $trace[$i];
  319. }
  320. }
  321. if(array_key_exists('file:protected', $trace[$i])) {
  322. if(stristr($trace[$i]['file:protected'], $testName.'.php') != false) {
  323. return $trace[$i];
  324. }
  325. }
  326. }
  327. return array();
  328. }
  329. /**
  330. * Writes a status message to the output stream in a user readable HTML format
  331. * @param string $name Name of the object that generated the error
  332. * @param string $message Message of the error
  333. * @param array $trace Stacktrace
  334. */
  335. protected function writeResultError($name, $message, $trace) {
  336. echo "<div class=\"failure\"><h2 class=\"test-case\">&otimes; ". $this->testNameToPhrase($name) ."</h2>";
  337. echo "<pre>".htmlentities($message, ENT_COMPAT, 'UTF-8')."</pre>";
  338. echo SS_Backtrace::get_rendered_backtrace($trace);
  339. echo "</div>";
  340. }
  341. /**
  342. * Display error bar if it exists
  343. */
  344. public function writeResults() {
  345. $passCount = 0;
  346. $failCount = 0;
  347. $testCount = 0;
  348. $incompleteCount = 0;
  349. $errorCount = 0; // Includes both suite and test level errors
  350. // Ensure that the current suite is cleanly ended.
  351. // A suite may not end correctly if there was an error during setUp
  352. $this->endCurrentTestSuite();
  353. // Write session errors
  354. if($this->currentSession['error']) {
  355. $errorCount += $this->currentSession['errors'];
  356. $failCount += $this->currentSession['failures'];
  357. $incompleteCount += $this->currentSession['incomplete'];
  358. foreach($this->currentSession['error'] as $error) {
  359. $this->writeResultError(
  360. 'Session',
  361. $error['message'],
  362. $error['trace']
  363. );
  364. }
  365. }
  366. // Write suite errors
  367. foreach($this->suiteResults['suites'] as $suite) {
  368. // Report suite error. In the case of fatal non-success messages
  369. // These should be reported as errors. Failure/Success relate only
  370. // to individual tests directly
  371. if($suite['error']) {
  372. $errorCount++;
  373. $this->writeResultError(
  374. $suite['suite']->getName(),
  375. $suite['error']['message'],
  376. $suite['error']['trace']
  377. );
  378. }
  379. // Run through all tests in this suite
  380. foreach($suite['tests'] as $test) {
  381. $testCount++;
  382. switch($test['status']) {
  383. case TEST_ERROR: $errorCount++; break;
  384. case TEST_INCOMPLETE: $incompleteCount++; break;
  385. case TEST_SUCCESS: $passCount++; break;
  386. case TEST_FAILURE: $failCount++; break;
  387. }
  388. // Report test error
  389. if ($test['status'] != TEST_SUCCESS) {
  390. $this->writeResultError(
  391. $test['name'],
  392. $test['message'],
  393. $test['trace']
  394. );
  395. }
  396. }
  397. }
  398. $result = ($failCount || $errorCount) ? 'fail' : 'pass';
  399. echo "<div class=\"status $result\">";
  400. echo "<h2><span>$testCount</span> tests run: <span>$passCount</span> passes, <span>$failCount</span> failures,"
  401. . " and <span>$incompleteCount</span> incomplete with <span>$errorCount</span> errors</h2>";
  402. echo "</div>";
  403. }
  404. protected function testNameToPhrase($name) {
  405. return ucfirst(preg_replace("/([a-z])([A-Z])/", "$1 $2", $name));
  406. }
  407. /**
  408. * Get name for this test
  409. *
  410. * @param PHPUnit_Framework_Test $test
  411. * @return string
  412. */
  413. protected function descriptiveTestName(PHPUnit_Framework_Test $test) {
  414. if ($test instanceof PHPUnit_Framework_TestCase) {
  415. $name = $test->toString();
  416. } elseif(method_exists($test, 'getName')) {
  417. $name = $test->getName();
  418. } else {
  419. $name = get_class($test);
  420. }
  421. // the name of the test (without the suite name)
  422. return preg_replace('(\(.*\))', '', $name);
  423. }
  424. }