_rawCoverage = $coverage; $this->setParams($reporter); } /** * Pulls params out of the reporter. * * @return void */ protected function setParams(CakeBaseReporter $reporter) { if ($reporter->params['app']) { $this->appTest = true; } if ($reporter->params['group']) { $this->groupTest = true; } if ($reporter->params['plugin']) { $this->pluginTest = Inflector::underscore($reporter->params['plugin']); } } /** * Set the coverage data array * * @return void */ public function setCoverage($coverage) { $this->_rawCoverage = $coverage; } /** * Generates report html to display. * * @return string compiled html report. */ public function report() { $pathFilter = $this->getPathFilter(); $coverageData = $this->filterCoverageDataByPath($pathFilter); if (empty($coverageData)) { return '

No files to generate coverage for

'; } $output = $this->coverageScript(); $output .= <<Code coverage results Toggle all files HTML; foreach ($coverageData as $file => $coverageData) { $fileData = file($file); $output .= $this->generateDiff($file, $fileData, $coverageData); } return $output; } /** * Gets the base path that the files we are interested in live in. * If appTest ist * * @return void */ public function getPathFilter() { $path = ROOT . DS; if ($this->appTest) { $path .= APP_DIR . DS; } elseif ($this->pluginTest) { $path = App::pluginPath($this->pluginTest); } else { $path = TEST_CAKE_CORE_INCLUDE_PATH; } return $path; } /** * Filters the coverage data by path. Files not in the provided path will be removed. * This method will merge all the various test run reports as well into a single report per file. * * @param string $path Path to filter files by. * @return array Array of coverage data for files that match the given path. */ public function filterCoverageDataByPath($path) { $files = array(); foreach ($this->_rawCoverage as $testRun) { foreach ($testRun['files'] as $filename => $fileCoverage) { if (strpos($filename, $path) !== 0) { continue; } $dead = isset($testRun['dead'][$filename]) ? $testRun['dead'][$filename] : array(); $executable = isset($testRun['executable'][$filename]) ? $testRun['executable'][$filename] : array(); if (!isset($files[$filename])) { $files[$filename] = array( 'covered' => array(), 'dead' => array(), 'executable' => array() ); } $files[$filename]['covered'] += $fileCoverage; $files[$filename]['executable'] += $executable; $files[$filename]['dead'] += $dead; } if (isset($testRun['test'])) { $testReflection = new ReflectionClass(get_class($testRun['test'])); list($fileBasename, $x) = explode('.', basename($testReflection->getFileName()), 2); $this->_testNames[] = $fileBasename; } } ksort($files); return $files; } /** * Generates an HTML diff for $file based on $coverageData. * * @param array $fileData File data as an array. See file() for how to get one of these. * @param array $coverageData Array of coverage data to use to generate HTML diffs with * @return string HTML diff. */ public function generateDiff($filename, $fileLines, $coverageData) { $output = ''; $diff = array(); $covered = 0; $total = 0; //shift line numbers forward one; array_unshift($fileLines, ' '); unset($fileLines[0]); foreach ($fileLines as $lineno => $line) { $class = 'ignored'; $coveringTests = array(); if (isset($coverageData['covered'][$lineno])) { $coveringTests = PHPUnit_Util_CodeCoverage::getCoveringTests( $this->_rawCoverage, $filename, $lineno ); $class = 'covered'; $covered++; $total++; } elseif (isset($coverageData['executable'][$lineno])) { $class = 'uncovered'; $total++; } elseif (isset($coverageData['dead'][$lineno])) { $class .= ' dead'; } $diff[] = $this->_paintLine($line, $lineno, $class, $coveringTests); } $percentCovered = round(100 * $covered / $total, 2); $output .= $this->coverageHeader($filename, $percentCovered); $output .= implode("", $diff); $output .= $this->coverageFooter(); return $output; } /** * Renders the html for a single line in the html diff. * * @return void */ protected function _paintLine($line, $linenumber, $class, $coveringTests) { $coveredBy = ''; if (!empty($coveringTests)) { $coveredBy = "Covered by:\n"; foreach ($coveringTests as &$test) { $coveredBy .= $test->getName() . "\n"; } } return sprintf( '
%s%s
', $class, $coveredBy, $linenumber, htmlspecialchars($line) ); } /** * generate some javascript for the coverage report. * * @return void */ public function coverageScript() { return << function coverage_show_hide(selector) { var element = document.getElementById(selector); element.style.display = (element.style.display == 'none') ? '' : 'none'; } function coverage_toggle_all () { var divs = document.querySelectorAll('div.coverage-container'); var i = divs.length; while (i--) { if (divs[i] && divs[i].className.indexOf('primary') == -1) { divs[i].style.display = (divs[i].style.display == 'none') ? '' : 'none'; } } } HTML; } /** * Generate an HTML snippet for coverage headers * * @return void */ public function coverageHeader($filename, $percent) { $filename = basename($filename); list($file, $ext) = explode('.', $filename); $display = in_array($file, $this->_testNames) ? 'block' : 'none'; $primary = $display == 'block' ? 'primary' : ''; return <<

$filename Code coverage: $percent%

"; } }