Skip to content

Instantly share code, notes, and snippets.

@beberlei
Created May 2, 2009 14:15
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save beberlei/105570 to your computer and use it in GitHub Desktop.
Save beberlei/105570 to your computer and use it in GitHub Desktop.
#!/usr/bin/env php
<?php
/**
* PHPUnit
*
* Copyright (c) 2002-2009, Sebastian Bergmann <sb@sebastian-bergmann.de>.
* Copyright (c) 2009, Benjamin Eberlei <kontakt@beberlei.de>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided with the
* distribution.
*
* * Neither the name of Sebastian Bergmann nor the names of his
* contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
* ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*
* @category Testing
* @package PHPUnit
* @subpackage CoverageRunner
* @author Benjamin Eberlei <kontakt@beberlei.de>
* @copyright 2002-2009 Sebastian Bergmann <sb@sebastian-bergmann.de>
* @copyright 2009 Benjamin Eberlei <kontakt|beberlei.de>
* @license http://www.opensource.org/licenses/bsd-license.php BSD License
*/
require_once 'PHPUnit/Util/Filter.php';
PHPUnit_Util_Filter::addFileToFilter(__FILE__, 'PHPUNIT');
require 'PHPUnit/TextUI/Command.php';
define('PHPUnit_MAIN_METHOD', 'Whitewashing_TextUI_Command::main');
class Whitewashing_Util_ClassCoverageReport
{
private $classes = array();
private $projectStatementsTotal = 0;
private $projectStatementsCovered = 0;
public function __construct($classes, $projectStatementsTotal, $projectCoveredStatements)
{
$this->classes = $classes;
$this->projectStatementsTotal = $projectStatementsTotal;
$this->projectStatementsCovered = $projectCoveredStatements;
}
/**
* @return array('className' => '', 'statements' => 0, 'coveredStatements' => 0);
*/
public function getCoveredClasses()
{
return $this->classes;
}
public function getProjectStatements()
{
return $this->projectStatementsTotal;
}
public function getProjectCoveredStatements()
{
return $this->projectStatementsCovered;
}
}
class Whitewashing_Util_CoverageCalculator
{
private $projectStatements = 0;
private $projectCoveredStatements = 0;
private $fileStatements = 0;
private $fileCoveredStatements = 0;
private $projectClassData = array();
private $lines = array();
/**
* @param array $files
* @return Whitewashing_Util_ClassCoverageReport
*/
public function generateReportFromFileCoverageSummary($files)
{
$this->resetProjectCounters();
foreach ($files as $filename => $data) {
$this->resetFileCounters();
$this->calculateCoverageOfClasses($filename, $data);
$this->calculateFileCoverage($data);
$this->addFileCoverageToProjectCoverage($filename);
}
return new Whitewashing_Util_ClassCoverageReport(
$this->projectClassData, $this->projectStatements, $this->projectCoveredStatements
);
}
private function resetProjectCounters()
{
$this->projectClassData = array();
$this->projectCoveredStatements = 0;
$this->projectStatements = 0;
}
private function calculateCoverageOfClasses($filename, $fileCoverageData)
{
$classes = PHPUnit_Util_Class::getClassesInFile($filename);
foreach ($classes as $class) {
if ($class->isInterface()) {
continue;
}
$className = $class->getName();
$methods = $class->getMethods();
$classStatements = 0;
$classCoveredStatements = 0;
foreach($methods as $method) {
$methodName = $method->getName();
$methodCount = 0;
if ($method->getDeclaringClass()->getName() == $class->getName()) {
$startLine = $method->getStartLine();
$endLine = $method->getEndLine();
for ($i = $startLine; $i <= $endLine; $i++) {
$add = TRUE;
$count = 0;
if (isset($fileCoverageData[$i])) {
if ($fileCoverageData[$i] != -2) {
$classStatements++;
}
if (is_array($fileCoverageData[$i])) {
$classCoveredStatements++;
$count = count($fileCoverageData[$i]);
}
else if ($fileCoverageData[$i] == -2) {
$add = FALSE;
}
} else {
$add = FALSE;
}
$methodCount = max($methodCount, $count);
if ($add) {
$this->lines[$i] = array(
'count' => $count,
'type' => 'stmt'
);
}
}
$this->lines[$startLine] = array(
'count' => $methodCount,
'type' => 'method'
);
}
}
$this->projectClassData[] = array(
'className' => $className,
'statements' => $classStatements,
'coveredStatements' => $classCoveredStatements,
);
}
}
private function resetFileCounters()
{
$this->lines = array();
$this->fileStatements = 0;
$this->fileCoveredStatements = 0;
}
private function calculateFileCoverage($data)
{
foreach ($data as $_line => $_data) {
if (is_array($_data)) {
$count = count($_data);
}
else if ($_data == -1) {
$count = 0;
}
else if ($_data == -2) {
continue;
}
$this->lines[$_line] = array(
'count' => $count,
'type' => 'stmt'
);
}
foreach ($this->lines as $_line => $_data) {
if ($_data['type'] == 'stmt') {
if ($_data['count'] != 0) {
$this->fileCoveredStatements++;
}
$this->fileStatements++;
}
}
}
private function addFileCoverageToProjectCoverage($filename)
{
if (file_exists($filename)) {
$this->projectStatements += $this->fileStatements;
$this->projectCoveredStatements += $this->fileCoveredStatements;
}
}
}
class Whitewashing_TextUI_CoveragePrinter extends PHPUnit_TextUI_ResultPrinter
{
const COLOR_RED = "\x1b[37;41m\x1b[2K";
const COLOR_YELLOW = "\x1b[30;43m\x1b[2K";
const COLOR_GREEN = "\x1b[30;42m\x1b[2K";
const COLOR_DEFAULT = "\x1b[0m\x1b[2K";
/**
* Prints Code Coverage Report if no errors or failures are found, default report otherwise.
*
* @param PHPUnit_Framework_TestResult $result
*/
public function printResult(PHPUnit_Framework_TestResult $result)
{
if ($result->errorCount() > 0 || $result->failureCount() > 0) {
$this->write("Will not print Class-Level Code Coverage Report, because of failures and errors.\n\n");
parent::printResult($result);
} else {
$this->printClassLevelCoverageReport($result);
}
}
/**
* @param PHPUnit_Framework_TestResult $result
*/
private function printClassLevelCoverageReport(PHPUnit_Framework_TestResult $result)
{
$report = $this->generateClassLevelCoverageReport($result);
$this->printCoverageReportHeader();
$this->printCoverageReportClassList($report);
$this->printCoverageReportProjectOverview($report);
$this->flush();
}
private function generateClassLevelCoverageReport(PHPUnit_Framework_TestResult $result)
{
$codeCoverageInformation = $result->getCodeCoverageInformation();
$files = PHPUnit_Util_CodeCoverage::getSummary($codeCoverageInformation);
$calculator = new Whitewashing_Util_CoverageCalculator();
return $calculator->generateReportFromFileCoverageSummary($files);
}
private function printCoverageReportHeader()
{
$this->write("\n\n");
$this->write("Class-Level Code Coverage Report:\n\n");
}
private function printCoverageReportClassList($report)
{
$projectClassData = $report->getCoveredClasses();
usort($projectClassData, array($this, 'sortByClassNameCallback'));
foreach($projectClassData AS $class) {
$this->printCoverageLine($class['className'], $class['statements'], $class['coveredStatements']);
}
$this->write("\n");
}
private function printCoverageReportProjectOverview($report)
{
$projectStatements = $report->getProjectStatements();
$projectCoveredStatements = $report->getProjectCoveredStatements();
$this->printCoverageLine("PROJECT COVERAGE", $projectStatements, $projectCoveredStatements);
$this->write("\n");
}
private function sortByClassNameCallback($classA, $classB)
{
if($classA['className'] > $classB['className']) {
return 1;
} else if($classA['className'] == $classB['className']) {
return 0;
} else {
return -1;
}
}
private function printCoverageLine($className, $statements, $coveredStatements)
{
$len = strlen($className);
$coveredStatements = min($statements, $coveredStatements);
$percentage = 0;
if($statements > 0) {
$percentage = $coveredStatements/$statements*100;
}
$this->setCoverageLineColorBasedOnRatio($percentage);
$percentage = sprintf('%03.2f', $percentage);
$ratio = $coveredStatements."/".$statements;
$this->write($className);
$this->write(str_repeat(" ", 70-$len));
$this->write($ratio);
$this->write(str_repeat(" ", 14-strlen($ratio)));
$this->write($percentage."%\n");
$this->write(self::COLOR_DEFAULT);
}
private function setCoverageLineColorBasedOnRatio($percentage)
{
if($percentage > 80) {
$this->write(self::COLOR_GREEN);
} else if($percentage > 50) {
$this->write(self::COLOR_YELLOW);
} else {
$this->write(self::COLOR_RED);
}
}
}
class Whitewashing_TextUI_CoverageRunner extends PHPUnit_TextUI_TestRunner
{
/**
* @return PHPUnit_Framework_TestResult
*/
protected function createTestResult()
{
$result = new PHPUnit_Framework_TestResult;
$result->collectCodeCoverageInformation(true);
return $result;
}
}
class Whitewashing_TextUI_Command extends PHPUnit_TextUI_Command
{
/**
*/
public static function main($exit = TRUE)
{
$arguments = self::handleArguments();
$runner = new Whitewashing_TextUI_CoverageRunner;
$runner->setPrinter(new Whitewashing_TextUI_CoveragePrinter);
if (is_object($arguments['test']) && $arguments['test'] instanceof PHPUnit_Framework_Test) {
$suite = $arguments['test'];
} else {
$suite = $runner->getTest(
$arguments['test'],
$arguments['testFile'],
$arguments['syntaxCheck']
);
}
if ($suite->testAt(0) instanceof PHPUnit_Framework_Warning &&
strpos($suite->testAt(0)->getMessage(), 'No tests found in class') !== FALSE) {
$message = $suite->testAt(0)->getMessage();
$start = strpos($message, '"') + 1;
$end = strpos($message, '"', $start);
$className = substr($message, $start, $end - $start);
require_once 'PHPUnit/Util/Skeleton/Test.php';
$skeleton = new PHPUnit_Util_Skeleton_Test(
$className,
$arguments['testFile']
);
$result = $skeleton->generate(TRUE);
if (!$result['incomplete']) {
eval(str_replace(array('<?php', '?>'), '', $result['code']));
$suite = new PHPUnit_Framework_TestSuite($arguments['test'] . 'Test');
}
}
if ($arguments['listGroups']) {
PHPUnit_TextUI_TestRunner::printVersionString();
print "Available test group(s):\n";
$groups = $suite->getGroups();
sort($groups);
foreach ($groups as $group) {
print " - $group\n";
}
exit(PHPUnit_TextUI_TestRunner::SUCCESS_EXIT);
}
try {
$result = $runner->doRun(
$suite,
$arguments
);
}
catch (Exception $e) {
throw new RuntimeException(
'Could not create and run test suite: ' . $e->getMessage()
);
}
if ($exit) {
if ($result->wasSuccessful()) {
exit(PHPUnit_TextUI_TestRunner::SUCCESS_EXIT);
}
else if ($result->errorCount() > 0) {
exit(PHPUnit_TextUI_TestRunner::EXCEPTION_EXIT);
}
else {
exit(PHPUnit_TextUI_TestRunner::FAILURE_EXIT);
}
}
}
}
Whitewashing_TextUI_Command::main();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment