428 lines
15 KiB
PHP
428 lines
15 KiB
PHP
<?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);
|
|
}
|
|
|
|
?>
|