MDL-19579 code coverage - add library supporting all the extensions both to simpletest and spikephpcoverage required to have code coverage available
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
?>
|
||||
Reference in New Issue
Block a user