MDL-27148 whitespace fix
[moodle.git] / lib / simpletestcoveragelib.php
blobeb0bd862a5d16b212092a151c535d305f95e6fdc
1 <?php
3 // This file is part of Moodle - http://moodle.org/
4 //
5 // Moodle is free software: you can redistribute it and/or modify
6 // it under the terms of the GNU General Public License as published by
7 // the Free Software Foundation, either version 3 of the License, or
8 // (at your option) any later version.
9 //
10 // Moodle is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 // GNU General Public License for more details.
15 // You should have received a copy of the GNU General Public License
16 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
18 /**
19 * Extend simpletest to support code coverage analysis
21 * This package contains a collection of classes that, extending standard simpletest
22 * ones, provide code coverage analysis to already existing tests. Also there are some
23 * utility functions designed to make the coverage control easier.
25 * @package core
26 * @subpackage simpletestcoverage
27 * @copyright 2003 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
28 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
31 defined('MOODLE_INTERNAL') || die();
33 /**
34 * Includes
36 require_once(dirname(__FILE__) . '/../config.php');
37 require_once($CFG->libdir.'/tablelib.php');
39 require_once($CFG->libdir . '/simpletestlib.php');
40 require_once($CFG->dirroot . '/' . $CFG->admin . '/report/unittest/ex_simple_test.php');
42 require_once($CFG->libdir . '/spikephpcoverage/src/CoverageRecorder.php');
43 require_once($CFG->libdir . '/spikephpcoverage/src/reporter/HtmlCoverageReporter.php');
45 /**
46 * AutoGroupTest class extension supporting code coverage
48 * This class extends AutoGroupTest to add the funcitionalities
49 * necessary to run code coverage, allowing its activation and
50 * specifying included / excluded files to be analysed
52 * @package moodlecore
53 * @subpackage simpletestcoverage
54 * @copyright 2003 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
55 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
57 class autogroup_test_coverage extends AutoGroupTest {
59 private $performcoverage; // boolean
60 private $coveragename; // title of the coverage report
61 private $coveragedir; // dir, relative to dataroot/coverage where the report will be saved
62 private $includecoverage; // paths to be analysed by the coverage report
63 private $excludecoverage; // paths to be excluded from the coverage report
65 function __construct($showsearch, $test_name = null,
66 $performcoverage = false, $coveragename = 'Code Coverage Report',
67 $coveragedir = 'report') {
68 parent::__construct($showsearch, $test_name);
69 $this->performcoverage = $performcoverage;
70 $this->coveragename = $coveragename;
71 $this->coveragedir = $coveragedir;
72 $this->includecoverage = array();
73 $this->excludecoverage = array();
76 public function addTestFile($file, $internalcall = false) {
77 global $CFG;
79 if ($this->performcoverage) {
80 $refinfo = moodle_reflect_file($file);
81 require_once($file);
82 if ($refinfo->classes) {
83 foreach ($refinfo->classes as $class) {
84 $reflection = new ReflectionClass($class);
85 if ($staticprops = $reflection->getStaticProperties()) {
86 if (isset($staticprops['includecoverage']) && is_array($staticprops['includecoverage'])) {
87 foreach ($staticprops['includecoverage'] as $toinclude) {
88 $this->add_coverage_include_path($toinclude);
91 if (isset($staticprops['excludecoverage']) && is_array($staticprops['excludecoverage'])) {
92 foreach ($staticprops['excludecoverage'] as $toexclude) {
93 $this->add_coverage_exclude_path($toexclude);
98 // Automatically add the test dir itself, so nothing will be covered there
99 $this->add_coverage_exclude_path(dirname($file));
102 parent::addTestFile($file, $internalcall);
105 public function add_coverage_include_path($path) {
106 global $CFG;
108 $path = $CFG->dirroot . '/' . $path; // Convert to full path
109 if (!in_array($path, $this->includecoverage)) {
110 array_push($this->includecoverage, $path);
114 public function add_coverage_exclude_path($path) {
115 global $CFG;
117 $path = $CFG->dirroot . '/' . $path; // Convert to full path
118 if (!in_array($path, $this->excludecoverage)) {
119 array_push($this->excludecoverage, $path);
124 * Run the autogroup_test_coverage using one internally defined code coverage reporter
125 * automatically generating the coverage report. Only supports one instrumentation
126 * to be executed and reported.
128 public function run(&$simpletestreporter) {
129 global $CFG;
131 if (moodle_coverage_recorder::can_run_codecoverage() && $this->performcoverage) {
132 // Testing with coverage
133 $covreporter = new moodle_coverage_reporter($this->coveragename, $this->coveragedir);
134 $covrecorder = new moodle_coverage_recorder($covreporter);
135 $covrecorder->setIncludePaths($this->includecoverage);
136 $covrecorder->setExcludePaths($this->excludecoverage);
137 $covrecorder->start_instrumentation();
138 parent::run($simpletestreporter);
139 $covrecorder->stop_instrumentation();
140 $covrecorder->generate_report();
141 moodle_coverage_reporter::print_summary_info(basename($this->coveragedir));
142 } else {
143 // Testing without coverage
144 parent::run($simpletestreporter);
149 * Run the autogroup_test_coverage tests using one externally defined code coverage reporter
150 * allowing further process of coverage data once tests are over. Supports multiple
151 * instrumentations (code coverage gathering sessions) to be executed.
153 public function run_with_external_coverage(&$simpletestreporter, &$covrecorder) {
155 if (moodle_coverage_recorder::can_run_codecoverage() && $this->performcoverage) {
156 $covrecorder->setIncludePaths($this->includecoverage);
157 $covrecorder->setExcludePaths($this->excludecoverage);
158 $covrecorder->start_instrumentation();
159 parent::run($simpletestreporter);
160 $covrecorder->stop_instrumentation();
161 } else {
162 // Testing without coverage
163 parent::run($simpletestreporter);
169 * CoverageRecorder class extension supporting multiple
170 * coverage instrumentations to be accumulated
172 * This class extends CoverageRecorder class in order to
173 * support multimple xdebug code coverage sessions to be
174 * executed and get acummulated info about all them in order
175 * to produce one unique report (default CoverageRecorder
176 * resets info on each instrumentation (coverage session)
178 * @package moodlecore
179 * @subpackage simpletestcoverage
180 * @copyright 2003 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
181 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
183 class moodle_coverage_recorder extends CoverageRecorder {
185 public function __construct($reporter='new moodle_coverage_reporter()') {
186 parent::__construct(array(), array(), $reporter);
190 * Stop gathering coverage data, saving it for later reporting
192 public function stop_instrumentation() {
193 if(extension_loaded("xdebug")) {
194 $lastcoveragedata = xdebug_get_code_coverage(); // Get last instrumentation coverage data
195 xdebug_stop_code_coverage(); // Stop code coverage
196 $this->coverageData = self::merge_coverage_data($this->coverageData, $lastcoveragedata); // Append lastcoveragedata
197 $this->logger->debug("[moodle_coverage_recorder::stopInstrumentation()] Code coverage: " . print_r($this->coverageData, true),
198 __FILE__, __LINE__);
199 return true;
200 } else {
201 $this->logger->critical("[moodle_coverage_recorder::stopInstrumentation()] Xdebug not loaded.", __FILE__, __LINE__);
203 return false;
207 * Start gathering coverage data
209 public function start_instrumentation() {
210 $this->startInstrumentation(); /// Simple lowercase wrap over Spike function
214 * Generate the code coverage report
216 public function generate_report() {
217 $this->generateReport(); /// Simple lowercase wrap over Spike function
221 * Determines if the server is able to run code coverage analysis
223 * @return bool
225 static public function can_run_codecoverage() {
226 // Only req is xdebug loaded. PEAR XML is already in place and available
227 if(!extension_loaded("xdebug")) {
228 return false;
230 return true;
234 * Merge two collections of complete code coverage data
236 protected static function merge_coverage_data($cov1, $cov2) {
238 $result = array();
240 // protection against empty coverage collections
241 if (!is_array($cov1)) {
242 $cov1 = array();
244 if (!is_array($cov2)) {
245 $cov2 = array();
248 // Get all the files used in both coverage datas
249 $files = array_unique(array_merge(array_keys($cov1), array_keys($cov2)));
251 // Iterate, getting results
252 foreach($files as $file) {
253 // If file exists in both coverages, let's merge their lines
254 if (array_key_exists($file, $cov1) && array_key_exists($file, $cov2)) {
255 $result[$file] = self::merge_lines_coverage_data($cov1[$file], $cov2[$file]);
256 // Only one of the coverages has the file
257 } else if (array_key_exists($file, $cov1)) {
258 $result[$file] = $cov1[$file];
259 } else {
260 $result[$file] = $cov2[$file];
263 return $result;
267 * Merge two collections of lines of code coverage data belonging to the same file
269 * Merge algorithm obtained from Phing: http://phing.info
271 protected static function merge_lines_coverage_data($lines1, $lines2) {
273 $result = array();
275 reset($lines1);
276 reset($lines2);
278 while (current($lines1) && current($lines2)) {
279 $linenr1 = key($lines1);
280 $linenr2 = key($lines2);
282 if ($linenr1 < $linenr2) {
283 $result[$linenr1] = current($lines1);
284 next($lines1);
285 } else if ($linenr2 < $linenr1) {
286 $result[$linenr2] = current($lines2);
287 next($lines2);
288 } else {
289 if (current($lines1) < 0) {
290 $result[$linenr2] = current($lines2);
291 } else if (current($lines2) < 0) {
292 $result[$linenr2] = current($lines1);
293 } else {
294 $result[$linenr2] = current($lines1) + current($lines2);
296 next($lines1);
297 next($lines2);
301 while (current($lines1)) {
302 $result[key($lines1)] = current($lines1);
303 next($lines1);
306 while (current($lines2)) {
307 $result[key($lines2)] = current($lines2);
308 next($lines2);
311 return $result;
316 * HtmlCoverageReporter class extension supporting Moodle customizations
318 * This class extends the HtmlCoverageReporter class in order to
319 * implement Moodle look and feel, inline reporting after executing
320 * unit tests, proper linking and other tweaks here and there.
322 * @package moodlecore
323 * @subpackage simpletestcoverage
324 * @copyright 2003 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
325 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
327 class moodle_coverage_reporter extends HtmlCoverageReporter {
329 public function __construct($heading='Coverage Report', $dir='report') {
330 global $CFG;
331 parent::__construct($heading, '', $CFG->dataroot . '/codecoverage/' . $dir);
335 * Writes one row in the index.html table to display filename
336 * and coverage recording.
338 * Overrided to transform names and links to shorter format
340 * @param $fileLink link to html details file.
341 * @param $realFile path to real PHP file.
342 * @param $fileCoverage Coverage recording for that file.
343 * @return string HTML code for a single row.
344 * @access protected
346 protected function writeIndexFileTableRow($fileLink, $realFile, $fileCoverage) {
348 global $CFG;
350 $fileLink = str_replace($CFG->dirroot, '', $fileLink);
351 $realFile = str_replace($CFG->dirroot, '', $realFile);
353 return parent::writeIndexFileTableRow($fileLink, $realFile, $fileCoverage);;
357 * Mark a source code file based on the coverage data gathered
359 * Overrided to transform names and links to shorter format
361 * @param $phpFile Name of the actual source file
362 * @param $fileLink Link to the html mark-up file for the $phpFile
363 * @param &$coverageLines Coverage recording for $phpFile
364 * @return boolean FALSE on failure
365 * @access protected
367 protected function markFile($phpFile, $fileLink, &$coverageLines) {
368 global $CFG;
370 $fileLink = str_replace($CFG->dirroot, '', $fileLink);
372 return parent::markFile($phpFile, $fileLink, $coverageLines);
377 * Update the grand totals
379 * Overrided to avoid the call to recordFileCoverageInfo()
380 * because it has been already executed by writeIndexFile() and
381 * cause files to be duplicated in the fileCoverage property
383 protected function updateGrandTotals(&$coverageCounts) {
384 $this->grandTotalLines += $coverageCounts['total'];
385 $this->grandTotalCoveredLines += $coverageCounts['covered'];
386 $this->grandTotalUncoveredLines += $coverageCounts['uncovered'];
390 * Generate the static report
392 * Overrided to generate the serialised object to be displayed inline
393 * with the test results.
395 * @param &$data Reference to Coverage Data
397 public function generateReport(&$data) {
398 parent::generateReport($data);
400 // head data
401 $data = new stdClass();
402 $data->time = time();
403 $data->title = $this->heading;
404 $data->output = $this->outputDir;
406 // summary data
407 $data->totalfiles = $this->grandTotalFiles;
408 $data->totalln = $this->grandTotalLines;
409 $data->totalcoveredln = $this->grandTotalCoveredLines;
410 $data->totaluncoveredln = $this->grandTotalUncoveredLines;
411 $data->totalpercentage = $this->getGrandCodeCoveragePercentage();
413 // file details data
414 $data->coveragedetails = $this->fileCoverage;
416 // save serialised object
417 file_put_contents($data->output . '/codecoverage.ser', serialize($data));
421 * Return the html contents for the summary for the last execution of the
422 * given test type
424 * @param string $type of the test to return last execution summary (dbtest|unittest)
425 * @return string html contents of the summary
427 static public function get_summary_info($type) {
428 global $CFG, $OUTPUT;
430 $serfilepath = $CFG->dataroot . '/codecoverage/' . $type . '/codecoverage.ser';
431 if (file_exists($serfilepath) && is_readable($serfilepath)) {
432 if ($data = unserialize(file_get_contents($serfilepath))) {
433 // return one table with all the totals (we avoid individual file results here)
434 $result = '';
435 $table = new html_table();
436 $table->align = array('right', 'left');
437 $table->tablealign = 'center';
438 $table->attributes['class'] = 'codecoveragetable';
439 $table->id = 'codecoveragetable_' . $type;
440 $table->rowclasses = array('label', 'value');
441 $table->data = array(
442 array(get_string('date') , userdate($data->time)),
443 array(get_string('files') , format_float($data->totalfiles, 0)),
444 array(get_string('totallines', 'simpletest') , format_float($data->totalln, 0)),
445 array(get_string('executablelines', 'simpletest') , format_float($data->totalcoveredln + $data->totaluncoveredln, 0)),
446 array(get_string('coveredlines', 'simpletest') , format_float($data->totalcoveredln, 0)),
447 array(get_string('uncoveredlines', 'simpletest') , format_float($data->totaluncoveredln, 0)),
448 array(get_string('coveredpercentage', 'simpletest'), format_float($data->totalpercentage, 2) . '%')
451 $url = $CFG->wwwroot . '/admin/report/unittest/coveragefile.php/' . $type . '/index.html';
452 $result .= $OUTPUT->heading($data->title, 3, 'main codecoverageheading');
453 $result .= $OUTPUT->heading('<a href="' . $url . '" onclick="javascript:window.open(' . "'" . $url . "'" . ');return false;"' .
454 ' title="">' . get_string('codecoveragecompletereport', 'simpletest') . '</a>', 4, 'main codecoveragelink');
455 $result .= html_writer::table($table);
457 return $OUTPUT->box($result, 'generalbox boxwidthwide boxaligncenter codecoveragebox', '', true);
460 return false;
464 * Print the html contents for the summary for the last execution of the
465 * given test type
467 * @param string $type of the test to return last execution summary (dbtest|unittest)
468 * @return string html contents of the summary
470 static public function print_summary_info($type) {
471 echo self::get_summary_info($type);
475 * Return the html code needed to browse latest code coverage complete report of the
476 * given test type
478 * @param string $type of the test to return last execution summary (dbtest|unittest)
479 * @return string html contents of the summary
481 static public function get_link_to_latest($type) {
482 global $CFG, $OUTPUT;
484 $serfilepath = $CFG->dataroot . '/codecoverage/' . $type . '/codecoverage.ser';
485 if (file_exists($serfilepath) && is_readable($serfilepath)) {
486 if ($data = unserialize(file_get_contents($serfilepath))) {
487 $info = new stdClass();
488 $info->date = userdate($data->time);
489 $info->files = format_float($data->totalfiles, 0);
490 $info->percentage = format_float($data->totalpercentage, 2) . '%';
492 $strlatestreport = get_string('codecoveragelatestreport', 'simpletest');
493 $strlatestdetails = get_string('codecoveragelatestdetails', 'simpletest', $info);
495 // return one link to latest complete report
496 $result = '';
497 $url = $CFG->wwwroot . '/admin/report/unittest/coveragefile.php/' . $type . '/index.html';
498 $result .= $OUTPUT->heading('<a href="' . $url . '" onclick="javascript:window.open(' . "'" . $url . "'" . ');return false;"' .
499 ' title="">' . $strlatestreport . '</a>', 3, 'main codecoveragelink');
500 $result .= $OUTPUT->heading($strlatestdetails, 4, 'main codecoveragedetails');
501 return $OUTPUT->box($result, 'generalbox boxwidthwide boxaligncenter codecoveragebox', '', true);
504 return false;
508 * Print the html code needed to browse latest code coverage complete report of the
509 * given test type
511 * @param string $type of the test to return last execution summary (dbtest|unittest)
512 * @return string html contents of the summary
514 static public function print_link_to_latest($type) {
515 echo self::get_link_to_latest($type);
521 * Return information about classes and functions
523 * This function will parse any PHP file, extracting information about the
524 * classes and functions defined within it, providing "File Reflection" as
525 * PHP standard reflection classes don't support that.
527 * The idea and the code has been obtained from the Zend Framework Reflection API
528 * http://framework.zend.com/manual/en/zend.reflection.reference.html
530 * Usage: $ref_file = moodle_reflect_file($file);
532 * @param string $file full path to the php file to introspect
533 * @return object object with both 'classes' and 'functions' properties
535 function moodle_reflect_file($file) {
537 $contents = file_get_contents($file);
538 $tokens = token_get_all($contents);
540 $functionTrapped = false;
541 $classTrapped = false;
542 $openBraces = 0;
544 $classes = array();
545 $functions = array();
547 foreach ($tokens as $token) {
549 * Tokens are characters representing symbols or arrays
550 * representing strings. The keys/values in the arrays are
552 * - 0 => token id,
553 * - 1 => string,
554 * - 2 => line number
556 * Token ID's are explained here:
557 * http://www.php.net/manual/en/tokens.php.
560 if (is_array($token)) {
561 $type = $token[0];
562 $value = $token[1];
563 $lineNum = $token[2];
564 } else {
565 // It's a symbol
566 // Maintain the count of open braces
567 if ($token == '{') {
568 $openBraces++;
569 } else if ($token == '}') {
570 $openBraces--;
573 continue;
576 switch ($type) {
577 // Name of something
578 case T_STRING:
579 if ($functionTrapped) {
580 $functions[] = $value;
581 $functionTrapped = false;
582 } elseif ($classTrapped) {
583 $classes[] = $value;
584 $classTrapped = false;
586 continue;
588 // Functions
589 case T_FUNCTION:
590 if ($openBraces == 0) {
591 $functionTrapped = true;
593 break;
595 // Classes
596 case T_CLASS:
597 $classTrapped = true;
598 break;
600 // Default case: do nothing
601 default:
602 break;
606 return (object)array('classes' => $classes, 'functions' => $functions);