/wee/tests/weeTestSuite.class.php
PHP | 412 lines | 212 code | 84 blank | 116 comment | 53 complexity | 451f81b20f83dd88907d8d35b2012749 MD5 | raw file
- <?php
- /*
- Web:Extend
- Copyright (c) 2006-2010 Dev:Extend
- This library is free software; you can redistribute it and/or
- modify it under the terms of the GNU Lesser General Public
- License as published by the Free Software Foundation; either
- version 2.1 of the License, or (at your option) any later version.
- This library is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- Lesser General Public License for more details.
- You should have received a copy of the GNU Lesser General Public
- License along with this library; if not, write to the Free Software
- Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
- if (!defined('ALLOW_INCLUSION')) die;
- /**
- Automated CLI unit testing and code coverage analysis tool.
- This unit testing tool is designed for use with the Web:Extend's framework only.
- It is meant to be very light and embedded with the framework's distribution to
- allow anyone to check if it will work correctly on their platform using a simple
- "make test". It is not meant to be used to test applications. There are much
- better tools for this purpose, like PHPUnit, available at http://phpunit.de
- Unit test cases that return false value will be ignored.
- Return false if you need additional files that are not unit test cases.
- */
- class weeTestSuite implements Mappable, Printable
- {
- /**
- Extended data generated by the unit test suite.
- */
- protected $aExtData = array();
- /**
- The result of the last test ran.
- Used to produce a more readable output by separating tests with different results.
- */
- protected $mLastResult = '';
- /**
- Array containing the memory usages of all the successful tests.
- */
- protected $aMemoryResults = array();
- /**
- Array containing the results of the unit test suite, after its completion.
- */
- protected $aResults = array();
- /**
- Array containing the execution times of all the successful tests.
- */
- protected $aTimeResults = array();
- /**
- Initialize the test suite.
- @param $sTestsPath Path to the unit test cases.
- */
- public function __construct($sTestsPath)
- {
- !defined('WEE_CODE_COVERAGE') || function_exists('xdebug_enable')
- or burn('ConfigurationException', _WT('The XDebug PHP extension is required for code coverage analysis.'));
- if (defined('WEE_ON_WINDOWS'))
- $sTestsPath = realpath(getcwd()) . '\\' . str_replace('/', '\\', $sTestsPath);
- else
- $sTestsPath = realpath(getcwd()) . '/' . $sTestsPath;
- // Build the array containing the unit tests results
- $oDirectory = new RecursiveDirectoryIterator($sTestsPath);
- foreach (new RecursiveIteratorIterator($oDirectory) as $sPath) {
- $sPath = (string)$sPath;
- // Skip files already included
- if (in_array($sPath, get_included_files()))
- continue;
- // Skip files not ending with .php
- if (substr($sPath, -strlen(PHP_EXT)) != PHP_EXT)
- continue;
- // Skip class files
- if (substr($sPath, -strlen(CLASS_EXT)) == CLASS_EXT)
- continue;
- $this->aResults[$sPath] = 'skip';
- }
- }
- /**
- Add a result to the result array.
- Results must be either "success" or "skip" or an Exception.
- @param $sFile The filename of the unit test case.
- @param $mResult The result of the unit test case.
- @param $fTime Execution time of the test.
- @throw DomainException $mResult is not a valid result.
- */
- protected function addResult($sFile, $mResult, $fTime = 0, $iMemoryUsed = 0)
- {
- $mResult == 'success' or $mResult == 'skip' or is_object($mResult) and $mResult instanceof Exception
- or burn('DomainException', _WT('$mResult is not a valid result.'));
- $this->aResults[$sFile] = $mResult;
- // Shorten the filename by limiting it to ROOT_PATH when we are inside ROOT_PATH
- // Just improves the display, don't change it for the $aResults array
- $sFile = str_replace(realpath(ROOT_PATH) . '/', '', $sFile);
- if (!is_string($this->mLastResult) || !is_string($mResult))
- echo "--\n";
- echo $sFile . ': ';
- if ($mResult === 'success') {
- echo _WT('success') . ' [' . round($fTime, 2) . "s]\n";
- $this->aMemoryResults[$sFile] = $iMemoryUsed;
- $this->aTimeResults[$sFile] = $fTime;
- } elseif ($mResult === 'skip')
- echo _WT('skip') . "\n";
- elseif ($mResult instanceof UnitTestException) {
- $aTrace = $mResult->getTrace();
- echo _WT('failure') . "\n" . sprintf(_WT("Message: %s\nLine: %s\n"),
- $mResult->getMessage(), array_value($aTrace[0], 'line', '?'));
- if ($mResult instanceof ComparisonTestException)
- echo sprintf(_WT("Expected: %s\nActual: %s\n"),
- var_export($mResult->getExpected(), true), var_export($mResult->getActual(), true));
- } elseif ($mResult instanceof ErrorException)
- echo _WT('error') . "\n" . sprintf(_WT("Class: ErrorException\nMessage: %s\nLevel: %s\nFile: %s\nLine: %s\n"),
- $mResult->getMessage(), weeException::getLevelName($mResult->getSeverity()),
- $mResult->getFile(), $mResult->getLine());
- else
- echo _WT('error') . "\n" . sprintf(_WT("Class: %s\nMessage: %s\nFile: %s\nLine: %s\nTrace:\n%s\n"),
- get_class($mResult), $mResult->getMessage(), $mResult->getFile(),
- $mResult->getLine(), $mResult->getTraceAsString());
- $this->mLastResult = $mResult;
- }
- /**
- Return an analyze of the collected information for code coverage.
- @return array The covered code.
- */
- protected function analyzeCodeCoverage()
- {
- $aCoveredCode = array();
- foreach ($this->aExtData as $sTestFile => $aCoveredFiles) {
- foreach ($aCoveredFiles as $sFile => $aData)
- if (is_array($aData) && array_key_exists(0, $aData) && array_key_exists(1, $aData))
- if ($aData[0] === 'weeCoveredCode') {
- foreach ($aData[1] as $sCoveredFilename => $aCoveredLines)
- foreach($aCoveredLines as $iLine => $iCovered) {
- if (!isset($aCoveredCode[$sCoveredFilename][$iLine]))
- $aCoveredCode[$sCoveredFilename][$iLine] = $iCovered;
- elseif ($iCovered > $aCoveredCode[$sCoveredFilename][$iLine])
- $aCoveredCode[$sCoveredFilename][$iLine] = $iCovered;
- }
- unset($this->aExtData[$sTestFile][$sFile]);
- }
- // Clean-up
- if (empty($this->aExtData[$sTestFile]))
- unset($this->aExtData[$sTestFile]);
- }
- return $aCoveredCode;
- }
- /**
- Display information about covered code, non-covered code and dead code.
- @param $aCoveredCode The code coverage information returned by weeTestSuite::analyzeCodeCoverage.
- */
- protected function printCodeCoverage($aCoveredCode = array())
- {
- echo "\n" . _WT('Code coverage report:') . "\n";
- ksort($aCoveredCode);
- foreach ($aCoveredCode as $sFilename => $aLines) {
- echo "\n" . $sFilename;
- if (in_array(1, $aLines)) {
- echo "\n" . _WT('Covered lines:') . ' ';
- $this->printFileCoverage($aLines, 1);
- } else {
- echo "\n" . _WT('No code covered.');
- }
- if (in_array(-1, $aLines)) {
- echo "\n" . _WT('Non-covered lines:') . ' ';
- $this->printFileCoverage($aLines, -1);
- }
- // Dead-code handling is tricky. XDebug returns dead code for empty
- // lines with only a "}", often after a return. While it's technically
- // true, it is of no use to us and thus all these lines are removed
- // from our results (if possible). In short, first we remove them,
- // and if there's any dead code remaining, we output it.
- if (in_array(-2, $aLines)) {
- if (is_file($sFilename)) {
- $aFile = file($sFilename, FILE_IGNORE_NEW_LINES);
- foreach ($aLines as $iLine => $iValue)
- if (isset($aFile[$iLine - 1]) && trim($aFile[$iLine - 1]) == '}')
- unset($aLines[$iLine]);
- }
- }
- if (in_array(-2, $aLines)) {
- echo "\n" . _WT('Dead code:') . ' ';
- $this->printFileCoverage($aLines, -2);
- }
- }
- echo "\n";
- }
- /**
- Display information about which lines were executed or not.
- The values for $iDebugOption are:
- * 1: Covered code.
- * -1: Uncovered code.
- * -2: Dead code.
- @param $aLines List of lines with the coverage information associated (covered line, non-covered line or dead line).
- @param $iDebugOption The option indicates the kind of information to display.
- */
- protected function printFileCoverage($aLines, $iDebugOption)
- {
- foreach ($aLines as $iCur => $iCovered) {
- if ($iCovered != $iDebugOption)
- continue;
- if (!isset($iPrev)) {
- $iPrev = $iFirst = $iCur;
- continue;
- }
- if ($iPrev + 1 != $iCur) {
- if ($iFirst != $iPrev)
- echo $iFirst . '-' . $iPrev . ';';
- else
- echo $iFirst . ';';
- $iFirst = $iCur;
- }
- $iPrev = $iCur;
- }
- if (isset($iPrev)) {
- if ($iFirst != $iPrev)
- echo $iFirst . '-' . $iPrev . ';';
- else
- echo $iFirst . ';';
- }
- }
- /**
- Run the test suite.
- */
- public function run()
- {
- foreach ($this->aResults as $sPath => $mResult) {
- try {
- $oTest = new weeUnitTestCase($sPath);
- $fBefore = microtime(true);
- $iMemoryUsed = $oTest->run();
- $this->addResult($sPath, 'success', microtime(true) - $fBefore, $iMemoryUsed);
- if ($oTest->hasExtData())
- $this->aExtData[$sPath] = $oTest->getExtData();
- } catch (SkipTestException $o) {
- $this->addResult($sPath, 'skip');
- } catch (Exception $o) {
- $this->addResult($sPath, $o);
- }
- }
- }
- /**
- Return the results array of the unit test suite.
- It is always available, even when tests have not been run yet.
- @return array Results array for all the unit test cases.
- */
- public function toArray()
- {
- return $this->aResults;
- }
- /**
- Return the results of the unit test suite.
- @return string A report of the unit test suite after its completion.
- */
- public function toString()
- {
- // Analyze and output the code coverage results
- if (defined('WEE_CODE_COVERAGE') && !empty($this->aExtData))
- $this->printCodeCoverage($this->analyzeCodeCoverage());
- // Output the extended data returned by the test cases
- if (!empty($this->aExtData)) {
- echo "\n" . _WT('Extended data:') . "\n\n";
- foreach ($this->aExtData as $sFile => $aTestData) {
- echo $sFile . "\n";
- foreach ($aTestData as $aData) {
- echo ' ' . $aData[0] . ': ';
- if (is_array($aData[1]))
- echo str_replace(array("\n ", "\n"), '', var_export($aData[1], true));
- else
- echo $aData[1];
- echo "\n";
- }
- }
- }
- // Output the tests that took the most time to run
- $s = "\n" . _WT('List of tests with the biggest execution time:');
- arsort($this->aTimeResults);
- $iTotal = count($this->aTimeResults);
- if ($iTotal > 10)
- $iTotal = 10;
- reset($this->aTimeResults);
- for ($i = 1; $i <= $iTotal; $i++) {
- $s .= "\n" . sprintf('%2s', $i) . '- ' . key($this->aTimeResults) . ': ' . round(current($this->aTimeResults), 2) . 's';
- next($this->aTimeResults);
- }
- // Output the tests that used the most memory
- $s .= "\n\n" . _WT('List of tests with the biggest memory consumption at the end of the test:');
- arsort($this->aMemoryResults);
- $iTotal = count($this->aMemoryResults);
- if ($iTotal > 10)
- $iTotal = 10;
- reset($this->aMemoryResults);
- for ($i = 1; $i <= $iTotal; $i++) {
- $s .= "\n" . sprintf('%2s', $i) . '- ' . key($this->aMemoryResults) . ': ' . current($this->aMemoryResults) . ' bytes';
- next($this->aMemoryResults);
- }
- $s .= "\n" . sprintf(_WT('Peak memory usage: %d bytes'), memory_get_peak_usage()) . "\n\n";
- // Count the number of tests failed, succeeded and skipped and output a summary
- // The array contains "skip" and "success" values but also instances of Exception,
- // array_count_values triggers a warning when one of the values in the array cannot
- // be used as a key.
- $aCounts = @array_count_values($this->aResults);
- if (!isset($aCounts['skip']))
- $aCounts['skip'] = 0;
- $iSkippedAndSucceededCount = $aCounts['success'] + $aCounts['skip'];
- $iTestsCount = count($this->aResults);
- if ($iSkippedAndSucceededCount == $iTestsCount)
- $s .= sprintf(_WT('All %d tests succeeded!'), $aCounts['success']);
- else
- $s .= sprintf(_WT('%d of %d tests failed.'), $iTestsCount - $iSkippedAndSucceededCount, $iTestsCount - $aCounts['skip']);
- if ($aCounts['skip'] != 0)
- $s .= ' ' . sprintf(_WT('%d tests have been skipped.'), $aCounts['skip']);
- return $s . "\n";
- }
- }