PageRenderTime 27ms CodeModel.GetById 10ms RepoModel.GetById 0ms app.codeStats 0ms

/wee/tests/weeTestSuite.class.php

https://github.com/extend/wee
PHP | 412 lines | 212 code | 84 blank | 116 comment | 53 complexity | 451f81b20f83dd88907d8d35b2012749 MD5 | raw file
  1. <?php
  2. /*
  3. Web:Extend
  4. Copyright (c) 2006-2010 Dev:Extend
  5. This library is free software; you can redistribute it and/or
  6. modify it under the terms of the GNU Lesser General Public
  7. License as published by the Free Software Foundation; either
  8. version 2.1 of the License, or (at your option) any later version.
  9. This library is distributed in the hope that it will be useful,
  10. but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  12. Lesser General Public License for more details.
  13. You should have received a copy of the GNU Lesser General Public
  14. License along with this library; if not, write to the Free Software
  15. Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
  16. */
  17. if (!defined('ALLOW_INCLUSION')) die;
  18. /**
  19. Automated CLI unit testing and code coverage analysis tool.
  20. This unit testing tool is designed for use with the Web:Extend's framework only.
  21. It is meant to be very light and embedded with the framework's distribution to
  22. allow anyone to check if it will work correctly on their platform using a simple
  23. "make test". It is not meant to be used to test applications. There are much
  24. better tools for this purpose, like PHPUnit, available at http://phpunit.de
  25. Unit test cases that return false value will be ignored.
  26. Return false if you need additional files that are not unit test cases.
  27. */
  28. class weeTestSuite implements Mappable, Printable
  29. {
  30. /**
  31. Extended data generated by the unit test suite.
  32. */
  33. protected $aExtData = array();
  34. /**
  35. The result of the last test ran.
  36. Used to produce a more readable output by separating tests with different results.
  37. */
  38. protected $mLastResult = '';
  39. /**
  40. Array containing the memory usages of all the successful tests.
  41. */
  42. protected $aMemoryResults = array();
  43. /**
  44. Array containing the results of the unit test suite, after its completion.
  45. */
  46. protected $aResults = array();
  47. /**
  48. Array containing the execution times of all the successful tests.
  49. */
  50. protected $aTimeResults = array();
  51. /**
  52. Initialize the test suite.
  53. @param $sTestsPath Path to the unit test cases.
  54. */
  55. public function __construct($sTestsPath)
  56. {
  57. !defined('WEE_CODE_COVERAGE') || function_exists('xdebug_enable')
  58. or burn('ConfigurationException', _WT('The XDebug PHP extension is required for code coverage analysis.'));
  59. if (defined('WEE_ON_WINDOWS'))
  60. $sTestsPath = realpath(getcwd()) . '\\' . str_replace('/', '\\', $sTestsPath);
  61. else
  62. $sTestsPath = realpath(getcwd()) . '/' . $sTestsPath;
  63. // Build the array containing the unit tests results
  64. $oDirectory = new RecursiveDirectoryIterator($sTestsPath);
  65. foreach (new RecursiveIteratorIterator($oDirectory) as $sPath) {
  66. $sPath = (string)$sPath;
  67. // Skip files already included
  68. if (in_array($sPath, get_included_files()))
  69. continue;
  70. // Skip files not ending with .php
  71. if (substr($sPath, -strlen(PHP_EXT)) != PHP_EXT)
  72. continue;
  73. // Skip class files
  74. if (substr($sPath, -strlen(CLASS_EXT)) == CLASS_EXT)
  75. continue;
  76. $this->aResults[$sPath] = 'skip';
  77. }
  78. }
  79. /**
  80. Add a result to the result array.
  81. Results must be either "success" or "skip" or an Exception.
  82. @param $sFile The filename of the unit test case.
  83. @param $mResult The result of the unit test case.
  84. @param $fTime Execution time of the test.
  85. @throw DomainException $mResult is not a valid result.
  86. */
  87. protected function addResult($sFile, $mResult, $fTime = 0, $iMemoryUsed = 0)
  88. {
  89. $mResult == 'success' or $mResult == 'skip' or is_object($mResult) and $mResult instanceof Exception
  90. or burn('DomainException', _WT('$mResult is not a valid result.'));
  91. $this->aResults[$sFile] = $mResult;
  92. // Shorten the filename by limiting it to ROOT_PATH when we are inside ROOT_PATH
  93. // Just improves the display, don't change it for the $aResults array
  94. $sFile = str_replace(realpath(ROOT_PATH) . '/', '', $sFile);
  95. if (!is_string($this->mLastResult) || !is_string($mResult))
  96. echo "--\n";
  97. echo $sFile . ': ';
  98. if ($mResult === 'success') {
  99. echo _WT('success') . ' [' . round($fTime, 2) . "s]\n";
  100. $this->aMemoryResults[$sFile] = $iMemoryUsed;
  101. $this->aTimeResults[$sFile] = $fTime;
  102. } elseif ($mResult === 'skip')
  103. echo _WT('skip') . "\n";
  104. elseif ($mResult instanceof UnitTestException) {
  105. $aTrace = $mResult->getTrace();
  106. echo _WT('failure') . "\n" . sprintf(_WT("Message: %s\nLine: %s\n"),
  107. $mResult->getMessage(), array_value($aTrace[0], 'line', '?'));
  108. if ($mResult instanceof ComparisonTestException)
  109. echo sprintf(_WT("Expected: %s\nActual: %s\n"),
  110. var_export($mResult->getExpected(), true), var_export($mResult->getActual(), true));
  111. } elseif ($mResult instanceof ErrorException)
  112. echo _WT('error') . "\n" . sprintf(_WT("Class: ErrorException\nMessage: %s\nLevel: %s\nFile: %s\nLine: %s\n"),
  113. $mResult->getMessage(), weeException::getLevelName($mResult->getSeverity()),
  114. $mResult->getFile(), $mResult->getLine());
  115. else
  116. echo _WT('error') . "\n" . sprintf(_WT("Class: %s\nMessage: %s\nFile: %s\nLine: %s\nTrace:\n%s\n"),
  117. get_class($mResult), $mResult->getMessage(), $mResult->getFile(),
  118. $mResult->getLine(), $mResult->getTraceAsString());
  119. $this->mLastResult = $mResult;
  120. }
  121. /**
  122. Return an analyze of the collected information for code coverage.
  123. @return array The covered code.
  124. */
  125. protected function analyzeCodeCoverage()
  126. {
  127. $aCoveredCode = array();
  128. foreach ($this->aExtData as $sTestFile => $aCoveredFiles) {
  129. foreach ($aCoveredFiles as $sFile => $aData)
  130. if (is_array($aData) && array_key_exists(0, $aData) && array_key_exists(1, $aData))
  131. if ($aData[0] === 'weeCoveredCode') {
  132. foreach ($aData[1] as $sCoveredFilename => $aCoveredLines)
  133. foreach($aCoveredLines as $iLine => $iCovered) {
  134. if (!isset($aCoveredCode[$sCoveredFilename][$iLine]))
  135. $aCoveredCode[$sCoveredFilename][$iLine] = $iCovered;
  136. elseif ($iCovered > $aCoveredCode[$sCoveredFilename][$iLine])
  137. $aCoveredCode[$sCoveredFilename][$iLine] = $iCovered;
  138. }
  139. unset($this->aExtData[$sTestFile][$sFile]);
  140. }
  141. // Clean-up
  142. if (empty($this->aExtData[$sTestFile]))
  143. unset($this->aExtData[$sTestFile]);
  144. }
  145. return $aCoveredCode;
  146. }
  147. /**
  148. Display information about covered code, non-covered code and dead code.
  149. @param $aCoveredCode The code coverage information returned by weeTestSuite::analyzeCodeCoverage.
  150. */
  151. protected function printCodeCoverage($aCoveredCode = array())
  152. {
  153. echo "\n" . _WT('Code coverage report:') . "\n";
  154. ksort($aCoveredCode);
  155. foreach ($aCoveredCode as $sFilename => $aLines) {
  156. echo "\n" . $sFilename;
  157. if (in_array(1, $aLines)) {
  158. echo "\n" . _WT('Covered lines:') . ' ';
  159. $this->printFileCoverage($aLines, 1);
  160. } else {
  161. echo "\n" . _WT('No code covered.');
  162. }
  163. if (in_array(-1, $aLines)) {
  164. echo "\n" . _WT('Non-covered lines:') . ' ';
  165. $this->printFileCoverage($aLines, -1);
  166. }
  167. // Dead-code handling is tricky. XDebug returns dead code for empty
  168. // lines with only a "}", often after a return. While it's technically
  169. // true, it is of no use to us and thus all these lines are removed
  170. // from our results (if possible). In short, first we remove them,
  171. // and if there's any dead code remaining, we output it.
  172. if (in_array(-2, $aLines)) {
  173. if (is_file($sFilename)) {
  174. $aFile = file($sFilename, FILE_IGNORE_NEW_LINES);
  175. foreach ($aLines as $iLine => $iValue)
  176. if (isset($aFile[$iLine - 1]) && trim($aFile[$iLine - 1]) == '}')
  177. unset($aLines[$iLine]);
  178. }
  179. }
  180. if (in_array(-2, $aLines)) {
  181. echo "\n" . _WT('Dead code:') . ' ';
  182. $this->printFileCoverage($aLines, -2);
  183. }
  184. }
  185. echo "\n";
  186. }
  187. /**
  188. Display information about which lines were executed or not.
  189. The values for $iDebugOption are:
  190. * 1: Covered code.
  191. * -1: Uncovered code.
  192. * -2: Dead code.
  193. @param $aLines List of lines with the coverage information associated (covered line, non-covered line or dead line).
  194. @param $iDebugOption The option indicates the kind of information to display.
  195. */
  196. protected function printFileCoverage($aLines, $iDebugOption)
  197. {
  198. foreach ($aLines as $iCur => $iCovered) {
  199. if ($iCovered != $iDebugOption)
  200. continue;
  201. if (!isset($iPrev)) {
  202. $iPrev = $iFirst = $iCur;
  203. continue;
  204. }
  205. if ($iPrev + 1 != $iCur) {
  206. if ($iFirst != $iPrev)
  207. echo $iFirst . '-' . $iPrev . ';';
  208. else
  209. echo $iFirst . ';';
  210. $iFirst = $iCur;
  211. }
  212. $iPrev = $iCur;
  213. }
  214. if (isset($iPrev)) {
  215. if ($iFirst != $iPrev)
  216. echo $iFirst . '-' . $iPrev . ';';
  217. else
  218. echo $iFirst . ';';
  219. }
  220. }
  221. /**
  222. Run the test suite.
  223. */
  224. public function run()
  225. {
  226. foreach ($this->aResults as $sPath => $mResult) {
  227. try {
  228. $oTest = new weeUnitTestCase($sPath);
  229. $fBefore = microtime(true);
  230. $iMemoryUsed = $oTest->run();
  231. $this->addResult($sPath, 'success', microtime(true) - $fBefore, $iMemoryUsed);
  232. if ($oTest->hasExtData())
  233. $this->aExtData[$sPath] = $oTest->getExtData();
  234. } catch (SkipTestException $o) {
  235. $this->addResult($sPath, 'skip');
  236. } catch (Exception $o) {
  237. $this->addResult($sPath, $o);
  238. }
  239. }
  240. }
  241. /**
  242. Return the results array of the unit test suite.
  243. It is always available, even when tests have not been run yet.
  244. @return array Results array for all the unit test cases.
  245. */
  246. public function toArray()
  247. {
  248. return $this->aResults;
  249. }
  250. /**
  251. Return the results of the unit test suite.
  252. @return string A report of the unit test suite after its completion.
  253. */
  254. public function toString()
  255. {
  256. // Analyze and output the code coverage results
  257. if (defined('WEE_CODE_COVERAGE') && !empty($this->aExtData))
  258. $this->printCodeCoverage($this->analyzeCodeCoverage());
  259. // Output the extended data returned by the test cases
  260. if (!empty($this->aExtData)) {
  261. echo "\n" . _WT('Extended data:') . "\n\n";
  262. foreach ($this->aExtData as $sFile => $aTestData) {
  263. echo $sFile . "\n";
  264. foreach ($aTestData as $aData) {
  265. echo ' ' . $aData[0] . ': ';
  266. if (is_array($aData[1]))
  267. echo str_replace(array("\n ", "\n"), '', var_export($aData[1], true));
  268. else
  269. echo $aData[1];
  270. echo "\n";
  271. }
  272. }
  273. }
  274. // Output the tests that took the most time to run
  275. $s = "\n" . _WT('List of tests with the biggest execution time:');
  276. arsort($this->aTimeResults);
  277. $iTotal = count($this->aTimeResults);
  278. if ($iTotal > 10)
  279. $iTotal = 10;
  280. reset($this->aTimeResults);
  281. for ($i = 1; $i <= $iTotal; $i++) {
  282. $s .= "\n" . sprintf('%2s', $i) . '- ' . key($this->aTimeResults) . ': ' . round(current($this->aTimeResults), 2) . 's';
  283. next($this->aTimeResults);
  284. }
  285. // Output the tests that used the most memory
  286. $s .= "\n\n" . _WT('List of tests with the biggest memory consumption at the end of the test:');
  287. arsort($this->aMemoryResults);
  288. $iTotal = count($this->aMemoryResults);
  289. if ($iTotal > 10)
  290. $iTotal = 10;
  291. reset($this->aMemoryResults);
  292. for ($i = 1; $i <= $iTotal; $i++) {
  293. $s .= "\n" . sprintf('%2s', $i) . '- ' . key($this->aMemoryResults) . ': ' . current($this->aMemoryResults) . ' bytes';
  294. next($this->aMemoryResults);
  295. }
  296. $s .= "\n" . sprintf(_WT('Peak memory usage: %d bytes'), memory_get_peak_usage()) . "\n\n";
  297. // Count the number of tests failed, succeeded and skipped and output a summary
  298. // The array contains "skip" and "success" values but also instances of Exception,
  299. // array_count_values triggers a warning when one of the values in the array cannot
  300. // be used as a key.
  301. $aCounts = @array_count_values($this->aResults);
  302. if (!isset($aCounts['skip']))
  303. $aCounts['skip'] = 0;
  304. $iSkippedAndSucceededCount = $aCounts['success'] + $aCounts['skip'];
  305. $iTestsCount = count($this->aResults);
  306. if ($iSkippedAndSucceededCount == $iTestsCount)
  307. $s .= sprintf(_WT('All %d tests succeeded!'), $aCounts['success']);
  308. else
  309. $s .= sprintf(_WT('%d of %d tests failed.'), $iTestsCount - $iSkippedAndSucceededCount, $iTestsCount - $aCounts['skip']);
  310. if ($aCounts['skip'] != 0)
  311. $s .= ' ' . sprintf(_WT('%d tests have been skipped.'), $aCounts['skip']);
  312. return $s . "\n";
  313. }
  314. }