diff --git a/app/webroot/css/cake.generic.css b/app/webroot/css/cake.generic.css index 4040d00de..89f466853 100644 --- a/app/webroot/css/cake.generic.css +++ b/app/webroot/css/cake.generic.css @@ -415,4 +415,29 @@ div.cake-code-dump pre, div.cake-code-dump pre code { div.cake-code-dump span.code-highlight { background-color: #FFFF00; padding: 4px; +} +span.code-line { + padding-left:20px; + display:block; + margin-left:50px; +} +span.uncovered { + background:#ecc; +} +span.covered { + background:#cec; +} +span.ignored { + color:#aaa; +} +span.line-num { + color:#aaa; + display:block; + float:left; + width:20px; + text-align:right; + margin-right:5px; +} +span.line-num strong { + color:#666; } \ No newline at end of file diff --git a/app/webroot/test.php b/app/webroot/test.php index ca482b010..68c4e8093 100644 --- a/app/webroot/test.php +++ b/app/webroot/test.php @@ -102,6 +102,18 @@ if (!App::import('Vendor', 'simpletest' . DS . 'reporter')) { exit(); } +$analyzeCodeCoverage = false; +if (isset($_GET['code_coverage'])) { + $analyzeCodeCoverage = true; + require_once CAKE_TESTS_LIB . 'code_coverage_manager.php'; + if (!extension_loaded('xdebug')) { + CakePHPTestHeader(); + include CAKE_TESTS_LIB . 'xdebug.php'; + CakePHPTestSuiteFooter(); + exit(); + } +} + CakePHPTestHeader(); CakePHPTestSuiteHeader(); define('RUN_TEST_LINK', $_SERVER['PHP_SELF']); @@ -114,9 +126,20 @@ if (isset($_GET['group'])) { } CakePHPTestRunMore(); } elseif (isset($_GET['case'])) { + + if ($analyzeCodeCoverage) { + CodeCoverageManager::start($_GET['case'], CakeTestsGetReporter()); + } + TestManager::runTestCase($_GET['case'], CakeTestsGetReporter()); + + if ($analyzeCodeCoverage) { + CodeCoverageManager::report(); + } + CakePHPTestRunMore(); -}elseif (isset($_GET['show']) && $_GET['show'] == 'cases') { + CakePHPTestAnalyzeCodeCoverage(); +} elseif (isset($_GET['show']) && $_GET['show'] == 'cases') { CakePHPTestCaseList(); } else { CakePHPTestGroupTestList(); diff --git a/cake/tests/cases/libs/code_coverage_manager.test.php b/cake/tests/cases/libs/code_coverage_manager.test.php new file mode 100644 index 000000000..16b5a63fb --- /dev/null +++ b/cake/tests/cases/libs/code_coverage_manager.test.php @@ -0,0 +1,120 @@ + + * Copyright 2005-2008, Cake Software Foundation, Inc. + * 1785 E. Sahara Avenue, Suite 490-204 + * Las Vegas, Nevada 89104 + * + * Licensed under The Open Group Test Suite License + * Redistributions of files must retain the above copyright notice. + * + * @filesource + * @copyright Copyright 2005-2008, Cake Software Foundation, Inc. + * @link https://trac.cakephp.org/wiki/Developement/TestSuite CakePHP(tm) Tests + * @package cake.tests + * @subpackage cake.tests.cases.libs + * @since CakePHP(tm) v 1.2.0.4206 + * @version $Revision: 6563 $ + * @modifiedby $LastChangedBy: phpnut $ + * @lastmodified $Date: 2008-03-12 22:19:31 +0100 (Wed, 12 Mar 2008) $ + * @license http://www.opensource.org/licenses/opengroup.php The Open Group Test Suite License + */ +App::import('Core', 'CodeCoverageManager'); +/** + * Short description for class. + * + * @package cake.tests + * @subpackage cake.tests.cases.libs + */ +class CodeCoverageManagerTest extends UnitTestCase { +/** + * Test that invalid supplied files will raise an error; test if random library files can be analyzed without errors + * + */ + function testNoTestCaseSupplied() { + CodeCoverageManager::start(substr(md5(microtime()), 0, 5), CakeTestsGetReporter()); + CodeCoverageManager::report(false); + $this->assertError(); + + CodeCoverageManager::start('libs/code_coverage_manager.test.php', CakeTestsGetReporter()); + CodeCoverageManager::report(false); + $this->assertError(); + + App::import('Core', 'Folder'); + $folder = new Folder(); + $folder->cd(ROOT.DS.LIBS); + $contents = $folder->ls(); + function remove($var) { + return ($var != 'code_coverage_manager.test.php'); + } + $contents[1] = array_filter($contents[1], "remove"); + $keys = array_rand($contents[1], 5); + + foreach ($keys as $key) { + CodeCoverageManager::start('libs'.DS.$contents[1][$key], CakeTestsGetReporter()); + CodeCoverageManager::report(false); + $this->assertNoErrors(); + } + } + + function testGetTestObjectFileNameFromTestCaseFile() { + $manager = CodeCoverageManager::getInstance(); + + $expected = $manager->_testObjectFileFromCaseFile('models/some_file.test.php', true); + $this->assertIdentical(APP.'models'.DS.'some_file.php', $expected); + + $expected = $manager->_testObjectFileFromCaseFile('controllers/some_file.test.php', true); + $this->assertIdentical(APP.'controllers'.DS.'some_file.php', $expected); + + $expected = $manager->_testObjectFileFromCaseFile('views/some_file.test.php', true); + $this->assertIdentical(APP.'views'.DS.'some_file.php', $expected); + + $expected = $manager->_testObjectFileFromCaseFile('behaviors/some_file.test.php', true); + $this->assertIdentical(APP.'models'.DS.'behaviors'.DS.'some_file.php', $expected); + + $expected = $manager->_testObjectFileFromCaseFile('components/some_file.test.php', true); + $this->assertIdentical(APP.'controllers'.DS.'components'.DS.'some_file.php', $expected); + + $expected = $manager->_testObjectFileFromCaseFile('helpers/some_file.test.php', true); + $this->assertIdentical(APP.'views'.DS.'helpers'.DS.'some_file.php', $expected); + } + + function testGetExecutableLines() { + $manager = CodeCoverageManager::getInstance(); + $code = <<_getExecutableLines($code); + foreach ($result as $line) { + $this->assertNotIdentical($line, ''); + } + + $code = << + ?> + _getExecutableLines($code); + foreach ($result as $line) { + $this->assertIdentical(trim($line), ''); + } + } +} +?> \ No newline at end of file diff --git a/cake/tests/lib/code_coverage_manager.php b/cake/tests/lib/code_coverage_manager.php new file mode 100644 index 000000000..57a940cbc --- /dev/null +++ b/cake/tests/lib/code_coverage_manager.php @@ -0,0 +1,280 @@ + + * Copyright 2005-2008, Cake Software Foundation, Inc. + * 1785 E. Sahara Avenue, Suite 490-204 + * Las Vegas, Nevada 89104 + * + * Licensed under The Open Group Test Suite License + * Redistributions of files must retain the above copyright notice. + * + * @filesource + * @copyright Copyright 2005-2008, Cake Software Foundation, Inc. + * @link https://trac.cakephp.org/wiki/Developement/TestSuite CakePHP(tm) Tests + * @package cake + * @subpackage cake.cake.tests.lib + * @since CakePHP(tm) v 1.2.0.4433 + * @version $Revision: 6527 $ + * @modifiedby $LastChangedBy: gwoo $ + * @lastmodified $Date: 2008-03-09 05:07:56 +0100 (Sun, 09 Mar 2008) $ + * @license http://www.opensource.org/licenses/opengroup.php The Open Group Test Suite License + */ +App::import('Core', 'Folder'); +/** + * Short description for class. + * + * @package cake + * @subpackage cake.cake.tests.lib + */ +class CodeCoverageManager { +/** + * Is this an app test case? + * + * @var string + */ + var $appTest = false; +/** + * Is this an app test case? + * + * @var string + */ + var $pluginTest = false; +/** + * The test case file to analyze + * + * @var string + */ + var $testCaseFile = ''; +/** + * The currently used CakeTestReporter + * + * @var string + */ + var $reporter = ''; +/** + * Returns a singleton instance + * + * @return object + * @access public + */ + function &getInstance() { + static $instance = array(); + if (!isset($instance[0]) || !$instance[0]) { + $instance[0] =& new CodeCoverageManager(); + } + return $instance[0]; + } +/** + * Starts a new Coverage Analyzation for a given test case file + * @TODO: Works with $_GET now within the function body, which will make it hard when we do code coverage reports for CLI + * + * @param string $testCaseFile + * @param string $reporter + * @return void + */ + function start($testCaseFile, &$reporter) { + $manager =& CodeCoverageManager::getInstance(); + $manager->reporter = $reporter; + + $thisFile = r('.php', '.test.php', basename(__FILE__)); + if (strpos($testCaseFile, $thisFile) !== false) { + trigger_error('Xdebug supports no parallel coverage analysis - so this is not possible.', E_USER_ERROR); + } + + if (isset($_GET['app'])) { + $manager->appTest = true; + } + + if (isset($_GET['plugin'])) { + $manager->pluginTest = true; + } + + $manager->testCaseFile = $testCaseFile; + xdebug_start_code_coverage(XDEBUG_CC_UNUSED | XDEBUG_CC_DEAD_CODE); + } +/** + * Stops the current code coverage analyzation and dumps a nice report depending on the reporter that was passed to start() + * + * @return void + */ + function report($output = true) { + $manager =& CodeCoverageManager::getInstance(); + + $testObjectFile = $manager->_testObjectFileFromCaseFile($manager->testCaseFile, $manager->appTest); + $dump = xdebug_get_code_coverage(); + $coverageData = array(); + foreach ($dump as $file => $data) { + if ($file == $testObjectFile) { + $coverageData = $data; + break; + } + } + + if (empty($coverageData) && $output) { + echo 'The test object file is never loaded.'; + } + + $execCodeLines = $manager->_getExecutableLines(file_get_contents($testObjectFile)); + switch (get_class($manager->reporter)) { + case 'CakeHtmlReporter': + $manager->reportHtml($testObjectFile, $coverageData, $execCodeLines, $output); + break; + default: + trigger_error('Currently only HTML reporting is supported for code coverage analysis.'); + break; + } + } +/** + * Html reporting + * + * @param string $testObjectFile + * @param string $coverageData + * @param string $execCodeLines + * @param string $output + * @return void + */ + function reportHtml($testObjectFile, $coverageData, $execCodeLines, $output) { + if (file_exists($testObjectFile)) { + $file = file($testObjectFile); + + $lineCount = 0; + $coveredCount = 0; + $report = ''; + foreach ($file as $num => $line) { + // start line count at 1 + $num++; + + $foundByManualFinder = trim($execCodeLines[$num]) != ''; + $foundByXdebug = array_key_exists($num, $coverageData); + + // xdebug does not find all executable lines (zend engine fault) + if ($foundByManualFinder && $foundByXdebug) { + $class = 'uncovered'; + $lineCount++; + + if ($coverageData[$num] !== -1 && $coverageData[$num] !== -2) { + $class = 'covered'; + $coveredCount++; + $numExecuted = $coverageData[$num]; + } + } else { + $class = 'ignored'; + } + $report .= ''.$num.''.h($line).''; + } + + $codeCoverage = ($lineCount != 0) + ? round(100*$coveredCount/$lineCount, 2) + : '0.00'; + + if ($output) { + echo '

Code Coverage: '.$codeCoverage.'%

'; + echo '
'.$report.'
'; + } + } + } +/** + * Returns the name of the test object file based on a given test case file name + * + * @param string $file + * @param string $isApp + * @return void + */ + function _testObjectFileFromCaseFile($file, $isApp = true) { + $path = ROOT.DS; + if ($isApp) { + $path .= APP_DIR.DS; + } else { + $path .= CAKE; + } + + $folderPrefixMap = array( + 'behaviors' => 'models', + 'components' => 'controllers', + 'helpers' => 'views' + ); + foreach ($folderPrefixMap as $dir => $prefix) { + if (strpos($file, $dir) === 0) { + $path .= $prefix.DS; + break; + } + } + + $testManager =& new TestManager(); + $testFile = r($testManager->_testExtension, '.php', $file); + + // if this is a file from the test lib, we cannot find the test object file in /cake/libs + // but need to search for it in /cake/test/lib + $folder = new Folder(); + $folder->cd(ROOT.DS.CAKE_TESTS_LIB); + $contents = $folder->ls(); + + if (in_array(basename($testFile), $contents[1])) { + $testFile = basename($testFile); + $path = ROOT.DS.CAKE_TESTS_LIB; + } + $path .= $testFile; + + return $path; + } +/** + * Parses a given code string into an array of lines and replaces every non-executable code line with the needed + * amount of new lines in order for the code line numbers to stay in sync + * + * @param string $content + * @return array array of lines + */ + function _getExecutableLines($content) { + $content = h($content); + + // arrays are 0-indexed, but we want 1-indexed stuff now as we are talking code lines mind you (**) + $content = "\n".$content; + + // strip unwanted lines + $content = preg_replace_callback("/(@codeCoverageIgnoreStart.*?@codeCoverageIgnoreEnd)/is", array('CodeCoverageManager', '_replaceWithNewlines'), $content); + + // strip multiline comments + $content = preg_replace_callback('/\/\\*[\\s\\S]*?\\*\//', array('CodeCoverageManager', '_replaceWithNewlines'), $content); + + // strip singleline comments + $content = preg_replace('/\/\/.*/', '', $content); + + // strip function declarations as xdebug does not count them as covered + $content = preg_replace('/[ |\t]*function[^\n]*\([^\n]*[ |\t]*\{/', '', $content); + $content = preg_replace('/[ |\t]*function[^\n]*\([^\n]*[ |\t]*(\n)+[ |\t]*\{/', '$1', $content); + + // strip php | ?\> tag only lines + $content = preg_replace('/[ |\t]*[ |<\?php|\?>|\t]*/', '', $content); + + // strip var declarations as xdebug does not count them as covered + $content = preg_replace('/[ |\t]*var[ |\t]+\$[\w]+[ |\t]*=[ |\t]*.*?;/', '', $content); + + // strip lines than contain only braces + $content = preg_replace('/[ |\t]*[{|}|\(|\)]+[ |\t]*/', '', $content); + + $result = explode("\n", $content); + + // unset the zero line again to get the original line numbers, but starting at 1, see (**) + unset($result[0]); + + return $result; + } +/** + * Replaces a given arg with the number of newlines in it + * + * @return void + */ + function _replaceWithNewlines() { + $args = func_get_args(); + $numLineBreaks = count(explode("\n", $args[0][0])); + return str_pad('', $numLineBreaks-1, "\n"); + } +} +?> \ No newline at end of file diff --git a/cake/tests/lib/test_manager.php b/cake/tests/lib/test_manager.php index 9d4b42ec5..1056d7d48 100644 --- a/cake/tests/lib/test_manager.php +++ b/cake/tests/lib/test_manager.php @@ -448,6 +448,26 @@ if (function_exists('caketestsgetreporter')) { } } + function CakePHPTestAnalyzeCodeCoverage() { + switch (CAKE_TEST_OUTPUT) { + case CAKE_TEST_OUTPUT_HTML: + if (isset($_GET['case'])) { + $query = '?case='.$_GET['case']; + if (isset($_GET['app'])) { + $query .= '&app=true'; + } + $query .= '&code_coverage=true'; + // else if (isset($_GET['plugin'])) { + // $show = '?show=cases&plugin=' . $_GET['plugin']; + // } else { + // $show = '?show=cases'; + // } + echo "

Analyze Code Coverage

\n"; + } + break; + } + } + function CakePHPTestCaseList() { switch (CAKE_TEST_OUTPUT) { case CAKE_TEST_OUTPUT_HTML: diff --git a/cake/tests/lib/xdebug.php b/cake/tests/lib/xdebug.php new file mode 100644 index 000000000..bbd7319ee --- /dev/null +++ b/cake/tests/lib/xdebug.php @@ -0,0 +1,33 @@ + + * Copyright 2005-2008, Cake Software Foundation, Inc. + * 1785 E. Sahara Avenue, Suite 490-204 + * Las Vegas, Nevada 89104 + * + * Licensed under The Open Group Test Suite License + * Redistributions of files must retain the above copyright notice. + * + * @filesource + * @copyright Copyright 2005-2008, Cake Software Foundation, Inc. + * @link https://trac.cakephp.org/wiki/Developement/TestSuite CakePHP(tm) Tests + * @package cake + * @subpackage cake.cake.tests.libs + * @since CakePHP(tm) v 1.2.0.4433 + * @version $Revision: 6488 $ + * @modifiedby $LastChangedBy: gwoo $ + * @lastmodified $Date: 2008-02-28 16:52:07 +0100 (Thu, 28 Feb 2008) $ + * @license http://www.opensource.org/licenses/opengroup.php The Open Group Test Suite License + */ +?> +
+

Xdebug is not installed

+

You must install Xdebug to use the CakePHP(tm) Code Coverage Analyzation.

+

Learn How To Install Xdebug

\ No newline at end of file