|
Moodle
2.2.1
http://www.collinsharper.com
|
00001 <?php 00002 // This file is part of Moodle - http://moodle.org/ 00003 // 00004 // Moodle is free software: you can redistribute it and/or modify 00005 // it under the terms of the GNU General Public License as published by 00006 // the Free Software Foundation, either version 3 of the License, or 00007 // (at your option) any later version. 00008 // 00009 // Moodle is distributed in the hope that it will be useful, 00010 // but WITHOUT ANY WARRANTY; without even the implied warranty of 00011 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 00012 // GNU General Public License for more details. 00013 // 00014 // You should have received a copy of the GNU General Public License 00015 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 00016 00030 defined('MOODLE_INTERNAL') || die(); 00031 00035 require_once($CFG->libdir.'/tablelib.php'); 00036 00037 require_once($CFG->dirroot . '/' . $CFG->admin . '/tool/unittest/simpletestlib.php'); 00038 require_once($CFG->dirroot . '/' . $CFG->admin . '/tool/unittest/ex_simple_test.php'); 00039 00040 require_once($CFG->libdir . '/spikephpcoverage/src/CoverageRecorder.php'); 00041 require_once($CFG->libdir . '/spikephpcoverage/src/reporter/HtmlCoverageReporter.php'); 00042 00055 class autogroup_test_coverage extends AutoGroupTest { 00056 00057 private $performcoverage; // boolean 00058 private $coveragename; // title of the coverage report 00059 private $coveragedir; // dir, relative to dataroot/coverage where the report will be saved 00060 private $includecoverage; // paths to be analysed by the coverage report 00061 private $excludecoverage; // paths to be excluded from the coverage report 00062 00063 function __construct($showsearch, $test_name = null, 00064 $performcoverage = false, $coveragename = 'Code Coverage Report', 00065 $coveragedir = 'report') { 00066 parent::__construct($showsearch, $test_name); 00067 $this->performcoverage = $performcoverage; 00068 $this->coveragename = $coveragename; 00069 $this->coveragedir = $coveragedir; 00070 $this->includecoverage = array(); 00071 $this->excludecoverage = array(); 00072 } 00073 00074 public function addTestFile($file, $internalcall = false) { 00075 global $CFG; 00076 00077 if ($this->performcoverage) { 00078 $refinfo = moodle_reflect_file($file); 00079 require_once($file); 00080 if ($refinfo->classes) { 00081 foreach ($refinfo->classes as $class) { 00082 $reflection = new ReflectionClass($class); 00083 if ($staticprops = $reflection->getStaticProperties()) { 00084 if (isset($staticprops['includecoverage']) && is_array($staticprops['includecoverage'])) { 00085 foreach ($staticprops['includecoverage'] as $toinclude) { 00086 $this->add_coverage_include_path($toinclude); 00087 } 00088 } 00089 if (isset($staticprops['excludecoverage']) && is_array($staticprops['excludecoverage'])) { 00090 foreach ($staticprops['excludecoverage'] as $toexclude) { 00091 $this->add_coverage_exclude_path($toexclude); 00092 } 00093 } 00094 } 00095 } 00096 // Automatically add the test dir itself, so nothing will be covered there 00097 $this->add_coverage_exclude_path(dirname($file)); 00098 } 00099 } 00100 parent::addTestFile($file, $internalcall); 00101 } 00102 00103 public function add_coverage_include_path($path) { 00104 global $CFG; 00105 00106 $path = $CFG->dirroot . '/' . $path; // Convert to full path 00107 if (!in_array($path, $this->includecoverage)) { 00108 array_push($this->includecoverage, $path); 00109 } 00110 } 00111 00112 public function add_coverage_exclude_path($path) { 00113 global $CFG; 00114 00115 $path = $CFG->dirroot . '/' . $path; // Convert to full path 00116 if (!in_array($path, $this->excludecoverage)) { 00117 array_push($this->excludecoverage, $path); 00118 } 00119 } 00120 00126 public function run(&$simpletestreporter) { 00127 global $CFG; 00128 00129 if (moodle_coverage_recorder::can_run_codecoverage() && $this->performcoverage) { 00130 // Testing with coverage 00131 $covreporter = new moodle_coverage_reporter($this->coveragename, $this->coveragedir); 00132 $covrecorder = new moodle_coverage_recorder($covreporter); 00133 $covrecorder->setIncludePaths($this->includecoverage); 00134 $covrecorder->setExcludePaths($this->excludecoverage); 00135 $covrecorder->start_instrumentation(); 00136 parent::run($simpletestreporter); 00137 $covrecorder->stop_instrumentation(); 00138 $covrecorder->generate_report(); 00139 moodle_coverage_reporter::print_summary_info(basename($this->coveragedir)); 00140 } else { 00141 // Testing without coverage 00142 parent::run($simpletestreporter); 00143 } 00144 } 00145 00151 public function run_with_external_coverage(&$simpletestreporter, &$covrecorder) { 00152 00153 if (moodle_coverage_recorder::can_run_codecoverage() && $this->performcoverage) { 00154 $covrecorder->setIncludePaths($this->includecoverage); 00155 $covrecorder->setExcludePaths($this->excludecoverage); 00156 $covrecorder->start_instrumentation(); 00157 parent::run($simpletestreporter); 00158 $covrecorder->stop_instrumentation(); 00159 } else { 00160 // Testing without coverage 00161 parent::run($simpletestreporter); 00162 } 00163 } 00164 } 00165 00181 class moodle_coverage_recorder extends CoverageRecorder { 00182 00183 public function __construct($reporter='new moodle_coverage_reporter()') { 00184 parent::__construct(array(), array(), $reporter); 00185 } 00186 00190 public function stop_instrumentation() { 00191 if(extension_loaded("xdebug")) { 00192 $lastcoveragedata = xdebug_get_code_coverage(); // Get last instrumentation coverage data 00193 xdebug_stop_code_coverage(); // Stop code coverage 00194 $this->coverageData = self::merge_coverage_data($this->coverageData, $lastcoveragedata); // Append lastcoveragedata 00195 $this->logger->debug("[moodle_coverage_recorder::stopInstrumentation()] Code coverage: " . print_r($this->coverageData, true), 00196 __FILE__, __LINE__); 00197 return true; 00198 } else { 00199 $this->logger->critical("[moodle_coverage_recorder::stopInstrumentation()] Xdebug not loaded.", __FILE__, __LINE__); 00200 } 00201 return false; 00202 } 00203 00207 public function start_instrumentation() { 00208 $this->startInstrumentation(); 00209 } 00210 00214 public function generate_report() { 00215 $this->generateReport(); 00216 } 00217 00223 static public function can_run_codecoverage() { 00224 // Only req is xdebug loaded. PEAR XML is already in place and available 00225 if(!extension_loaded("xdebug")) { 00226 return false; 00227 } 00228 return true; 00229 } 00230 00234 protected static function merge_coverage_data($cov1, $cov2) { 00235 00236 $result = array(); 00237 00238 // protection against empty coverage collections 00239 if (!is_array($cov1)) { 00240 $cov1 = array(); 00241 } 00242 if (!is_array($cov2)) { 00243 $cov2 = array(); 00244 } 00245 00246 // Get all the files used in both coverage datas 00247 $files = array_unique(array_merge(array_keys($cov1), array_keys($cov2))); 00248 00249 // Iterate, getting results 00250 foreach($files as $file) { 00251 // If file exists in both coverages, let's merge their lines 00252 if (array_key_exists($file, $cov1) && array_key_exists($file, $cov2)) { 00253 $result[$file] = self::merge_lines_coverage_data($cov1[$file], $cov2[$file]); 00254 // Only one of the coverages has the file 00255 } else if (array_key_exists($file, $cov1)) { 00256 $result[$file] = $cov1[$file]; 00257 } else { 00258 $result[$file] = $cov2[$file]; 00259 } 00260 } 00261 return $result; 00262 } 00263 00269 protected static function merge_lines_coverage_data($lines1, $lines2) { 00270 00271 $result = array(); 00272 00273 reset($lines1); 00274 reset($lines2); 00275 00276 while (current($lines1) && current($lines2)) { 00277 $linenr1 = key($lines1); 00278 $linenr2 = key($lines2); 00279 00280 if ($linenr1 < $linenr2) { 00281 $result[$linenr1] = current($lines1); 00282 next($lines1); 00283 } else if ($linenr2 < $linenr1) { 00284 $result[$linenr2] = current($lines2); 00285 next($lines2); 00286 } else { 00287 if (current($lines1) < 0) { 00288 $result[$linenr2] = current($lines2); 00289 } else if (current($lines2) < 0) { 00290 $result[$linenr2] = current($lines1); 00291 } else { 00292 $result[$linenr2] = current($lines1) + current($lines2); 00293 } 00294 next($lines1); 00295 next($lines2); 00296 } 00297 } 00298 00299 while (current($lines1)) { 00300 $result[key($lines1)] = current($lines1); 00301 next($lines1); 00302 } 00303 00304 while (current($lines2)) { 00305 $result[key($lines2)] = current($lines2); 00306 next($lines2); 00307 } 00308 00309 return $result; 00310 } 00311 } 00312 00325 class moodle_coverage_reporter extends HtmlCoverageReporter { 00326 00327 public function __construct($heading='Coverage Report', $dir='report') { 00328 global $CFG; 00329 parent::__construct($heading, '', $CFG->dataroot . '/codecoverage/' . $dir); 00330 } 00331 00344 protected function writeIndexFileTableRow($fileLink, $realFile, $fileCoverage) { 00345 00346 global $CFG; 00347 00348 $fileLink = str_replace($CFG->dirroot, '', $fileLink); 00349 $realFile = str_replace($CFG->dirroot, '', $realFile); 00350 00351 return parent::writeIndexFileTableRow($fileLink, $realFile, $fileCoverage);; 00352 } 00353 00365 protected function markFile($phpFile, $fileLink, &$coverageLines) { 00366 global $CFG; 00367 00368 $fileLink = str_replace($CFG->dirroot, '', $fileLink); 00369 00370 return parent::markFile($phpFile, $fileLink, $coverageLines); 00371 } 00372 00373 00381 protected function updateGrandTotals(&$coverageCounts) { 00382 $this->grandTotalLines += $coverageCounts['total']; 00383 $this->grandTotalCoveredLines += $coverageCounts['covered']; 00384 $this->grandTotalUncoveredLines += $coverageCounts['uncovered']; 00385 } 00386 00395 public function generateReport(&$data) { 00396 parent::generateReport($data); 00397 00398 // head data 00399 $data = new stdClass(); 00400 $data->time = time(); 00401 $data->title = $this->heading; 00402 $data->output = $this->outputDir; 00403 00404 // summary data 00405 $data->totalfiles = $this->grandTotalFiles; 00406 $data->totalln = $this->grandTotalLines; 00407 $data->totalcoveredln = $this->grandTotalCoveredLines; 00408 $data->totaluncoveredln = $this->grandTotalUncoveredLines; 00409 $data->totalpercentage = $this->getGrandCodeCoveragePercentage(); 00410 00411 // file details data 00412 $data->coveragedetails = $this->fileCoverage; 00413 00414 // save serialised object 00415 file_put_contents($data->output . '/codecoverage.ser', serialize($data)); 00416 } 00417 00425 static public function get_summary_info($type) { 00426 global $CFG, $OUTPUT; 00427 00428 $serfilepath = $CFG->dataroot . '/codecoverage/' . $type . '/codecoverage.ser'; 00429 if (file_exists($serfilepath) && is_readable($serfilepath)) { 00430 if ($data = unserialize(file_get_contents($serfilepath))) { 00431 // return one table with all the totals (we avoid individual file results here) 00432 $result = ''; 00433 $table = new html_table(); 00434 $table->align = array('right', 'left'); 00435 $table->tablealign = 'center'; 00436 $table->attributes['class'] = 'codecoveragetable'; 00437 $table->id = 'codecoveragetable_' . $type; 00438 $table->rowclasses = array('label', 'value'); 00439 $table->data = array( 00440 array(get_string('date') , userdate($data->time)), 00441 array(get_string('files') , format_float($data->totalfiles, 0)), 00442 array(get_string('totallines', 'tool_unittest') , format_float($data->totalln, 0)), 00443 array(get_string('executablelines', 'tool_unittest') , format_float($data->totalcoveredln + $data->totaluncoveredln, 0)), 00444 array(get_string('coveredlines', 'tool_unittest') , format_float($data->totalcoveredln, 0)), 00445 array(get_string('uncoveredlines', 'tool_unittest') , format_float($data->totaluncoveredln, 0)), 00446 array(get_string('coveredpercentage', 'tool_unittest'), format_float($data->totalpercentage, 2) . '%') 00447 ); 00448 00449 $url = $CFG->wwwroot . '/'.$CFG->admin.'/tool/unittest/coveragefile.php/' . $type . '/index.html'; 00450 $result .= $OUTPUT->heading($data->title, 3, 'main codecoverageheading'); 00451 $result .= $OUTPUT->heading('<a href="' . $url . '" onclick="javascript:window.open(' . "'" . $url . "'" . ');return false;"' . 00452 ' title="">' . get_string('codecoveragecompletereport', 'tool_unittest') . '</a>', 4, 'main codecoveragelink'); 00453 $result .= html_writer::table($table); 00454 00455 return $OUTPUT->box($result, 'generalbox boxwidthwide boxaligncenter codecoveragebox', '', true); 00456 } 00457 } 00458 return false; 00459 } 00460 00468 static public function print_summary_info($type) { 00469 echo self::get_summary_info($type); 00470 } 00471 00479 static public function get_link_to_latest($type) { 00480 global $CFG, $OUTPUT; 00481 00482 $serfilepath = $CFG->dataroot . '/codecoverage/' . $type . '/codecoverage.ser'; 00483 if (file_exists($serfilepath) && is_readable($serfilepath)) { 00484 if ($data = unserialize(file_get_contents($serfilepath))) { 00485 $info = new stdClass(); 00486 $info->date = userdate($data->time); 00487 $info->files = format_float($data->totalfiles, 0); 00488 $info->percentage = format_float($data->totalpercentage, 2) . '%'; 00489 00490 $strlatestreport = get_string('codecoveragelatestreport', 'tool_unittest'); 00491 $strlatestdetails = get_string('codecoveragelatestdetails', 'tool_unittest', $info); 00492 00493 // return one link to latest complete report 00494 $result = ''; 00495 $url = $CFG->wwwroot . '/'.$CFG->admin.'/tool/unittest/coveragefile.php/' . $type . '/index.html'; 00496 $result .= $OUTPUT->heading('<a href="' . $url . '" onclick="javascript:window.open(' . "'" . $url . "'" . ');return false;"' . 00497 ' title="">' . $strlatestreport . '</a>', 3, 'main codecoveragelink'); 00498 $result .= $OUTPUT->heading($strlatestdetails, 4, 'main codecoveragedetails'); 00499 return $OUTPUT->box($result, 'generalbox boxwidthwide boxaligncenter codecoveragebox', '', true); 00500 } 00501 } 00502 return false; 00503 } 00504 00512 static public function print_link_to_latest($type) { 00513 echo self::get_link_to_latest($type); 00514 } 00515 } 00516 00517 00533 function moodle_reflect_file($file) { 00534 00535 $contents = file_get_contents($file); 00536 $tokens = token_get_all($contents); 00537 00538 $functionTrapped = false; 00539 $classTrapped = false; 00540 $openBraces = 0; 00541 00542 $classes = array(); 00543 $functions = array(); 00544 00545 foreach ($tokens as $token) { 00546 /* 00547 * Tokens are characters representing symbols or arrays 00548 * representing strings. The keys/values in the arrays are 00549 * 00550 * - 0 => token id, 00551 * - 1 => string, 00552 * - 2 => line number 00553 * 00554 * Token ID's are explained here: 00555 * http://www.php.net/manual/en/tokens.php. 00556 */ 00557 00558 if (is_array($token)) { 00559 $type = $token[0]; 00560 $value = $token[1]; 00561 $lineNum = $token[2]; 00562 } else { 00563 // It's a symbol 00564 // Maintain the count of open braces 00565 if ($token == '{') { 00566 $openBraces++; 00567 } else if ($token == '}') { 00568 $openBraces--; 00569 } 00570 00571 continue; 00572 } 00573 00574 switch ($type) { 00575 // Name of something 00576 case T_STRING: 00577 if ($functionTrapped) { 00578 $functions[] = $value; 00579 $functionTrapped = false; 00580 } elseif ($classTrapped) { 00581 $classes[] = $value; 00582 $classTrapped = false; 00583 } 00584 continue; 00585 00586 // Functions 00587 case T_FUNCTION: 00588 if ($openBraces == 0) { 00589 $functionTrapped = true; 00590 } 00591 break; 00592 00593 // Classes 00594 case T_CLASS: 00595 $classTrapped = true; 00596 break; 00597 00598 // Default case: do nothing 00599 default: 00600 break; 00601 } 00602 } 00603 00604 return (object)array('classes' => $classes, 'functions' => $functions); 00605 }