From 86cc669c1b1898fb534e3e91778caeda522c4afe Mon Sep 17 00:00:00 2001 From: stronk7 Date: Tue, 23 Jun 2009 09:09:54 +0000 Subject: [PATCH] MDL-19579 code coverage - add library supporting all the extensions both to simpletest and spikephpcoverage required to have code coverage available --- lib/simpletestcoveragelib.php | 427 ++++++++++++++++++++++++++++++++++ 1 file changed, 427 insertions(+) create mode 100644 lib/simpletestcoveragelib.php diff --git a/lib/simpletestcoveragelib.php b/lib/simpletestcoveragelib.php new file mode 100644 index 0000000000..7d034e0319 --- /dev/null +++ b/lib/simpletestcoveragelib.php @@ -0,0 +1,427 @@ +. + +/** + * Extend simpletest to support code coverage analysis + * + * This package contains a collection of classes that, extending standard simpletest + * ones, provide code coverage analysis to already existing tests. Also there are some + * utility functions designed to make the coverage control easier. + * + * @package moodlecore + * @subpackage simpletestcoverage + * @copyright 2003 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/// TODO: implement safe and simple file server to dataroot/coverage +/// TODO: serialize summary in reporter (to render it inline) +/// TODO: provide one helper function to show links from test page to coverage report +/// TODO: use dirroot-based names/links in the report + +/** + * Includes + */ +require_once(dirname(__FILE__) . '/../config.php'); +require_once($CFG->libdir.'/tablelib.php'); + +require_once($CFG->libdir . '/simpletestlib.php'); +require_once($CFG->dirroot . '/' . $CFG->admin . '/report/unittest/ex_simple_test.php'); + +require_once($CFG->libdir . '/spikephpcoverage/src/CoverageRecorder.php'); +require_once($CFG->libdir . '/spikephpcoverage/src/reporter/HtmlCoverageReporter.php'); + +/** + * AutoGroupTest class extension supporting code coverage + * + * This class extends AutoGroupTest to add the funcitionalities + * necessary to run code coverage, allowing its activation and + * specifying included / excluded files to be analysed + * + * @package moodlecore + * @subpackage simpletestcoverage + * @copyright 2003 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class autogroup_test_coverage extends AutoGroupTest { + + private $performcoverage; // boolean + private $coveragename; // title of the coverage report + private $coveragedir; // dir, relative to dataroot/coverage where the report will be saved + private $includecoverage; // paths to be analysed by the coverage report + private $excludecoverage; // paths to be excluded from the coverage report + + function __construct($showsearch, $test_name = null, + $performcoverage = false, $coveragename = 'Code Coverage Report', + $coveragedir = 'report') { + parent::__construct($showsearch, $test_name); + $this->performcoverage = $performcoverage; + $this->coveragename = $coveragename; + $this->coveragedir = $coveragedir; + $this->includecoverage = array(); + $this->excludecoverage = array(); + } + + public function addTestFile($file, $internalcall = false) { + global $CFG; + + if ($this->performcoverage) { + $refinfo = moodle_reflect_file($file); + require_once($file); + if ($refinfo->classes) { + foreach ($refinfo->classes as $class) { + $reflection = new ReflectionClass($class); + if ($staticprops = $reflection->getStaticProperties()) { + if (isset($staticprops['includecoverage']) && is_array($staticprops['includecoverage'])) { + foreach ($staticprops['includecoverage'] as $toinclude) { + $this->add_coverage_include_path($toinclude); + } + } + if (isset($staticprops['excludecoverage']) && is_array($staticprops['excludecoverage'])) { + foreach ($staticprops['excludecoverage'] as $toexclude) { + $this->add_coverage_exclude_path($toexclude); + } + } + } + } + // Automatically add the test dir itself, so nothing will be covered there + $this->add_coverage_exclude_path(dirname($file)); + } + } + parent::addTestFile($file, $internalcall); + } + + public function add_coverage_include_path($path) { + global $CFG; + + $path = $CFG->dirroot . '/' . $path; // Convert to full path + if (!in_array($path, $this->includecoverage)) { + array_push($this->includecoverage, $path); + } + } + + public function add_coverage_exclude_path($path) { + global $CFG; + + $path = $CFG->dirroot . '/' . $path; // Convert to full path + if (!in_array($path, $this->excludecoverage)) { + array_push($this->excludecoverage, $path); + } + } + + /** + * Run the autogroup_test_coverage using one internally defined code coverage reporter + * automatically generating the coverage report. Only supports one instrumentation + * to be executed and reported. + */ + public function run(&$simpletestreporter) { + global $CFG; + + if (moodle_coverage_recorder::can_run_codecoverage() && $this->performcoverage) { + // Testing with coverage + $covreporter = new moodle_coverage_reporter($this->coveragename, $this->coveragedir); + $covrecorder = new moodle_coverage_recorder($covreporter); + $covrecorder->setIncludePaths($this->includecoverage); + $covrecorder->setExcludePaths($this->excludecoverage); + $covrecorder->start_instrumentation(); + parent::run($simpletestreporter); + $covrecorder->stop_instrumentation(); + $covrecorder->generate_report(); + } else { + // Testing without coverage + parent::run($simpletestreporter); + } + } + + /** + * Run the autogroup_test_coverage tests using one externally defined code coverage reporter + * allowing further process of coverage data once tests are over. Supports multiple + * instrumentations (code coverage gathering sessions) to be executed. + */ + public function run_with_external_coverage(&$simpletestreporter, &$covrecorder) { + + if (moodle_coverage_recorder::can_run_codecoverage() && $this->performcoverage) { + $covrecorder->setIncludePaths($this->includecoverage); + $covrecorder->setExcludePaths($this->excludecoverage); + $covrecorder->start_instrumentation(); + parent::run($simpletestreporter); + $covrecorder->stop_instrumentation(); + } else { + // Testing without coverage + parent::run($simpletestreporter); + } + } +} + +/** + * CoverageRecorder class extension supporting multiple + * coverage instrumentations to be accumulated + * + * This class extends CoverageRecorder class in order to + * support multimple xdebug code coverage sessions to be + * executed and get acummulated info about all them in order + * to produce one unique report (default CoverageRecorder + * resets info on each instrumentation (coverage session) + * + * @package moodlecore + * @subpackage simpletestcoverage + * @copyright 2003 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class moodle_coverage_recorder extends CoverageRecorder { + + public function __construct($reporter='new moodle_coverage_reporter()') { + parent::__construct(array(), array(), $reporter); + } + + /** + * Stop gathering coverage data, saving it for later reporting + */ + public function stop_instrumentation() { + if(extension_loaded("xdebug")) { + $lastcoveragedata = xdebug_get_code_coverage(); // Get last instrumentation coverage data + xdebug_stop_code_coverage(); // Stop code coverage + $this->coverageData = self::merge_coverage_data($this->coverageData, $lastcoveragedata); // Append lastcoveragedata + $this->logger->debug("[moodle_coverage_recorder::stopInstrumentation()] Code coverage: " . print_r($this->coverageData, true), + __FILE__, __LINE__); + return true; + } else { + $this->logger->critical("[moodle_coverage_recorder::stopInstrumentation()] Xdebug not loaded.", __FILE__, __LINE__); + } + return false; + } + + /** + * Start gathering coverage data + */ + public function start_instrumentation() { + $this->startInstrumentation(); /// Simple lowercase wrap over Spike function + } + + /** + * Generate the code coverage report + */ + public function generate_report() { + $this->generateReport(); /// Simple lowercase wrap over Spike function + } + + /** + * Determines if the server is able to run code coverage analysis + * + * @return bool + */ + static public function can_run_codecoverage() { + // Only req is xdebug loaded. PEAR XML is already in place and available + if(!extension_loaded("xdebug")) { + return false; + } + return true; + } + + /** + * Merge two collections of complete code coverage data + */ + protected static function merge_coverage_data($cov1, $cov2) { + + $result = array(); + + // protection against empty coverage collections + if (!is_array($cov1)) { + $cov1 = array(); + } + if (!is_array($cov2)) { + $cov2 = array(); + } + + // Get all the files used in both coverage datas + $files = array_unique(array_merge(array_keys($cov1), array_keys($cov2))); + + // Iterate, getting results + foreach($files as $file) { + // If file exists in both coverages, let's merge their lines + if (array_key_exists($file, $cov1) && array_key_exists($file, $cov2)) { + $result[$file] = self::merge_lines_coverage_data($cov1[$file], $cov2[$file]); + // Only one of the coverages has the file + } else if (array_key_exists($file, $cov1)) { + $result[$file] = $cov1[$file]; + } else { + $result[$file] = $cov2[$file]; + } + } + return $result; + } + + /** + * Merge two collections of lines of code coverage data belonging to the same file + * + * Merge algorithm obtained from Phing: http://phing.info + */ + protected static function merge_lines_coverage_data($lines1, $lines2) { + + $result = array(); + + reset($lines1); + reset($lines2); + + while (current($lines1) && current($lines2)) { + $linenr1 = key($lines1); + $linenr2 = key($lines2); + + if ($linenr1 < $linenr2) { + $result[$linenr1] = current($lines1); + next($lines1); + } else if ($linenr2 < $linenr1) { + $result[$linenr2] = current($lines2); + next($lines2); + } else { + if (current($lines1) < 0) { + $result[$linenr2] = current($lines2); + } else if (current($lines2) < 0) { + $result[$linenr2] = current($lines1); + } else { + $result[$linenr2] = current($lines1) + current($lines2); + } + next($lines1); + next($lines2); + } + } + + while (current($lines1)) { + $result[key($lines1)] = current($lines1); + next($lines1); + } + + while (current($lines2)) { + $result[key($lines2)] = current($lines2); + next($lines2); + } + + return $result; + } +} + +/** + * HtmlCoverageReporter class extension supporting Moodle customizations + * + * This class extends the HtmlCoverageReporter class in order to + * implement Moodle look and feel, inline reporting after executing + * unit tests, proper linking and other tweaks here and there. + * + * @package moodlecore + * @subpackage simpletestcoverage + * @copyright 2003 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class moodle_coverage_reporter extends HtmlCoverageReporter { + + public function __construct($heading='Coverage Report', $dir='report') { + global $CFG; + parent::__construct($heading, '', $CFG->dataroot . '/codecoverage/' . $dir); + } +} + + +/** + * Return information about classes and functions + * + * This function will parse any PHP file, extracting information about the + * classes and functions defined within it, providing "File Reflection" as + * PHP standard reflection classes don't support that. + * + * The idea and the code has been obtained from the Zend Framework Reflection API + * http://framework.zend.com/manual/en/zend.reflection.reference.html + * + * Usage: $ref_file = moodle_reflect_file($file); + * + * @param string $file full path to the php file to introspect + * @return object object with both 'classes' and 'functions' properties + */ +function moodle_reflect_file($file) { + + $contents = file_get_contents($file); + $tokens = token_get_all($contents); + + $functionTrapped = false; + $classTrapped = false; + $openBraces = 0; + + $classes = array(); + $functions = array(); + + foreach ($tokens as $token) { + /* + * Tokens are characters representing symbols or arrays + * representing strings. The keys/values in the arrays are + * + * - 0 => token id, + * - 1 => string, + * - 2 => line number + * + * Token ID's are explained here: + * http://www.php.net/manual/en/tokens.php. + */ + + if (is_array($token)) { + $type = $token[0]; + $value = $token[1]; + $lineNum = $token[2]; + } else { + // It's a symbol + // Maintain the count of open braces + if ($token == '{') { + $openBraces++; + } else if ($token == '}') { + $openBraces--; + } + + continue; + } + + switch ($type) { + // Name of something + case T_STRING: + if ($functionTrapped) { + $functions[] = $value; + $functionTrapped = false; + } elseif ($classTrapped) { + $classes[] = $value; + $classTrapped = false; + } + continue; + + // Functions + case T_FUNCTION: + if ($openBraces == 0) { + $functionTrapped = true; + } + break; + + // Classes + case T_CLASS: + $classTrapped = true; + break; + + // Default case: do nothing + default: + break; + } + } + + return (object)array('classes' => $classes, 'functions' => $functions); +} + +?> -- 2.39.5