]> git.mjollnir.org Git - moodle.git/commitdiff
MDL-19579 code coverage - add library supporting all the extensions both to simpletes...
authorstronk7 <stronk7>
Tue, 23 Jun 2009 09:09:54 +0000 (09:09 +0000)
committerstronk7 <stronk7>
Tue, 23 Jun 2009 09:09:54 +0000 (09:09 +0000)
lib/simpletestcoveragelib.php [new file with mode: 0644]

diff --git a/lib/simpletestcoveragelib.php b/lib/simpletestcoveragelib.php
new file mode 100644 (file)
index 0000000..7d034e0
--- /dev/null
@@ -0,0 +1,427 @@
+<?php
+
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle 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 General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * 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);
+}
+
+?>