[COMPOSER] Add new php-ffmpeg package

This commit is contained in:
t3nma
2020-08-07 23:42:38 +01:00
parent 0a6bb5190f
commit c527ad0803
8874 changed files with 1090008 additions and 154 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,47 @@
<?php declare(strict_types=1);
/*
* This file is part of the php-code-coverage package.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage\Driver;
/**
* Interface for code coverage drivers.
*/
interface Driver
{
/**
* @var int
*
* @see http://xdebug.org/docs/code_coverage
*/
public const LINE_EXECUTED = 1;
/**
* @var int
*
* @see http://xdebug.org/docs/code_coverage
*/
public const LINE_NOT_EXECUTED = -1;
/**
* @var int
*
* @see http://xdebug.org/docs/code_coverage
*/
public const LINE_NOT_EXECUTABLE = -2;
/**
* Start collection of code coverage information.
*/
public function start(bool $determineUnusedAndDead = true): void;
/**
* Stop collection of code coverage information.
*/
public function stop(): array;
}

View File

@@ -0,0 +1,45 @@
<?php declare(strict_types=1);
/*
* This file is part of the php-code-coverage package.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage\Driver;
/**
* Driver for PCOV code coverage functionality.
*
* @codeCoverageIgnore
*/
final class PCOV implements Driver
{
/**
* Start collection of code coverage information.
*/
public function start(bool $determineUnusedAndDead = true): void
{
\pcov\start();
}
/**
* Stop collection of code coverage information.
*/
public function stop(): array
{
\pcov\stop();
$waiting = \pcov\waiting();
$collect = [];
if ($waiting) {
$collect = \pcov\collect(\pcov\inclusive, $waiting);
\pcov\clear();
}
return $collect;
}
}

View File

@@ -0,0 +1,96 @@
<?php declare(strict_types=1);
/*
* This file is part of the php-code-coverage package.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage\Driver;
use SebastianBergmann\CodeCoverage\RuntimeException;
/**
* Driver for PHPDBG's code coverage functionality.
*
* @codeCoverageIgnore
*/
final class PHPDBG implements Driver
{
/**
* @throws RuntimeException
*/
public function __construct()
{
if (\PHP_SAPI !== 'phpdbg') {
throw new RuntimeException(
'This driver requires the PHPDBG SAPI'
);
}
if (!\function_exists('phpdbg_start_oplog')) {
throw new RuntimeException(
'This build of PHPDBG does not support code coverage'
);
}
}
/**
* Start collection of code coverage information.
*/
public function start(bool $determineUnusedAndDead = true): void
{
\phpdbg_start_oplog();
}
/**
* Stop collection of code coverage information.
*/
public function stop(): array
{
static $fetchedLines = [];
$dbgData = \phpdbg_end_oplog();
if ($fetchedLines == []) {
$sourceLines = \phpdbg_get_executable();
} else {
$newFiles = \array_diff(\get_included_files(), \array_keys($fetchedLines));
$sourceLines = [];
if ($newFiles) {
$sourceLines = phpdbg_get_executable(['files' => $newFiles]);
}
}
foreach ($sourceLines as $file => $lines) {
foreach ($lines as $lineNo => $numExecuted) {
$sourceLines[$file][$lineNo] = self::LINE_NOT_EXECUTED;
}
}
$fetchedLines = \array_merge($fetchedLines, $sourceLines);
return $this->detectExecutedLines($fetchedLines, $dbgData);
}
/**
* Convert phpdbg based data into the format CodeCoverage expects
*/
private function detectExecutedLines(array $sourceLines, array $dbgData): array
{
foreach ($dbgData as $file => $coveredLines) {
foreach ($coveredLines as $lineNo => $numExecuted) {
// phpdbg also reports $lineNo=0 when e.g. exceptions get thrown.
// make sure we only mark lines executed which are actually executable.
if (isset($sourceLines[$file][$lineNo])) {
$sourceLines[$file][$lineNo] = self::LINE_EXECUTED;
}
}
}
return $sourceLines;
}
}

View File

@@ -0,0 +1,112 @@
<?php declare(strict_types=1);
/*
* This file is part of the php-code-coverage package.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage\Driver;
use SebastianBergmann\CodeCoverage\Filter;
use SebastianBergmann\CodeCoverage\RuntimeException;
/**
* Driver for Xdebug's code coverage functionality.
*
* @codeCoverageIgnore
*/
final class Xdebug implements Driver
{
/**
* @var array
*/
private $cacheNumLines = [];
/**
* @var Filter
*/
private $filter;
/**
* @throws RuntimeException
*/
public function __construct(Filter $filter = null)
{
if (!\extension_loaded('xdebug')) {
throw new RuntimeException('This driver requires Xdebug');
}
if (!\ini_get('xdebug.coverage_enable')) {
throw new RuntimeException('xdebug.coverage_enable=On has to be set in php.ini');
}
if ($filter === null) {
$filter = new Filter;
}
$this->filter = $filter;
}
/**
* Start collection of code coverage information.
*/
public function start(bool $determineUnusedAndDead = true): void
{
if ($determineUnusedAndDead) {
\xdebug_start_code_coverage(XDEBUG_CC_UNUSED | XDEBUG_CC_DEAD_CODE);
} else {
\xdebug_start_code_coverage();
}
}
/**
* Stop collection of code coverage information.
*/
public function stop(): array
{
$data = \xdebug_get_code_coverage();
\xdebug_stop_code_coverage();
return $this->cleanup($data);
}
private function cleanup(array $data): array
{
foreach (\array_keys($data) as $file) {
unset($data[$file][0]);
if (!$this->filter->isFile($file)) {
continue;
}
$numLines = $this->getNumberOfLinesInFile($file);
foreach (\array_keys($data[$file]) as $line) {
if ($line > $numLines) {
unset($data[$file][$line]);
}
}
}
return $data;
}
private function getNumberOfLinesInFile(string $fileName): int
{
if (!isset($this->cacheNumLines[$fileName])) {
$buffer = \file_get_contents($fileName);
$lines = \substr_count($buffer, "\n");
if (\substr($buffer, -1) !== "\n") {
$lines++;
}
$this->cacheNumLines[$fileName] = $lines;
}
return $this->cacheNumLines[$fileName];
}
}

View File

@@ -0,0 +1,17 @@
<?php declare(strict_types=1);
/*
* This file is part of the php-code-coverage package.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage;
/**
* Exception that is raised when covered code is not executed.
*/
final class CoveredCodeNotExecutedException extends RuntimeException
{
}

View File

@@ -0,0 +1,17 @@
<?php declare(strict_types=1);
/*
* This file is part of the php-code-coverage package.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage;
/**
* Exception interface for php-code-coverage component.
*/
interface Exception
{
}

View File

@@ -0,0 +1,36 @@
<?php declare(strict_types=1);
/*
* This file is part of the php-code-coverage package.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage;
final class InvalidArgumentException extends \InvalidArgumentException implements Exception
{
/**
* @param int $argument
* @param string $type
* @param null|mixed $value
*
* @return InvalidArgumentException
*/
public static function create($argument, $type, $value = null): self
{
$stack = \debug_backtrace(0);
return new self(
\sprintf(
'Argument #%d%sof %s::%s() must be a %s',
$argument,
$value !== null ? ' (' . \gettype($value) . '#' . $value . ')' : ' (No Value) ',
$stack[1]['class'],
$stack[1]['function'],
$type
)
);
}
}

View File

@@ -0,0 +1,17 @@
<?php declare(strict_types=1);
/*
* This file is part of the php-code-coverage package.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage;
/**
* Exception that is raised when @covers must be used but is not.
*/
final class MissingCoversAnnotationException extends RuntimeException
{
}

View File

@@ -0,0 +1,14 @@
<?php declare(strict_types=1);
/*
* This file is part of the php-code-coverage package.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage;
class RuntimeException extends \RuntimeException implements Exception
{
}

View File

@@ -0,0 +1,44 @@
<?php declare(strict_types=1);
/*
* This file is part of the php-code-coverage package.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage;
/**
* Exception that is raised when code is unintentionally covered.
*/
final class UnintentionallyCoveredCodeException extends RuntimeException
{
/**
* @var array
*/
private $unintentionallyCoveredUnits = [];
public function __construct(array $unintentionallyCoveredUnits)
{
$this->unintentionallyCoveredUnits = $unintentionallyCoveredUnits;
parent::__construct($this->toString());
}
public function getUnintentionallyCoveredUnits(): array
{
return $this->unintentionallyCoveredUnits;
}
private function toString(): string
{
$message = '';
foreach ($this->unintentionallyCoveredUnits as $unit) {
$message .= '- ' . $unit . "\n";
}
return $message;
}
}

View File

@@ -0,0 +1,174 @@
<?php declare(strict_types=1);
/*
* This file is part of the php-code-coverage package.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage;
use SebastianBergmann\FileIterator\Facade as FileIteratorFacade;
/**
* Filter for whitelisting of code coverage information.
*/
final class Filter
{
/**
* Source files that are whitelisted.
*
* @var array
*/
private $whitelistedFiles = [];
/**
* Remembers the result of the `is_file()` calls.
*
* @var bool[]
*/
private $isFileCallsCache = [];
/**
* Adds a directory to the whitelist (recursively).
*/
public function addDirectoryToWhitelist(string $directory, string $suffix = '.php', string $prefix = ''): void
{
$facade = new FileIteratorFacade;
$files = $facade->getFilesAsArray($directory, $suffix, $prefix);
foreach ($files as $file) {
$this->addFileToWhitelist($file);
}
}
/**
* Adds a file to the whitelist.
*/
public function addFileToWhitelist(string $filename): void
{
$filename = \realpath($filename);
if (!$filename) {
return;
}
$this->whitelistedFiles[$filename] = true;
}
/**
* Adds files to the whitelist.
*
* @param string[] $files
*/
public function addFilesToWhitelist(array $files): void
{
foreach ($files as $file) {
$this->addFileToWhitelist($file);
}
}
/**
* Removes a directory from the whitelist (recursively).
*/
public function removeDirectoryFromWhitelist(string $directory, string $suffix = '.php', string $prefix = ''): void
{
$facade = new FileIteratorFacade;
$files = $facade->getFilesAsArray($directory, $suffix, $prefix);
foreach ($files as $file) {
$this->removeFileFromWhitelist($file);
}
}
/**
* Removes a file from the whitelist.
*/
public function removeFileFromWhitelist(string $filename): void
{
$filename = \realpath($filename);
if (!$filename || !isset($this->whitelistedFiles[$filename])) {
return;
}
unset($this->whitelistedFiles[$filename]);
}
/**
* Checks whether a filename is a real filename.
*/
public function isFile(string $filename): bool
{
if (isset($this->isFileCallsCache[$filename])) {
return $this->isFileCallsCache[$filename];
}
if ($filename === '-' ||
\strpos($filename, 'vfs://') === 0 ||
\strpos($filename, 'xdebug://debug-eval') !== false ||
\strpos($filename, 'eval()\'d code') !== false ||
\strpos($filename, 'runtime-created function') !== false ||
\strpos($filename, 'runkit created function') !== false ||
\strpos($filename, 'assert code') !== false ||
\strpos($filename, 'regexp code') !== false ||
\strpos($filename, 'Standard input code') !== false) {
$isFile = false;
} else {
$isFile = \file_exists($filename);
}
$this->isFileCallsCache[$filename] = $isFile;
return $isFile;
}
/**
* Checks whether or not a file is filtered.
*/
public function isFiltered(string $filename): bool
{
if (!$this->isFile($filename)) {
return true;
}
return !isset($this->whitelistedFiles[$filename]);
}
/**
* Returns the list of whitelisted files.
*
* @return string[]
*/
public function getWhitelist(): array
{
return \array_keys($this->whitelistedFiles);
}
/**
* Returns whether this filter has a whitelist.
*/
public function hasWhitelist(): bool
{
return !empty($this->whitelistedFiles);
}
/**
* Returns the whitelisted files.
*
* @return string[]
*/
public function getWhitelistedFiles(): array
{
return $this->whitelistedFiles;
}
/**
* Sets the whitelisted files.
*/
public function setWhitelistedFiles(array $whitelistedFiles): void
{
$this->whitelistedFiles = $whitelistedFiles;
}
}

View File

@@ -0,0 +1,328 @@
<?php declare(strict_types=1);
/*
* This file is part of the php-code-coverage package.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage\Node;
use SebastianBergmann\CodeCoverage\Util;
/**
* Base class for nodes in the code coverage information tree.
*/
abstract class AbstractNode implements \Countable
{
/**
* @var string
*/
private $name;
/**
* @var string
*/
private $path;
/**
* @var array
*/
private $pathArray;
/**
* @var AbstractNode
*/
private $parent;
/**
* @var string
*/
private $id;
public function __construct(string $name, self $parent = null)
{
if (\substr($name, -1) == \DIRECTORY_SEPARATOR) {
$name = \substr($name, 0, -1);
}
$this->name = $name;
$this->parent = $parent;
}
public function getName(): string
{
return $this->name;
}
public function getId(): string
{
if ($this->id === null) {
$parent = $this->getParent();
if ($parent === null) {
$this->id = 'index';
} else {
$parentId = $parent->getId();
if ($parentId === 'index') {
$this->id = \str_replace(':', '_', $this->name);
} else {
$this->id = $parentId . '/' . $this->name;
}
}
}
return $this->id;
}
public function getPath(): string
{
if ($this->path === null) {
if ($this->parent === null || $this->parent->getPath() === null || $this->parent->getPath() === false) {
$this->path = $this->name;
} else {
$this->path = $this->parent->getPath() . \DIRECTORY_SEPARATOR . $this->name;
}
}
return $this->path;
}
public function getPathAsArray(): array
{
if ($this->pathArray === null) {
if ($this->parent === null) {
$this->pathArray = [];
} else {
$this->pathArray = $this->parent->getPathAsArray();
}
$this->pathArray[] = $this;
}
return $this->pathArray;
}
public function getParent(): ?self
{
return $this->parent;
}
/**
* Returns the percentage of classes that has been tested.
*
* @return int|string
*/
public function getTestedClassesPercent(bool $asString = true)
{
return Util::percent(
$this->getNumTestedClasses(),
$this->getNumClasses(),
$asString
);
}
/**
* Returns the percentage of traits that has been tested.
*
* @return int|string
*/
public function getTestedTraitsPercent(bool $asString = true)
{
return Util::percent(
$this->getNumTestedTraits(),
$this->getNumTraits(),
$asString
);
}
/**
* Returns the percentage of classes and traits that has been tested.
*
* @return int|string
*/
public function getTestedClassesAndTraitsPercent(bool $asString = true)
{
return Util::percent(
$this->getNumTestedClassesAndTraits(),
$this->getNumClassesAndTraits(),
$asString
);
}
/**
* Returns the percentage of functions that has been tested.
*
* @return int|string
*/
public function getTestedFunctionsPercent(bool $asString = true)
{
return Util::percent(
$this->getNumTestedFunctions(),
$this->getNumFunctions(),
$asString
);
}
/**
* Returns the percentage of methods that has been tested.
*
* @return int|string
*/
public function getTestedMethodsPercent(bool $asString = true)
{
return Util::percent(
$this->getNumTestedMethods(),
$this->getNumMethods(),
$asString
);
}
/**
* Returns the percentage of functions and methods that has been tested.
*
* @return int|string
*/
public function getTestedFunctionsAndMethodsPercent(bool $asString = true)
{
return Util::percent(
$this->getNumTestedFunctionsAndMethods(),
$this->getNumFunctionsAndMethods(),
$asString
);
}
/**
* Returns the percentage of executed lines.
*
* @return int|string
*/
public function getLineExecutedPercent(bool $asString = true)
{
return Util::percent(
$this->getNumExecutedLines(),
$this->getNumExecutableLines(),
$asString
);
}
/**
* Returns the number of classes and traits.
*/
public function getNumClassesAndTraits(): int
{
return $this->getNumClasses() + $this->getNumTraits();
}
/**
* Returns the number of tested classes and traits.
*/
public function getNumTestedClassesAndTraits(): int
{
return $this->getNumTestedClasses() + $this->getNumTestedTraits();
}
/**
* Returns the classes and traits of this node.
*/
public function getClassesAndTraits(): array
{
return \array_merge($this->getClasses(), $this->getTraits());
}
/**
* Returns the number of functions and methods.
*/
public function getNumFunctionsAndMethods(): int
{
return $this->getNumFunctions() + $this->getNumMethods();
}
/**
* Returns the number of tested functions and methods.
*/
public function getNumTestedFunctionsAndMethods(): int
{
return $this->getNumTestedFunctions() + $this->getNumTestedMethods();
}
/**
* Returns the functions and methods of this node.
*/
public function getFunctionsAndMethods(): array
{
return \array_merge($this->getFunctions(), $this->getMethods());
}
/**
* Returns the classes of this node.
*/
abstract public function getClasses(): array;
/**
* Returns the traits of this node.
*/
abstract public function getTraits(): array;
/**
* Returns the functions of this node.
*/
abstract public function getFunctions(): array;
/**
* Returns the LOC/CLOC/NCLOC of this node.
*/
abstract public function getLinesOfCode(): array;
/**
* Returns the number of executable lines.
*/
abstract public function getNumExecutableLines(): int;
/**
* Returns the number of executed lines.
*/
abstract public function getNumExecutedLines(): int;
/**
* Returns the number of classes.
*/
abstract public function getNumClasses(): int;
/**
* Returns the number of tested classes.
*/
abstract public function getNumTestedClasses(): int;
/**
* Returns the number of traits.
*/
abstract public function getNumTraits(): int;
/**
* Returns the number of tested traits.
*/
abstract public function getNumTestedTraits(): int;
/**
* Returns the number of methods.
*/
abstract public function getNumMethods(): int;
/**
* Returns the number of tested methods.
*/
abstract public function getNumTestedMethods(): int;
/**
* Returns the number of functions.
*/
abstract public function getNumFunctions(): int;
/**
* Returns the number of tested functions.
*/
abstract public function getNumTestedFunctions(): int;
}

View File

@@ -0,0 +1,227 @@
<?php declare(strict_types=1);
/*
* This file is part of the php-code-coverage package.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage\Node;
use SebastianBergmann\CodeCoverage\CodeCoverage;
final class Builder
{
public function build(CodeCoverage $coverage): Directory
{
$files = $coverage->getData();
$commonPath = $this->reducePaths($files);
$root = new Directory(
$commonPath,
null
);
$this->addItems(
$root,
$this->buildDirectoryStructure($files),
$coverage->getTests(),
$coverage->getCacheTokens()
);
return $root;
}
private function addItems(Directory $root, array $items, array $tests, bool $cacheTokens): void
{
foreach ($items as $key => $value) {
$key = (string) $key;
if (\substr($key, -2) === '/f') {
$key = \substr($key, 0, -2);
if (\file_exists($root->getPath() . \DIRECTORY_SEPARATOR . $key)) {
$root->addFile($key, $value, $tests, $cacheTokens);
}
} else {
$child = $root->addDirectory($key);
$this->addItems($child, $value, $tests, $cacheTokens);
}
}
}
/**
* Builds an array representation of the directory structure.
*
* For instance,
*
* <code>
* Array
* (
* [Money.php] => Array
* (
* ...
* )
*
* [MoneyBag.php] => Array
* (
* ...
* )
* )
* </code>
*
* is transformed into
*
* <code>
* Array
* (
* [.] => Array
* (
* [Money.php] => Array
* (
* ...
* )
*
* [MoneyBag.php] => Array
* (
* ...
* )
* )
* )
* </code>
*/
private function buildDirectoryStructure(array $files): array
{
$result = [];
foreach ($files as $path => $file) {
$path = \explode(\DIRECTORY_SEPARATOR, $path);
$pointer = &$result;
$max = \count($path);
for ($i = 0; $i < $max; $i++) {
$type = '';
if ($i === ($max - 1)) {
$type = '/f';
}
$pointer = &$pointer[$path[$i] . $type];
}
$pointer = $file;
}
return $result;
}
/**
* Reduces the paths by cutting the longest common start path.
*
* For instance,
*
* <code>
* Array
* (
* [/home/sb/Money/Money.php] => Array
* (
* ...
* )
*
* [/home/sb/Money/MoneyBag.php] => Array
* (
* ...
* )
* )
* </code>
*
* is reduced to
*
* <code>
* Array
* (
* [Money.php] => Array
* (
* ...
* )
*
* [MoneyBag.php] => Array
* (
* ...
* )
* )
* </code>
*/
private function reducePaths(array &$files): string
{
if (empty($files)) {
return '.';
}
$commonPath = '';
$paths = \array_keys($files);
if (\count($files) === 1) {
$commonPath = \dirname($paths[0]) . \DIRECTORY_SEPARATOR;
$files[\basename($paths[0])] = $files[$paths[0]];
unset($files[$paths[0]]);
return $commonPath;
}
$max = \count($paths);
for ($i = 0; $i < $max; $i++) {
// strip phar:// prefixes
if (\strpos($paths[$i], 'phar://') === 0) {
$paths[$i] = \substr($paths[$i], 7);
$paths[$i] = \str_replace('/', \DIRECTORY_SEPARATOR, $paths[$i]);
}
$paths[$i] = \explode(\DIRECTORY_SEPARATOR, $paths[$i]);
if (empty($paths[$i][0])) {
$paths[$i][0] = \DIRECTORY_SEPARATOR;
}
}
$done = false;
$max = \count($paths);
while (!$done) {
for ($i = 0; $i < $max - 1; $i++) {
if (!isset($paths[$i][0]) ||
!isset($paths[$i + 1][0]) ||
$paths[$i][0] !== $paths[$i + 1][0]) {
$done = true;
break;
}
}
if (!$done) {
$commonPath .= $paths[0][0];
if ($paths[0][0] !== \DIRECTORY_SEPARATOR) {
$commonPath .= \DIRECTORY_SEPARATOR;
}
for ($i = 0; $i < $max; $i++) {
\array_shift($paths[$i]);
}
}
}
$original = \array_keys($files);
$max = \count($original);
for ($i = 0; $i < $max; $i++) {
$files[\implode(\DIRECTORY_SEPARATOR, $paths[$i])] = $files[$original[$i]];
unset($files[$original[$i]]);
}
\ksort($files);
return \substr($commonPath, 0, -1);
}
}

View File

@@ -0,0 +1,427 @@
<?php declare(strict_types=1);
/*
* This file is part of the php-code-coverage package.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage\Node;
use SebastianBergmann\CodeCoverage\InvalidArgumentException;
/**
* Represents a directory in the code coverage information tree.
*/
final class Directory extends AbstractNode implements \IteratorAggregate
{
/**
* @var AbstractNode[]
*/
private $children = [];
/**
* @var Directory[]
*/
private $directories = [];
/**
* @var File[]
*/
private $files = [];
/**
* @var array
*/
private $classes;
/**
* @var array
*/
private $traits;
/**
* @var array
*/
private $functions;
/**
* @var array
*/
private $linesOfCode;
/**
* @var int
*/
private $numFiles = -1;
/**
* @var int
*/
private $numExecutableLines = -1;
/**
* @var int
*/
private $numExecutedLines = -1;
/**
* @var int
*/
private $numClasses = -1;
/**
* @var int
*/
private $numTestedClasses = -1;
/**
* @var int
*/
private $numTraits = -1;
/**
* @var int
*/
private $numTestedTraits = -1;
/**
* @var int
*/
private $numMethods = -1;
/**
* @var int
*/
private $numTestedMethods = -1;
/**
* @var int
*/
private $numFunctions = -1;
/**
* @var int
*/
private $numTestedFunctions = -1;
/**
* Returns the number of files in/under this node.
*/
public function count(): int
{
if ($this->numFiles === -1) {
$this->numFiles = 0;
foreach ($this->children as $child) {
$this->numFiles += \count($child);
}
}
return $this->numFiles;
}
/**
* Returns an iterator for this node.
*/
public function getIterator(): \RecursiveIteratorIterator
{
return new \RecursiveIteratorIterator(
new Iterator($this),
\RecursiveIteratorIterator::SELF_FIRST
);
}
/**
* Adds a new directory.
*/
public function addDirectory(string $name): self
{
$directory = new self($name, $this);
$this->children[] = $directory;
$this->directories[] = &$this->children[\count($this->children) - 1];
return $directory;
}
/**
* Adds a new file.
*
* @throws InvalidArgumentException
*/
public function addFile(string $name, array $coverageData, array $testData, bool $cacheTokens): File
{
$file = new File($name, $this, $coverageData, $testData, $cacheTokens);
$this->children[] = $file;
$this->files[] = &$this->children[\count($this->children) - 1];
$this->numExecutableLines = -1;
$this->numExecutedLines = -1;
return $file;
}
/**
* Returns the directories in this directory.
*/
public function getDirectories(): array
{
return $this->directories;
}
/**
* Returns the files in this directory.
*/
public function getFiles(): array
{
return $this->files;
}
/**
* Returns the child nodes of this node.
*/
public function getChildNodes(): array
{
return $this->children;
}
/**
* Returns the classes of this node.
*/
public function getClasses(): array
{
if ($this->classes === null) {
$this->classes = [];
foreach ($this->children as $child) {
$this->classes = \array_merge(
$this->classes,
$child->getClasses()
);
}
}
return $this->classes;
}
/**
* Returns the traits of this node.
*/
public function getTraits(): array
{
if ($this->traits === null) {
$this->traits = [];
foreach ($this->children as $child) {
$this->traits = \array_merge(
$this->traits,
$child->getTraits()
);
}
}
return $this->traits;
}
/**
* Returns the functions of this node.
*/
public function getFunctions(): array
{
if ($this->functions === null) {
$this->functions = [];
foreach ($this->children as $child) {
$this->functions = \array_merge(
$this->functions,
$child->getFunctions()
);
}
}
return $this->functions;
}
/**
* Returns the LOC/CLOC/NCLOC of this node.
*/
public function getLinesOfCode(): array
{
if ($this->linesOfCode === null) {
$this->linesOfCode = ['loc' => 0, 'cloc' => 0, 'ncloc' => 0];
foreach ($this->children as $child) {
$linesOfCode = $child->getLinesOfCode();
$this->linesOfCode['loc'] += $linesOfCode['loc'];
$this->linesOfCode['cloc'] += $linesOfCode['cloc'];
$this->linesOfCode['ncloc'] += $linesOfCode['ncloc'];
}
}
return $this->linesOfCode;
}
/**
* Returns the number of executable lines.
*/
public function getNumExecutableLines(): int
{
if ($this->numExecutableLines === -1) {
$this->numExecutableLines = 0;
foreach ($this->children as $child) {
$this->numExecutableLines += $child->getNumExecutableLines();
}
}
return $this->numExecutableLines;
}
/**
* Returns the number of executed lines.
*/
public function getNumExecutedLines(): int
{
if ($this->numExecutedLines === -1) {
$this->numExecutedLines = 0;
foreach ($this->children as $child) {
$this->numExecutedLines += $child->getNumExecutedLines();
}
}
return $this->numExecutedLines;
}
/**
* Returns the number of classes.
*/
public function getNumClasses(): int
{
if ($this->numClasses === -1) {
$this->numClasses = 0;
foreach ($this->children as $child) {
$this->numClasses += $child->getNumClasses();
}
}
return $this->numClasses;
}
/**
* Returns the number of tested classes.
*/
public function getNumTestedClasses(): int
{
if ($this->numTestedClasses === -1) {
$this->numTestedClasses = 0;
foreach ($this->children as $child) {
$this->numTestedClasses += $child->getNumTestedClasses();
}
}
return $this->numTestedClasses;
}
/**
* Returns the number of traits.
*/
public function getNumTraits(): int
{
if ($this->numTraits === -1) {
$this->numTraits = 0;
foreach ($this->children as $child) {
$this->numTraits += $child->getNumTraits();
}
}
return $this->numTraits;
}
/**
* Returns the number of tested traits.
*/
public function getNumTestedTraits(): int
{
if ($this->numTestedTraits === -1) {
$this->numTestedTraits = 0;
foreach ($this->children as $child) {
$this->numTestedTraits += $child->getNumTestedTraits();
}
}
return $this->numTestedTraits;
}
/**
* Returns the number of methods.
*/
public function getNumMethods(): int
{
if ($this->numMethods === -1) {
$this->numMethods = 0;
foreach ($this->children as $child) {
$this->numMethods += $child->getNumMethods();
}
}
return $this->numMethods;
}
/**
* Returns the number of tested methods.
*/
public function getNumTestedMethods(): int
{
if ($this->numTestedMethods === -1) {
$this->numTestedMethods = 0;
foreach ($this->children as $child) {
$this->numTestedMethods += $child->getNumTestedMethods();
}
}
return $this->numTestedMethods;
}
/**
* Returns the number of functions.
*/
public function getNumFunctions(): int
{
if ($this->numFunctions === -1) {
$this->numFunctions = 0;
foreach ($this->children as $child) {
$this->numFunctions += $child->getNumFunctions();
}
}
return $this->numFunctions;
}
/**
* Returns the number of tested functions.
*/
public function getNumTestedFunctions(): int
{
if ($this->numTestedFunctions === -1) {
$this->numTestedFunctions = 0;
foreach ($this->children as $child) {
$this->numTestedFunctions += $child->getNumTestedFunctions();
}
}
return $this->numTestedFunctions;
}
}

View File

@@ -0,0 +1,611 @@
<?php declare(strict_types=1);
/*
* This file is part of the php-code-coverage package.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage\Node;
/**
* Represents a file in the code coverage information tree.
*/
final class File extends AbstractNode
{
/**
* @var array
*/
private $coverageData;
/**
* @var array
*/
private $testData;
/**
* @var int
*/
private $numExecutableLines = 0;
/**
* @var int
*/
private $numExecutedLines = 0;
/**
* @var array
*/
private $classes = [];
/**
* @var array
*/
private $traits = [];
/**
* @var array
*/
private $functions = [];
/**
* @var array
*/
private $linesOfCode = [];
/**
* @var int
*/
private $numClasses;
/**
* @var int
*/
private $numTestedClasses = 0;
/**
* @var int
*/
private $numTraits;
/**
* @var int
*/
private $numTestedTraits = 0;
/**
* @var int
*/
private $numMethods;
/**
* @var int
*/
private $numTestedMethods;
/**
* @var int
*/
private $numTestedFunctions;
/**
* @var bool
*/
private $cacheTokens;
/**
* @var array
*/
private $codeUnitsByLine = [];
public function __construct(string $name, AbstractNode $parent, array $coverageData, array $testData, bool $cacheTokens)
{
parent::__construct($name, $parent);
$this->coverageData = $coverageData;
$this->testData = $testData;
$this->cacheTokens = $cacheTokens;
$this->calculateStatistics();
}
/**
* Returns the number of files in/under this node.
*/
public function count(): int
{
return 1;
}
/**
* Returns the code coverage data of this node.
*/
public function getCoverageData(): array
{
return $this->coverageData;
}
/**
* Returns the test data of this node.
*/
public function getTestData(): array
{
return $this->testData;
}
/**
* Returns the classes of this node.
*/
public function getClasses(): array
{
return $this->classes;
}
/**
* Returns the traits of this node.
*/
public function getTraits(): array
{
return $this->traits;
}
/**
* Returns the functions of this node.
*/
public function getFunctions(): array
{
return $this->functions;
}
/**
* Returns the LOC/CLOC/NCLOC of this node.
*/
public function getLinesOfCode(): array
{
return $this->linesOfCode;
}
/**
* Returns the number of executable lines.
*/
public function getNumExecutableLines(): int
{
return $this->numExecutableLines;
}
/**
* Returns the number of executed lines.
*/
public function getNumExecutedLines(): int
{
return $this->numExecutedLines;
}
/**
* Returns the number of classes.
*/
public function getNumClasses(): int
{
if ($this->numClasses === null) {
$this->numClasses = 0;
foreach ($this->classes as $class) {
foreach ($class['methods'] as $method) {
if ($method['executableLines'] > 0) {
$this->numClasses++;
continue 2;
}
}
}
}
return $this->numClasses;
}
/**
* Returns the number of tested classes.
*/
public function getNumTestedClasses(): int
{
return $this->numTestedClasses;
}
/**
* Returns the number of traits.
*/
public function getNumTraits(): int
{
if ($this->numTraits === null) {
$this->numTraits = 0;
foreach ($this->traits as $trait) {
foreach ($trait['methods'] as $method) {
if ($method['executableLines'] > 0) {
$this->numTraits++;
continue 2;
}
}
}
}
return $this->numTraits;
}
/**
* Returns the number of tested traits.
*/
public function getNumTestedTraits(): int
{
return $this->numTestedTraits;
}
/**
* Returns the number of methods.
*/
public function getNumMethods(): int
{
if ($this->numMethods === null) {
$this->numMethods = 0;
foreach ($this->classes as $class) {
foreach ($class['methods'] as $method) {
if ($method['executableLines'] > 0) {
$this->numMethods++;
}
}
}
foreach ($this->traits as $trait) {
foreach ($trait['methods'] as $method) {
if ($method['executableLines'] > 0) {
$this->numMethods++;
}
}
}
}
return $this->numMethods;
}
/**
* Returns the number of tested methods.
*/
public function getNumTestedMethods(): int
{
if ($this->numTestedMethods === null) {
$this->numTestedMethods = 0;
foreach ($this->classes as $class) {
foreach ($class['methods'] as $method) {
if ($method['executableLines'] > 0 &&
$method['coverage'] === 100) {
$this->numTestedMethods++;
}
}
}
foreach ($this->traits as $trait) {
foreach ($trait['methods'] as $method) {
if ($method['executableLines'] > 0 &&
$method['coverage'] === 100) {
$this->numTestedMethods++;
}
}
}
}
return $this->numTestedMethods;
}
/**
* Returns the number of functions.
*/
public function getNumFunctions(): int
{
return \count($this->functions);
}
/**
* Returns the number of tested functions.
*/
public function getNumTestedFunctions(): int
{
if ($this->numTestedFunctions === null) {
$this->numTestedFunctions = 0;
foreach ($this->functions as $function) {
if ($function['executableLines'] > 0 &&
$function['coverage'] === 100) {
$this->numTestedFunctions++;
}
}
}
return $this->numTestedFunctions;
}
private function calculateStatistics(): void
{
if ($this->cacheTokens) {
$tokens = \PHP_Token_Stream_CachingFactory::get($this->getPath());
} else {
$tokens = new \PHP_Token_Stream($this->getPath());
}
$this->linesOfCode = $tokens->getLinesOfCode();
foreach (\range(1, $this->linesOfCode['loc']) as $lineNumber) {
$this->codeUnitsByLine[$lineNumber] = [];
}
try {
$this->processClasses($tokens);
$this->processTraits($tokens);
$this->processFunctions($tokens);
} catch (\OutOfBoundsException $e) {
// This can happen with PHP_Token_Stream if the file is syntactically invalid,
// and probably affects a file that wasn't executed.
}
unset($tokens);
foreach (\range(1, $this->linesOfCode['loc']) as $lineNumber) {
if (isset($this->coverageData[$lineNumber])) {
foreach ($this->codeUnitsByLine[$lineNumber] as &$codeUnit) {
$codeUnit['executableLines']++;
}
unset($codeUnit);
$this->numExecutableLines++;
if (\count($this->coverageData[$lineNumber]) > 0) {
foreach ($this->codeUnitsByLine[$lineNumber] as &$codeUnit) {
$codeUnit['executedLines']++;
}
unset($codeUnit);
$this->numExecutedLines++;
}
}
}
foreach ($this->traits as &$trait) {
foreach ($trait['methods'] as &$method) {
if ($method['executableLines'] > 0) {
$method['coverage'] = ($method['executedLines'] /
$method['executableLines']) * 100;
} else {
$method['coverage'] = 100;
}
$method['crap'] = $this->crap(
$method['ccn'],
$method['coverage']
);
$trait['ccn'] += $method['ccn'];
}
unset($method);
if ($trait['executableLines'] > 0) {
$trait['coverage'] = ($trait['executedLines'] /
$trait['executableLines']) * 100;
if ($trait['coverage'] === 100) {
$this->numTestedClasses++;
}
} else {
$trait['coverage'] = 100;
}
$trait['crap'] = $this->crap(
$trait['ccn'],
$trait['coverage']
);
}
unset($trait);
foreach ($this->classes as &$class) {
foreach ($class['methods'] as &$method) {
if ($method['executableLines'] > 0) {
$method['coverage'] = ($method['executedLines'] /
$method['executableLines']) * 100;
} else {
$method['coverage'] = 100;
}
$method['crap'] = $this->crap(
$method['ccn'],
$method['coverage']
);
$class['ccn'] += $method['ccn'];
}
unset($method);
if ($class['executableLines'] > 0) {
$class['coverage'] = ($class['executedLines'] /
$class['executableLines']) * 100;
if ($class['coverage'] === 100) {
$this->numTestedClasses++;
}
} else {
$class['coverage'] = 100;
}
$class['crap'] = $this->crap(
$class['ccn'],
$class['coverage']
);
}
unset($class);
foreach ($this->functions as &$function) {
if ($function['executableLines'] > 0) {
$function['coverage'] = ($function['executedLines'] /
$function['executableLines']) * 100;
} else {
$function['coverage'] = 100;
}
if ($function['coverage'] === 100) {
$this->numTestedFunctions++;
}
$function['crap'] = $this->crap(
$function['ccn'],
$function['coverage']
);
}
}
private function processClasses(\PHP_Token_Stream $tokens): void
{
$classes = $tokens->getClasses();
$link = $this->getId() . '.html#';
foreach ($classes as $className => $class) {
if (\strpos($className, 'anonymous') === 0) {
continue;
}
if (!empty($class['package']['namespace'])) {
$className = $class['package']['namespace'] . '\\' . $className;
}
$this->classes[$className] = [
'className' => $className,
'methods' => [],
'startLine' => $class['startLine'],
'executableLines' => 0,
'executedLines' => 0,
'ccn' => 0,
'coverage' => 0,
'crap' => 0,
'package' => $class['package'],
'link' => $link . $class['startLine'],
];
foreach ($class['methods'] as $methodName => $method) {
if (\strpos($methodName, 'anonymous') === 0) {
continue;
}
$this->classes[$className]['methods'][$methodName] = $this->newMethod($methodName, $method, $link);
foreach (\range($method['startLine'], $method['endLine']) as $lineNumber) {
$this->codeUnitsByLine[$lineNumber] = [
&$this->classes[$className],
&$this->classes[$className]['methods'][$methodName],
];
}
}
}
}
private function processTraits(\PHP_Token_Stream $tokens): void
{
$traits = $tokens->getTraits();
$link = $this->getId() . '.html#';
foreach ($traits as $traitName => $trait) {
$this->traits[$traitName] = [
'traitName' => $traitName,
'methods' => [],
'startLine' => $trait['startLine'],
'executableLines' => 0,
'executedLines' => 0,
'ccn' => 0,
'coverage' => 0,
'crap' => 0,
'package' => $trait['package'],
'link' => $link . $trait['startLine'],
];
foreach ($trait['methods'] as $methodName => $method) {
if (\strpos($methodName, 'anonymous') === 0) {
continue;
}
$this->traits[$traitName]['methods'][$methodName] = $this->newMethod($methodName, $method, $link);
foreach (\range($method['startLine'], $method['endLine']) as $lineNumber) {
$this->codeUnitsByLine[$lineNumber] = [
&$this->traits[$traitName],
&$this->traits[$traitName]['methods'][$methodName],
];
}
}
}
}
private function processFunctions(\PHP_Token_Stream $tokens): void
{
$functions = $tokens->getFunctions();
$link = $this->getId() . '.html#';
foreach ($functions as $functionName => $function) {
if (\strpos($functionName, 'anonymous') === 0) {
continue;
}
$this->functions[$functionName] = [
'functionName' => $functionName,
'signature' => $function['signature'],
'startLine' => $function['startLine'],
'executableLines' => 0,
'executedLines' => 0,
'ccn' => $function['ccn'],
'coverage' => 0,
'crap' => 0,
'link' => $link . $function['startLine'],
];
foreach (\range($function['startLine'], $function['endLine']) as $lineNumber) {
$this->codeUnitsByLine[$lineNumber] = [&$this->functions[$functionName]];
}
}
}
private function crap(int $ccn, float $coverage): string
{
if ($coverage === 0.0) {
return (string) ($ccn ** 2 + $ccn);
}
if ($coverage >= 95) {
return (string) $ccn;
}
return \sprintf(
'%01.2F',
$ccn ** 2 * (1 - $coverage / 100) ** 3 + $ccn
);
}
private function newMethod(string $methodName, array $method, string $link): array
{
return [
'methodName' => $methodName,
'visibility' => $method['visibility'],
'signature' => $method['signature'],
'startLine' => $method['startLine'],
'endLine' => $method['endLine'],
'executableLines' => 0,
'executedLines' => 0,
'ccn' => $method['ccn'],
'coverage' => 0,
'crap' => 0,
'link' => $link . $method['startLine'],
];
}
}

View File

@@ -0,0 +1,89 @@
<?php declare(strict_types=1);
/*
* This file is part of the php-code-coverage package.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage\Node;
/**
* Recursive iterator for node object graphs.
*/
final class Iterator implements \RecursiveIterator
{
/**
* @var int
*/
private $position;
/**
* @var AbstractNode[]
*/
private $nodes;
public function __construct(Directory $node)
{
$this->nodes = $node->getChildNodes();
}
/**
* Rewinds the Iterator to the first element.
*/
public function rewind(): void
{
$this->position = 0;
}
/**
* Checks if there is a current element after calls to rewind() or next().
*/
public function valid(): bool
{
return $this->position < \count($this->nodes);
}
/**
* Returns the key of the current element.
*/
public function key(): int
{
return $this->position;
}
/**
* Returns the current element.
*/
public function current(): AbstractNode
{
return $this->valid() ? $this->nodes[$this->position] : null;
}
/**
* Moves forward to next element.
*/
public function next(): void
{
$this->position++;
}
/**
* Returns the sub iterator for the current element.
*
* @return Iterator
*/
public function getChildren(): self
{
return new self($this->nodes[$this->position]);
}
/**
* Checks whether the current element has children.
*/
public function hasChildren(): bool
{
return $this->nodes[$this->position] instanceof Directory;
}
}

View File

@@ -0,0 +1,258 @@
<?php declare(strict_types=1);
/*
* This file is part of the php-code-coverage package.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage\Report;
use SebastianBergmann\CodeCoverage\CodeCoverage;
use SebastianBergmann\CodeCoverage\Node\File;
use SebastianBergmann\CodeCoverage\RuntimeException;
/**
* Generates a Clover XML logfile from a code coverage object.
*/
final class Clover
{
/**
* @throws \RuntimeException
*/
public function process(CodeCoverage $coverage, ?string $target = null, ?string $name = null): string
{
$xmlDocument = new \DOMDocument('1.0', 'UTF-8');
$xmlDocument->formatOutput = true;
$xmlCoverage = $xmlDocument->createElement('coverage');
$xmlCoverage->setAttribute('generated', (string) $_SERVER['REQUEST_TIME']);
$xmlDocument->appendChild($xmlCoverage);
$xmlProject = $xmlDocument->createElement('project');
$xmlProject->setAttribute('timestamp', (string) $_SERVER['REQUEST_TIME']);
if (\is_string($name)) {
$xmlProject->setAttribute('name', $name);
}
$xmlCoverage->appendChild($xmlProject);
$packages = [];
$report = $coverage->getReport();
foreach ($report as $item) {
if (!$item instanceof File) {
continue;
}
/* @var File $item */
$xmlFile = $xmlDocument->createElement('file');
$xmlFile->setAttribute('name', $item->getPath());
$classes = $item->getClassesAndTraits();
$coverageData = $item->getCoverageData();
$lines = [];
$namespace = 'global';
foreach ($classes as $className => $class) {
$classStatements = 0;
$coveredClassStatements = 0;
$coveredMethods = 0;
$classMethods = 0;
foreach ($class['methods'] as $methodName => $method) {
if ($method['executableLines'] == 0) {
continue;
}
$classMethods++;
$classStatements += $method['executableLines'];
$coveredClassStatements += $method['executedLines'];
if ($method['coverage'] == 100) {
$coveredMethods++;
}
$methodCount = 0;
foreach (\range($method['startLine'], $method['endLine']) as $line) {
if (isset($coverageData[$line]) && ($coverageData[$line] !== null)) {
$methodCount = \max($methodCount, \count($coverageData[$line]));
}
}
$lines[$method['startLine']] = [
'ccn' => $method['ccn'],
'count' => $methodCount,
'crap' => $method['crap'],
'type' => 'method',
'visibility' => $method['visibility'],
'name' => $methodName,
];
}
if (!empty($class['package']['namespace'])) {
$namespace = $class['package']['namespace'];
}
$xmlClass = $xmlDocument->createElement('class');
$xmlClass->setAttribute('name', $className);
$xmlClass->setAttribute('namespace', $namespace);
if (!empty($class['package']['fullPackage'])) {
$xmlClass->setAttribute(
'fullPackage',
$class['package']['fullPackage']
);
}
if (!empty($class['package']['category'])) {
$xmlClass->setAttribute(
'category',
$class['package']['category']
);
}
if (!empty($class['package']['package'])) {
$xmlClass->setAttribute(
'package',
$class['package']['package']
);
}
if (!empty($class['package']['subpackage'])) {
$xmlClass->setAttribute(
'subpackage',
$class['package']['subpackage']
);
}
$xmlFile->appendChild($xmlClass);
$xmlMetrics = $xmlDocument->createElement('metrics');
$xmlMetrics->setAttribute('complexity', (string) $class['ccn']);
$xmlMetrics->setAttribute('methods', (string) $classMethods);
$xmlMetrics->setAttribute('coveredmethods', (string) $coveredMethods);
$xmlMetrics->setAttribute('conditionals', '0');
$xmlMetrics->setAttribute('coveredconditionals', '0');
$xmlMetrics->setAttribute('statements', (string) $classStatements);
$xmlMetrics->setAttribute('coveredstatements', (string) $coveredClassStatements);
$xmlMetrics->setAttribute('elements', (string) ($classMethods + $classStatements /* + conditionals */));
$xmlMetrics->setAttribute('coveredelements', (string) ($coveredMethods + $coveredClassStatements /* + coveredconditionals */));
$xmlClass->appendChild($xmlMetrics);
}
foreach ($coverageData as $line => $data) {
if ($data === null || isset($lines[$line])) {
continue;
}
$lines[$line] = [
'count' => \count($data), 'type' => 'stmt',
];
}
\ksort($lines);
foreach ($lines as $line => $data) {
$xmlLine = $xmlDocument->createElement('line');
$xmlLine->setAttribute('num', (string) $line);
$xmlLine->setAttribute('type', $data['type']);
if (isset($data['name'])) {
$xmlLine->setAttribute('name', $data['name']);
}
if (isset($data['visibility'])) {
$xmlLine->setAttribute('visibility', $data['visibility']);
}
if (isset($data['ccn'])) {
$xmlLine->setAttribute('complexity', (string) $data['ccn']);
}
if (isset($data['crap'])) {
$xmlLine->setAttribute('crap', (string) $data['crap']);
}
$xmlLine->setAttribute('count', (string) $data['count']);
$xmlFile->appendChild($xmlLine);
}
$linesOfCode = $item->getLinesOfCode();
$xmlMetrics = $xmlDocument->createElement('metrics');
$xmlMetrics->setAttribute('loc', (string) $linesOfCode['loc']);
$xmlMetrics->setAttribute('ncloc', (string) $linesOfCode['ncloc']);
$xmlMetrics->setAttribute('classes', (string) $item->getNumClassesAndTraits());
$xmlMetrics->setAttribute('methods', (string) $item->getNumMethods());
$xmlMetrics->setAttribute('coveredmethods', (string) $item->getNumTestedMethods());
$xmlMetrics->setAttribute('conditionals', '0');
$xmlMetrics->setAttribute('coveredconditionals', '0');
$xmlMetrics->setAttribute('statements', (string) $item->getNumExecutableLines());
$xmlMetrics->setAttribute('coveredstatements', (string) $item->getNumExecutedLines());
$xmlMetrics->setAttribute('elements', (string) ($item->getNumMethods() + $item->getNumExecutableLines() /* + conditionals */));
$xmlMetrics->setAttribute('coveredelements', (string) ($item->getNumTestedMethods() + $item->getNumExecutedLines() /* + coveredconditionals */));
$xmlFile->appendChild($xmlMetrics);
if ($namespace === 'global') {
$xmlProject->appendChild($xmlFile);
} else {
if (!isset($packages[$namespace])) {
$packages[$namespace] = $xmlDocument->createElement(
'package'
);
$packages[$namespace]->setAttribute('name', $namespace);
$xmlProject->appendChild($packages[$namespace]);
}
$packages[$namespace]->appendChild($xmlFile);
}
}
$linesOfCode = $report->getLinesOfCode();
$xmlMetrics = $xmlDocument->createElement('metrics');
$xmlMetrics->setAttribute('files', (string) \count($report));
$xmlMetrics->setAttribute('loc', (string) $linesOfCode['loc']);
$xmlMetrics->setAttribute('ncloc', (string) $linesOfCode['ncloc']);
$xmlMetrics->setAttribute('classes', (string) $report->getNumClassesAndTraits());
$xmlMetrics->setAttribute('methods', (string) $report->getNumMethods());
$xmlMetrics->setAttribute('coveredmethods', (string) $report->getNumTestedMethods());
$xmlMetrics->setAttribute('conditionals', '0');
$xmlMetrics->setAttribute('coveredconditionals', '0');
$xmlMetrics->setAttribute('statements', (string) $report->getNumExecutableLines());
$xmlMetrics->setAttribute('coveredstatements', (string) $report->getNumExecutedLines());
$xmlMetrics->setAttribute('elements', (string) ($report->getNumMethods() + $report->getNumExecutableLines() /* + conditionals */));
$xmlMetrics->setAttribute('coveredelements', (string) ($report->getNumTestedMethods() + $report->getNumExecutedLines() /* + coveredconditionals */));
$xmlProject->appendChild($xmlMetrics);
$buffer = $xmlDocument->saveXML();
if ($target !== null) {
if (!$this->createDirectory(\dirname($target))) {
throw new \RuntimeException(\sprintf('Directory "%s" was not created', \dirname($target)));
}
if (@\file_put_contents($target, $buffer) === false) {
throw new RuntimeException(
\sprintf(
'Could not write to "%s',
$target
)
);
}
}
return $buffer;
}
private function createDirectory(string $directory): bool
{
return !(!\is_dir($directory) && !@\mkdir($directory, 0777, true) && !\is_dir($directory));
}
}

View File

@@ -0,0 +1,165 @@
<?php declare(strict_types=1);
/*
* This file is part of the php-code-coverage package.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage\Report;
use SebastianBergmann\CodeCoverage\CodeCoverage;
use SebastianBergmann\CodeCoverage\Node\File;
use SebastianBergmann\CodeCoverage\RuntimeException;
final class Crap4j
{
/**
* @var int
*/
private $threshold;
public function __construct(int $threshold = 30)
{
$this->threshold = $threshold;
}
/**
* @throws \RuntimeException
*/
public function process(CodeCoverage $coverage, ?string $target = null, ?string $name = null): string
{
$document = new \DOMDocument('1.0', 'UTF-8');
$document->formatOutput = true;
$root = $document->createElement('crap_result');
$document->appendChild($root);
$project = $document->createElement('project', \is_string($name) ? $name : '');
$root->appendChild($project);
$root->appendChild($document->createElement('timestamp', \date('Y-m-d H:i:s', $_SERVER['REQUEST_TIME'])));
$stats = $document->createElement('stats');
$methodsNode = $document->createElement('methods');
$report = $coverage->getReport();
unset($coverage);
$fullMethodCount = 0;
$fullCrapMethodCount = 0;
$fullCrapLoad = 0;
$fullCrap = 0;
foreach ($report as $item) {
$namespace = 'global';
if (!$item instanceof File) {
continue;
}
$file = $document->createElement('file');
$file->setAttribute('name', $item->getPath());
$classes = $item->getClassesAndTraits();
foreach ($classes as $className => $class) {
foreach ($class['methods'] as $methodName => $method) {
$crapLoad = $this->getCrapLoad($method['crap'], $method['ccn'], $method['coverage']);
$fullCrap += $method['crap'];
$fullCrapLoad += $crapLoad;
$fullMethodCount++;
if ($method['crap'] >= $this->threshold) {
$fullCrapMethodCount++;
}
$methodNode = $document->createElement('method');
if (!empty($class['package']['namespace'])) {
$namespace = $class['package']['namespace'];
}
$methodNode->appendChild($document->createElement('package', $namespace));
$methodNode->appendChild($document->createElement('className', $className));
$methodNode->appendChild($document->createElement('methodName', $methodName));
$methodNode->appendChild($document->createElement('methodSignature', \htmlspecialchars($method['signature'])));
$methodNode->appendChild($document->createElement('fullMethod', \htmlspecialchars($method['signature'])));
$methodNode->appendChild($document->createElement('crap', (string) $this->roundValue($method['crap'])));
$methodNode->appendChild($document->createElement('complexity', (string) $method['ccn']));
$methodNode->appendChild($document->createElement('coverage', (string) $this->roundValue($method['coverage'])));
$methodNode->appendChild($document->createElement('crapLoad', (string) \round($crapLoad)));
$methodsNode->appendChild($methodNode);
}
}
}
$stats->appendChild($document->createElement('name', 'Method Crap Stats'));
$stats->appendChild($document->createElement('methodCount', (string) $fullMethodCount));
$stats->appendChild($document->createElement('crapMethodCount', (string) $fullCrapMethodCount));
$stats->appendChild($document->createElement('crapLoad', (string) \round($fullCrapLoad)));
$stats->appendChild($document->createElement('totalCrap', (string) $fullCrap));
$crapMethodPercent = 0;
if ($fullMethodCount > 0) {
$crapMethodPercent = $this->roundValue((100 * $fullCrapMethodCount) / $fullMethodCount);
}
$stats->appendChild($document->createElement('crapMethodPercent', (string) $crapMethodPercent));
$root->appendChild($stats);
$root->appendChild($methodsNode);
$buffer = $document->saveXML();
if ($target !== null) {
if (!$this->createDirectory(\dirname($target))) {
throw new \RuntimeException(\sprintf('Directory "%s" was not created', \dirname($target)));
}
if (@\file_put_contents($target, $buffer) === false) {
throw new RuntimeException(
\sprintf(
'Could not write to "%s',
$target
)
);
}
}
return $buffer;
}
/**
* @param float $crapValue
* @param int $cyclomaticComplexity
* @param float $coveragePercent
*/
private function getCrapLoad($crapValue, $cyclomaticComplexity, $coveragePercent): float
{
$crapLoad = 0;
if ($crapValue >= $this->threshold) {
$crapLoad += $cyclomaticComplexity * (1.0 - $coveragePercent / 100);
$crapLoad += $cyclomaticComplexity / $this->threshold;
}
return $crapLoad;
}
/**
* @param float $value
*/
private function roundValue($value): float
{
return \round($value, 2);
}
private function createDirectory(string $directory): bool
{
return !(!\is_dir($directory) && !@\mkdir($directory, 0777, true) && !\is_dir($directory));
}
}

View File

@@ -0,0 +1,167 @@
<?php declare(strict_types=1);
/*
* This file is part of the php-code-coverage package.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage\Report\Html;
use SebastianBergmann\CodeCoverage\CodeCoverage;
use SebastianBergmann\CodeCoverage\Node\Directory as DirectoryNode;
use SebastianBergmann\CodeCoverage\RuntimeException;
/**
* Generates an HTML report from a code coverage object.
*/
final class Facade
{
/**
* @var string
*/
private $templatePath;
/**
* @var string
*/
private $generator;
/**
* @var int
*/
private $lowUpperBound;
/**
* @var int
*/
private $highLowerBound;
public function __construct(int $lowUpperBound = 50, int $highLowerBound = 90, string $generator = '')
{
$this->generator = $generator;
$this->highLowerBound = $highLowerBound;
$this->lowUpperBound = $lowUpperBound;
$this->templatePath = __DIR__ . '/Renderer/Template/';
}
/**
* @throws RuntimeException
* @throws \InvalidArgumentException
* @throws \RuntimeException
*/
public function process(CodeCoverage $coverage, string $target): void
{
$target = $this->getDirectory($target);
$report = $coverage->getReport();
if (!isset($_SERVER['REQUEST_TIME'])) {
$_SERVER['REQUEST_TIME'] = \time();
}
$date = \date('D M j G:i:s T Y', $_SERVER['REQUEST_TIME']);
$dashboard = new Dashboard(
$this->templatePath,
$this->generator,
$date,
$this->lowUpperBound,
$this->highLowerBound
);
$directory = new Directory(
$this->templatePath,
$this->generator,
$date,
$this->lowUpperBound,
$this->highLowerBound
);
$file = new File(
$this->templatePath,
$this->generator,
$date,
$this->lowUpperBound,
$this->highLowerBound
);
$directory->render($report, $target . 'index.html');
$dashboard->render($report, $target . 'dashboard.html');
foreach ($report as $node) {
$id = $node->getId();
if ($node instanceof DirectoryNode) {
if (!$this->createDirectory($target . $id)) {
throw new \RuntimeException(\sprintf('Directory "%s" was not created', $target . $id));
}
$directory->render($node, $target . $id . '/index.html');
$dashboard->render($node, $target . $id . '/dashboard.html');
} else {
$dir = \dirname($target . $id);
if (!$this->createDirectory($dir)) {
throw new \RuntimeException(\sprintf('Directory "%s" was not created', $dir));
}
$file->render($node, $target . $id . '.html');
}
}
$this->copyFiles($target);
}
/**
* @throws RuntimeException
*/
private function copyFiles(string $target): void
{
$dir = $this->getDirectory($target . '_css');
\copy($this->templatePath . 'css/bootstrap.min.css', $dir . 'bootstrap.min.css');
\copy($this->templatePath . 'css/nv.d3.min.css', $dir . 'nv.d3.min.css');
\copy($this->templatePath . 'css/style.css', $dir . 'style.css');
\copy($this->templatePath . 'css/custom.css', $dir . 'custom.css');
\copy($this->templatePath . 'css/octicons.css', $dir . 'octicons.css');
$dir = $this->getDirectory($target . '_icons');
\copy($this->templatePath . 'icons/file-code.svg', $dir . 'file-code.svg');
\copy($this->templatePath . 'icons/file-directory.svg', $dir . 'file-directory.svg');
$dir = $this->getDirectory($target . '_js');
\copy($this->templatePath . 'js/bootstrap.min.js', $dir . 'bootstrap.min.js');
\copy($this->templatePath . 'js/popper.min.js', $dir . 'popper.min.js');
\copy($this->templatePath . 'js/d3.min.js', $dir . 'd3.min.js');
\copy($this->templatePath . 'js/jquery.min.js', $dir . 'jquery.min.js');
\copy($this->templatePath . 'js/nv.d3.min.js', $dir . 'nv.d3.min.js');
\copy($this->templatePath . 'js/file.js', $dir . 'file.js');
}
/**
* @throws RuntimeException
*/
private function getDirectory(string $directory): string
{
if (\substr($directory, -1, 1) != \DIRECTORY_SEPARATOR) {
$directory .= \DIRECTORY_SEPARATOR;
}
if (!$this->createDirectory($directory)) {
throw new RuntimeException(
\sprintf(
'Directory "%s" does not exist.',
$directory
)
);
}
return $directory;
}
private function createDirectory(string $directory): bool
{
return !(!\is_dir($directory) && !@\mkdir($directory, 0777, true) && !\is_dir($directory));
}
}

View File

@@ -0,0 +1,277 @@
<?php declare(strict_types=1);
/*
* This file is part of the php-code-coverage package.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage\Report\Html;
use SebastianBergmann\CodeCoverage\Node\AbstractNode;
use SebastianBergmann\CodeCoverage\Node\Directory as DirectoryNode;
use SebastianBergmann\CodeCoverage\Node\File as FileNode;
use SebastianBergmann\CodeCoverage\Version;
use SebastianBergmann\Environment\Runtime;
/**
* Base class for node renderers.
*/
abstract class Renderer
{
/**
* @var string
*/
protected $templatePath;
/**
* @var string
*/
protected $generator;
/**
* @var string
*/
protected $date;
/**
* @var int
*/
protected $lowUpperBound;
/**
* @var int
*/
protected $highLowerBound;
/**
* @var string
*/
protected $version;
public function __construct(string $templatePath, string $generator, string $date, int $lowUpperBound, int $highLowerBound)
{
$this->templatePath = $templatePath;
$this->generator = $generator;
$this->date = $date;
$this->lowUpperBound = $lowUpperBound;
$this->highLowerBound = $highLowerBound;
$this->version = Version::id();
}
protected function renderItemTemplate(\Text_Template $template, array $data): string
{
$numSeparator = '&nbsp;/&nbsp;';
if (isset($data['numClasses']) && $data['numClasses'] > 0) {
$classesLevel = $this->getColorLevel($data['testedClassesPercent']);
$classesNumber = $data['numTestedClasses'] . $numSeparator .
$data['numClasses'];
$classesBar = $this->getCoverageBar(
$data['testedClassesPercent']
);
} else {
$classesLevel = '';
$classesNumber = '0' . $numSeparator . '0';
$classesBar = '';
$data['testedClassesPercentAsString'] = 'n/a';
}
if ($data['numMethods'] > 0) {
$methodsLevel = $this->getColorLevel($data['testedMethodsPercent']);
$methodsNumber = $data['numTestedMethods'] . $numSeparator .
$data['numMethods'];
$methodsBar = $this->getCoverageBar(
$data['testedMethodsPercent']
);
} else {
$methodsLevel = '';
$methodsNumber = '0' . $numSeparator . '0';
$methodsBar = '';
$data['testedMethodsPercentAsString'] = 'n/a';
}
if ($data['numExecutableLines'] > 0) {
$linesLevel = $this->getColorLevel($data['linesExecutedPercent']);
$linesNumber = $data['numExecutedLines'] . $numSeparator .
$data['numExecutableLines'];
$linesBar = $this->getCoverageBar(
$data['linesExecutedPercent']
);
} else {
$linesLevel = '';
$linesNumber = '0' . $numSeparator . '0';
$linesBar = '';
$data['linesExecutedPercentAsString'] = 'n/a';
}
$template->setVar(
[
'icon' => $data['icon'] ?? '',
'crap' => $data['crap'] ?? '',
'name' => $data['name'],
'lines_bar' => $linesBar,
'lines_executed_percent' => $data['linesExecutedPercentAsString'],
'lines_level' => $linesLevel,
'lines_number' => $linesNumber,
'methods_bar' => $methodsBar,
'methods_tested_percent' => $data['testedMethodsPercentAsString'],
'methods_level' => $methodsLevel,
'methods_number' => $methodsNumber,
'classes_bar' => $classesBar,
'classes_tested_percent' => $data['testedClassesPercentAsString'] ?? '',
'classes_level' => $classesLevel,
'classes_number' => $classesNumber,
]
);
return $template->render();
}
protected function setCommonTemplateVariables(\Text_Template $template, AbstractNode $node): void
{
$template->setVar(
[
'id' => $node->getId(),
'full_path' => $node->getPath(),
'path_to_root' => $this->getPathToRoot($node),
'breadcrumbs' => $this->getBreadcrumbs($node),
'date' => $this->date,
'version' => $this->version,
'runtime' => $this->getRuntimeString(),
'generator' => $this->generator,
'low_upper_bound' => $this->lowUpperBound,
'high_lower_bound' => $this->highLowerBound,
]
);
}
protected function getBreadcrumbs(AbstractNode $node): string
{
$breadcrumbs = '';
$path = $node->getPathAsArray();
$pathToRoot = [];
$max = \count($path);
if ($node instanceof FileNode) {
$max--;
}
for ($i = 0; $i < $max; $i++) {
$pathToRoot[] = \str_repeat('../', $i);
}
foreach ($path as $step) {
if ($step !== $node) {
$breadcrumbs .= $this->getInactiveBreadcrumb(
$step,
\array_pop($pathToRoot)
);
} else {
$breadcrumbs .= $this->getActiveBreadcrumb($step);
}
}
return $breadcrumbs;
}
protected function getActiveBreadcrumb(AbstractNode $node): string
{
$buffer = \sprintf(
' <li class="breadcrumb-item active">%s</li>' . "\n",
$node->getName()
);
if ($node instanceof DirectoryNode) {
$buffer .= ' <li class="breadcrumb-item">(<a href="dashboard.html">Dashboard</a>)</li>' . "\n";
}
return $buffer;
}
protected function getInactiveBreadcrumb(AbstractNode $node, string $pathToRoot): string
{
return \sprintf(
' <li class="breadcrumb-item"><a href="%sindex.html">%s</a></li>' . "\n",
$pathToRoot,
$node->getName()
);
}
protected function getPathToRoot(AbstractNode $node): string
{
$id = $node->getId();
$depth = \substr_count($id, '/');
if ($id !== 'index' &&
$node instanceof DirectoryNode) {
$depth++;
}
return \str_repeat('../', $depth);
}
protected function getCoverageBar(float $percent): string
{
$level = $this->getColorLevel($percent);
$template = new \Text_Template(
$this->templatePath . 'coverage_bar.html',
'{{',
'}}'
);
$template->setVar(['level' => $level, 'percent' => \sprintf('%.2F', $percent)]);
return $template->render();
}
protected function getColorLevel(float $percent): string
{
if ($percent <= $this->lowUpperBound) {
return 'danger';
}
if ($percent > $this->lowUpperBound &&
$percent < $this->highLowerBound) {
return 'warning';
}
return 'success';
}
private function getRuntimeString(): string
{
$runtime = new Runtime;
$buffer = \sprintf(
'<a href="%s" target="_top">%s %s</a>',
$runtime->getVendorUrl(),
$runtime->getName(),
$runtime->getVersion()
);
if ($runtime->hasXdebug() && !$runtime->hasPHPDBGCodeCoverage()) {
$buffer .= \sprintf(
' with <a href="https://xdebug.org/">Xdebug %s</a>',
\phpversion('xdebug')
);
}
if ($runtime->hasPCOV() && !$runtime->hasPHPDBGCodeCoverage()) {
$buffer .= \sprintf(
' with <a href="https://github.com/krakjoe/pcov">PCOV %s</a>',
\phpversion('pcov')
);
}
return $buffer;
}
}

View File

@@ -0,0 +1,281 @@
<?php declare(strict_types=1);
/*
* This file is part of the php-code-coverage package.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage\Report\Html;
use SebastianBergmann\CodeCoverage\Node\AbstractNode;
use SebastianBergmann\CodeCoverage\Node\Directory as DirectoryNode;
/**
* Renders the dashboard for a directory node.
*/
final class Dashboard extends Renderer
{
/**
* @throws \InvalidArgumentException
* @throws \RuntimeException
*/
public function render(DirectoryNode $node, string $file): void
{
$classes = $node->getClassesAndTraits();
$template = new \Text_Template(
$this->templatePath . 'dashboard.html',
'{{',
'}}'
);
$this->setCommonTemplateVariables($template, $node);
$baseLink = $node->getId() . '/';
$complexity = $this->complexity($classes, $baseLink);
$coverageDistribution = $this->coverageDistribution($classes);
$insufficientCoverage = $this->insufficientCoverage($classes, $baseLink);
$projectRisks = $this->projectRisks($classes, $baseLink);
$template->setVar(
[
'insufficient_coverage_classes' => $insufficientCoverage['class'],
'insufficient_coverage_methods' => $insufficientCoverage['method'],
'project_risks_classes' => $projectRisks['class'],
'project_risks_methods' => $projectRisks['method'],
'complexity_class' => $complexity['class'],
'complexity_method' => $complexity['method'],
'class_coverage_distribution' => $coverageDistribution['class'],
'method_coverage_distribution' => $coverageDistribution['method'],
]
);
$template->renderTo($file);
}
/**
* Returns the data for the Class/Method Complexity charts.
*/
protected function complexity(array $classes, string $baseLink): array
{
$result = ['class' => [], 'method' => []];
foreach ($classes as $className => $class) {
foreach ($class['methods'] as $methodName => $method) {
if ($className !== '*') {
$methodName = $className . '::' . $methodName;
}
$result['method'][] = [
$method['coverage'],
$method['ccn'],
\sprintf(
'<a href="%s">%s</a>',
\str_replace($baseLink, '', $method['link']),
$methodName
),
];
}
$result['class'][] = [
$class['coverage'],
$class['ccn'],
\sprintf(
'<a href="%s">%s</a>',
\str_replace($baseLink, '', $class['link']),
$className
),
];
}
return [
'class' => \json_encode($result['class']),
'method' => \json_encode($result['method']),
];
}
/**
* Returns the data for the Class / Method Coverage Distribution chart.
*/
protected function coverageDistribution(array $classes): array
{
$result = [
'class' => [
'0%' => 0,
'0-10%' => 0,
'10-20%' => 0,
'20-30%' => 0,
'30-40%' => 0,
'40-50%' => 0,
'50-60%' => 0,
'60-70%' => 0,
'70-80%' => 0,
'80-90%' => 0,
'90-100%' => 0,
'100%' => 0,
],
'method' => [
'0%' => 0,
'0-10%' => 0,
'10-20%' => 0,
'20-30%' => 0,
'30-40%' => 0,
'40-50%' => 0,
'50-60%' => 0,
'60-70%' => 0,
'70-80%' => 0,
'80-90%' => 0,
'90-100%' => 0,
'100%' => 0,
],
];
foreach ($classes as $class) {
foreach ($class['methods'] as $methodName => $method) {
if ($method['coverage'] === 0) {
$result['method']['0%']++;
} elseif ($method['coverage'] === 100) {
$result['method']['100%']++;
} else {
$key = \floor($method['coverage'] / 10) * 10;
$key = $key . '-' . ($key + 10) . '%';
$result['method'][$key]++;
}
}
if ($class['coverage'] === 0) {
$result['class']['0%']++;
} elseif ($class['coverage'] === 100) {
$result['class']['100%']++;
} else {
$key = \floor($class['coverage'] / 10) * 10;
$key = $key . '-' . ($key + 10) . '%';
$result['class'][$key]++;
}
}
return [
'class' => \json_encode(\array_values($result['class'])),
'method' => \json_encode(\array_values($result['method'])),
];
}
/**
* Returns the classes / methods with insufficient coverage.
*/
protected function insufficientCoverage(array $classes, string $baseLink): array
{
$leastTestedClasses = [];
$leastTestedMethods = [];
$result = ['class' => '', 'method' => ''];
foreach ($classes as $className => $class) {
foreach ($class['methods'] as $methodName => $method) {
if ($method['coverage'] < $this->highLowerBound) {
$key = $methodName;
if ($className !== '*') {
$key = $className . '::' . $methodName;
}
$leastTestedMethods[$key] = $method['coverage'];
}
}
if ($class['coverage'] < $this->highLowerBound) {
$leastTestedClasses[$className] = $class['coverage'];
}
}
\asort($leastTestedClasses);
\asort($leastTestedMethods);
foreach ($leastTestedClasses as $className => $coverage) {
$result['class'] .= \sprintf(
' <tr><td><a href="%s">%s</a></td><td class="text-right">%d%%</td></tr>' . "\n",
\str_replace($baseLink, '', $classes[$className]['link']),
$className,
$coverage
);
}
foreach ($leastTestedMethods as $methodName => $coverage) {
[$class, $method] = \explode('::', $methodName);
$result['method'] .= \sprintf(
' <tr><td><a href="%s"><abbr title="%s">%s</abbr></a></td><td class="text-right">%d%%</td></tr>' . "\n",
\str_replace($baseLink, '', $classes[$class]['methods'][$method]['link']),
$methodName,
$method,
$coverage
);
}
return $result;
}
/**
* Returns the project risks according to the CRAP index.
*/
protected function projectRisks(array $classes, string $baseLink): array
{
$classRisks = [];
$methodRisks = [];
$result = ['class' => '', 'method' => ''];
foreach ($classes as $className => $class) {
foreach ($class['methods'] as $methodName => $method) {
if ($method['coverage'] < $this->highLowerBound && $method['ccn'] > 1) {
$key = $methodName;
if ($className !== '*') {
$key = $className . '::' . $methodName;
}
$methodRisks[$key] = $method['crap'];
}
}
if ($class['coverage'] < $this->highLowerBound &&
$class['ccn'] > \count($class['methods'])) {
$classRisks[$className] = $class['crap'];
}
}
\arsort($classRisks);
\arsort($methodRisks);
foreach ($classRisks as $className => $crap) {
$result['class'] .= \sprintf(
' <tr><td><a href="%s">%s</a></td><td class="text-right">%d</td></tr>' . "\n",
\str_replace($baseLink, '', $classes[$className]['link']),
$className,
$crap
);
}
foreach ($methodRisks as $methodName => $crap) {
[$class, $method] = \explode('::', $methodName);
$result['method'] .= \sprintf(
' <tr><td><a href="%s"><abbr title="%s">%s</abbr></a></td><td class="text-right">%d</td></tr>' . "\n",
\str_replace($baseLink, '', $classes[$class]['methods'][$method]['link']),
$methodName,
$method,
$crap
);
}
return $result;
}
protected function getActiveBreadcrumb(AbstractNode $node): string
{
return \sprintf(
' <li class="breadcrumb-item"><a href="index.html">%s</a></li>' . "\n" .
' <li class="breadcrumb-item active">(Dashboard)</li>' . "\n",
$node->getName()
);
}
}

View File

@@ -0,0 +1,98 @@
<?php declare(strict_types=1);
/*
* This file is part of the php-code-coverage package.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage\Report\Html;
use SebastianBergmann\CodeCoverage\Node\AbstractNode as Node;
use SebastianBergmann\CodeCoverage\Node\Directory as DirectoryNode;
/**
* Renders a directory node.
*/
final class Directory extends Renderer
{
/**
* @throws \InvalidArgumentException
* @throws \RuntimeException
*/
public function render(DirectoryNode $node, string $file): void
{
$template = new \Text_Template($this->templatePath . 'directory.html', '{{', '}}');
$this->setCommonTemplateVariables($template, $node);
$items = $this->renderItem($node, true);
foreach ($node->getDirectories() as $item) {
$items .= $this->renderItem($item);
}
foreach ($node->getFiles() as $item) {
$items .= $this->renderItem($item);
}
$template->setVar(
[
'id' => $node->getId(),
'items' => $items,
]
);
$template->renderTo($file);
}
protected function renderItem(Node $node, bool $total = false): string
{
$data = [
'numClasses' => $node->getNumClassesAndTraits(),
'numTestedClasses' => $node->getNumTestedClassesAndTraits(),
'numMethods' => $node->getNumFunctionsAndMethods(),
'numTestedMethods' => $node->getNumTestedFunctionsAndMethods(),
'linesExecutedPercent' => $node->getLineExecutedPercent(false),
'linesExecutedPercentAsString' => $node->getLineExecutedPercent(),
'numExecutedLines' => $node->getNumExecutedLines(),
'numExecutableLines' => $node->getNumExecutableLines(),
'testedMethodsPercent' => $node->getTestedFunctionsAndMethodsPercent(false),
'testedMethodsPercentAsString' => $node->getTestedFunctionsAndMethodsPercent(),
'testedClassesPercent' => $node->getTestedClassesAndTraitsPercent(false),
'testedClassesPercentAsString' => $node->getTestedClassesAndTraitsPercent(),
];
if ($total) {
$data['name'] = 'Total';
} else {
if ($node instanceof DirectoryNode) {
$data['name'] = \sprintf(
'<a href="%s/index.html">%s</a>',
$node->getName(),
$node->getName()
);
$up = \str_repeat('../', \count($node->getPathAsArray()) - 2);
$data['icon'] = \sprintf('<img src="%s_icons/file-directory.svg" class="octicon" />', $up);
} else {
$data['name'] = \sprintf(
'<a href="%s.html">%s</a>',
$node->getName(),
$node->getName()
);
$up = \str_repeat('../', \count($node->getPathAsArray()) - 2);
$data['icon'] = \sprintf('<img src="%s_icons/file-code.svg" class="octicon" />', $up);
}
}
return $this->renderItemTemplate(
new \Text_Template($this->templatePath . 'directory_item.html', '{{', '}}'),
$data
);
}
}

View File

@@ -0,0 +1,529 @@
<?php declare(strict_types=1);
/*
* This file is part of the php-code-coverage package.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage\Report\Html;
use SebastianBergmann\CodeCoverage\Node\File as FileNode;
use SebastianBergmann\CodeCoverage\Util;
/**
* Renders a file node.
*/
final class File extends Renderer
{
/**
* @var int
*/
private $htmlSpecialCharsFlags = \ENT_COMPAT | \ENT_HTML401 | \ENT_SUBSTITUTE;
/**
* @throws \RuntimeException
*/
public function render(FileNode $node, string $file): void
{
$template = new \Text_Template($this->templatePath . 'file.html', '{{', '}}');
$template->setVar(
[
'items' => $this->renderItems($node),
'lines' => $this->renderSource($node),
]
);
$this->setCommonTemplateVariables($template, $node);
$template->renderTo($file);
}
protected function renderItems(FileNode $node): string
{
$template = new \Text_Template($this->templatePath . 'file_item.html', '{{', '}}');
$methodItemTemplate = new \Text_Template(
$this->templatePath . 'method_item.html',
'{{',
'}}'
);
$items = $this->renderItemTemplate(
$template,
[
'name' => 'Total',
'numClasses' => $node->getNumClassesAndTraits(),
'numTestedClasses' => $node->getNumTestedClassesAndTraits(),
'numMethods' => $node->getNumFunctionsAndMethods(),
'numTestedMethods' => $node->getNumTestedFunctionsAndMethods(),
'linesExecutedPercent' => $node->getLineExecutedPercent(false),
'linesExecutedPercentAsString' => $node->getLineExecutedPercent(),
'numExecutedLines' => $node->getNumExecutedLines(),
'numExecutableLines' => $node->getNumExecutableLines(),
'testedMethodsPercent' => $node->getTestedFunctionsAndMethodsPercent(false),
'testedMethodsPercentAsString' => $node->getTestedFunctionsAndMethodsPercent(),
'testedClassesPercent' => $node->getTestedClassesAndTraitsPercent(false),
'testedClassesPercentAsString' => $node->getTestedClassesAndTraitsPercent(),
'crap' => '<abbr title="Change Risk Anti-Patterns (CRAP) Index">CRAP</abbr>',
]
);
$items .= $this->renderFunctionItems(
$node->getFunctions(),
$methodItemTemplate
);
$items .= $this->renderTraitOrClassItems(
$node->getTraits(),
$template,
$methodItemTemplate
);
$items .= $this->renderTraitOrClassItems(
$node->getClasses(),
$template,
$methodItemTemplate
);
return $items;
}
protected function renderTraitOrClassItems(array $items, \Text_Template $template, \Text_Template $methodItemTemplate): string
{
$buffer = '';
if (empty($items)) {
return $buffer;
}
foreach ($items as $name => $item) {
$numMethods = 0;
$numTestedMethods = 0;
foreach ($item['methods'] as $method) {
if ($method['executableLines'] > 0) {
$numMethods++;
if ($method['executedLines'] === $method['executableLines']) {
$numTestedMethods++;
}
}
}
if ($item['executableLines'] > 0) {
$numClasses = 1;
$numTestedClasses = $numTestedMethods == $numMethods ? 1 : 0;
$linesExecutedPercentAsString = Util::percent(
$item['executedLines'],
$item['executableLines'],
true
);
} else {
$numClasses = 'n/a';
$numTestedClasses = 'n/a';
$linesExecutedPercentAsString = 'n/a';
}
$buffer .= $this->renderItemTemplate(
$template,
[
'name' => $this->abbreviateClassName($name),
'numClasses' => $numClasses,
'numTestedClasses' => $numTestedClasses,
'numMethods' => $numMethods,
'numTestedMethods' => $numTestedMethods,
'linesExecutedPercent' => Util::percent(
$item['executedLines'],
$item['executableLines'],
false
),
'linesExecutedPercentAsString' => $linesExecutedPercentAsString,
'numExecutedLines' => $item['executedLines'],
'numExecutableLines' => $item['executableLines'],
'testedMethodsPercent' => Util::percent(
$numTestedMethods,
$numMethods
),
'testedMethodsPercentAsString' => Util::percent(
$numTestedMethods,
$numMethods,
true
),
'testedClassesPercent' => Util::percent(
$numTestedMethods == $numMethods ? 1 : 0,
1
),
'testedClassesPercentAsString' => Util::percent(
$numTestedMethods == $numMethods ? 1 : 0,
1,
true
),
'crap' => $item['crap'],
]
);
foreach ($item['methods'] as $method) {
$buffer .= $this->renderFunctionOrMethodItem(
$methodItemTemplate,
$method,
'&nbsp;'
);
}
}
return $buffer;
}
protected function renderFunctionItems(array $functions, \Text_Template $template): string
{
if (empty($functions)) {
return '';
}
$buffer = '';
foreach ($functions as $function) {
$buffer .= $this->renderFunctionOrMethodItem(
$template,
$function
);
}
return $buffer;
}
protected function renderFunctionOrMethodItem(\Text_Template $template, array $item, string $indent = ''): string
{
$numMethods = 0;
$numTestedMethods = 0;
if ($item['executableLines'] > 0) {
$numMethods = 1;
if ($item['executedLines'] === $item['executableLines']) {
$numTestedMethods = 1;
}
}
return $this->renderItemTemplate(
$template,
[
'name' => \sprintf(
'%s<a href="#%d"><abbr title="%s">%s</abbr></a>',
$indent,
$item['startLine'],
\htmlspecialchars($item['signature'], $this->htmlSpecialCharsFlags),
$item['functionName'] ?? $item['methodName']
),
'numMethods' => $numMethods,
'numTestedMethods' => $numTestedMethods,
'linesExecutedPercent' => Util::percent(
$item['executedLines'],
$item['executableLines']
),
'linesExecutedPercentAsString' => Util::percent(
$item['executedLines'],
$item['executableLines'],
true
),
'numExecutedLines' => $item['executedLines'],
'numExecutableLines' => $item['executableLines'],
'testedMethodsPercent' => Util::percent(
$numTestedMethods,
1
),
'testedMethodsPercentAsString' => Util::percent(
$numTestedMethods,
1,
true
),
'crap' => $item['crap'],
]
);
}
protected function renderSource(FileNode $node): string
{
$coverageData = $node->getCoverageData();
$testData = $node->getTestData();
$codeLines = $this->loadFile($node->getPath());
$lines = '';
$i = 1;
foreach ($codeLines as $line) {
$trClass = '';
$popoverContent = '';
$popoverTitle = '';
if (\array_key_exists($i, $coverageData)) {
$numTests = ($coverageData[$i] ? \count($coverageData[$i]) : 0);
if ($coverageData[$i] === null) {
$trClass = ' class="warning"';
} elseif ($numTests == 0) {
$trClass = ' class="danger"';
} else {
$lineCss = 'covered-by-large-tests';
$popoverContent = '<ul>';
if ($numTests > 1) {
$popoverTitle = $numTests . ' tests cover line ' . $i;
} else {
$popoverTitle = '1 test covers line ' . $i;
}
foreach ($coverageData[$i] as $test) {
if ($lineCss == 'covered-by-large-tests' && $testData[$test]['size'] == 'medium') {
$lineCss = 'covered-by-medium-tests';
} elseif ($testData[$test]['size'] == 'small') {
$lineCss = 'covered-by-small-tests';
}
switch ($testData[$test]['status']) {
case 0:
switch ($testData[$test]['size']) {
case 'small':
$testCSS = ' class="covered-by-small-tests"';
break;
case 'medium':
$testCSS = ' class="covered-by-medium-tests"';
break;
default:
$testCSS = ' class="covered-by-large-tests"';
break;
}
break;
case 1:
case 2:
$testCSS = ' class="warning"';
break;
case 3:
$testCSS = ' class="danger"';
break;
case 4:
$testCSS = ' class="danger"';
break;
default:
$testCSS = '';
}
$popoverContent .= \sprintf(
'<li%s>%s</li>',
$testCSS,
\htmlspecialchars($test, $this->htmlSpecialCharsFlags)
);
}
$popoverContent .= '</ul>';
$trClass = ' class="' . $lineCss . ' popin"';
}
}
$popover = '';
if (!empty($popoverTitle)) {
$popover = \sprintf(
' data-title="%s" data-content="%s" data-placement="top" data-html="true"',
$popoverTitle,
\htmlspecialchars($popoverContent, $this->htmlSpecialCharsFlags)
);
}
$lines .= \sprintf(
' <tr%s><td%s><div align="right"><a name="%d"></a><a href="#%d">%d</a></div></td><td class="codeLine">%s</td></tr>' . "\n",
$trClass,
$popover,
$i,
$i,
$i,
$line
);
$i++;
}
return $lines;
}
/**
* @param string $file
*/
protected function loadFile($file): array
{
$buffer = \file_get_contents($file);
$tokens = \token_get_all($buffer);
$result = [''];
$i = 0;
$stringFlag = false;
$fileEndsWithNewLine = \substr($buffer, -1) == "\n";
unset($buffer);
foreach ($tokens as $j => $token) {
if (\is_string($token)) {
if ($token === '"' && $tokens[$j - 1] !== '\\') {
$result[$i] .= \sprintf(
'<span class="string">%s</span>',
\htmlspecialchars($token, $this->htmlSpecialCharsFlags)
);
$stringFlag = !$stringFlag;
} else {
$result[$i] .= \sprintf(
'<span class="keyword">%s</span>',
\htmlspecialchars($token, $this->htmlSpecialCharsFlags)
);
}
continue;
}
[$token, $value] = $token;
$value = \str_replace(
["\t", ' '],
['&nbsp;&nbsp;&nbsp;&nbsp;', '&nbsp;'],
\htmlspecialchars($value, $this->htmlSpecialCharsFlags)
);
if ($value === "\n") {
$result[++$i] = '';
} else {
$lines = \explode("\n", $value);
foreach ($lines as $jj => $line) {
$line = \trim($line);
if ($line !== '') {
if ($stringFlag) {
$colour = 'string';
} else {
switch ($token) {
case \T_INLINE_HTML:
$colour = 'html';
break;
case \T_COMMENT:
case \T_DOC_COMMENT:
$colour = 'comment';
break;
case \T_ABSTRACT:
case \T_ARRAY:
case \T_AS:
case \T_BREAK:
case \T_CALLABLE:
case \T_CASE:
case \T_CATCH:
case \T_CLASS:
case \T_CLONE:
case \T_CONTINUE:
case \T_DEFAULT:
case \T_ECHO:
case \T_ELSE:
case \T_ELSEIF:
case \T_EMPTY:
case \T_ENDDECLARE:
case \T_ENDFOR:
case \T_ENDFOREACH:
case \T_ENDIF:
case \T_ENDSWITCH:
case \T_ENDWHILE:
case \T_EXIT:
case \T_EXTENDS:
case \T_FINAL:
case \T_FINALLY:
case \T_FOREACH:
case \T_FUNCTION:
case \T_GLOBAL:
case \T_IF:
case \T_IMPLEMENTS:
case \T_INCLUDE:
case \T_INCLUDE_ONCE:
case \T_INSTANCEOF:
case \T_INSTEADOF:
case \T_INTERFACE:
case \T_ISSET:
case \T_LOGICAL_AND:
case \T_LOGICAL_OR:
case \T_LOGICAL_XOR:
case \T_NAMESPACE:
case \T_NEW:
case \T_PRIVATE:
case \T_PROTECTED:
case \T_PUBLIC:
case \T_REQUIRE:
case \T_REQUIRE_ONCE:
case \T_RETURN:
case \T_STATIC:
case \T_THROW:
case \T_TRAIT:
case \T_TRY:
case \T_UNSET:
case \T_USE:
case \T_VAR:
case \T_WHILE:
case \T_YIELD:
$colour = 'keyword';
break;
default:
$colour = 'default';
}
}
$result[$i] .= \sprintf(
'<span class="%s">%s</span>',
$colour,
$line
);
}
if (isset($lines[$jj + 1])) {
$result[++$i] = '';
}
}
}
}
if ($fileEndsWithNewLine) {
unset($result[\count($result) - 1]);
}
return $result;
}
private function abbreviateClassName(string $className): string
{
$tmp = \explode('\\', $className);
if (\count($tmp) > 1) {
$className = \sprintf(
'<abbr title="%s">%s</abbr>',
$className,
\array_pop($tmp)
);
}
return $className;
}
}

View File

@@ -0,0 +1,5 @@
<div class="progress">
<div class="progress-bar bg-{{level}}" role="progressbar" aria-valuenow="{{percent}}" aria-valuemin="0" aria-valuemax="100" style="width: {{percent}}%">
<span class="sr-only">{{percent}}% covered ({{level}})</span>
</div>
</div>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,5 @@
.octicon {
display: inline-block;
vertical-align: text-top;
fill: currentColor;
}

View File

@@ -0,0 +1,122 @@
body {
padding-top: 10px;
}
.popover {
max-width: none;
}
.octicon {
margin-right:.25em;
}
.table-bordered>thead>tr>td {
border-bottom-width: 1px;
}
.table tbody>tr>td, .table thead>tr>td {
padding-top: 3px;
padding-bottom: 3px;
}
.table-condensed tbody>tr>td {
padding-top: 0;
padding-bottom: 0;
}
.table .progress {
margin-bottom: inherit;
}
.table-borderless th, .table-borderless td {
border: 0 !important;
}
.table tbody tr.covered-by-large-tests, li.covered-by-large-tests, tr.success, td.success, li.success, span.success {
background-color: #dff0d8;
}
.table tbody tr.covered-by-medium-tests, li.covered-by-medium-tests {
background-color: #c3e3b5;
}
.table tbody tr.covered-by-small-tests, li.covered-by-small-tests {
background-color: #99cb84;
}
.table tbody tr.danger, .table tbody td.danger, li.danger, span.danger {
background-color: #f2dede;
}
.table tbody td.warning, li.warning, span.warning {
background-color: #fcf8e3;
}
.table tbody td.info {
background-color: #d9edf7;
}
td.big {
width: 117px;
}
td.small {
}
td.codeLine {
font-family: "Source Code Pro", "SFMono-Regular", Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
white-space: pre;
}
td span.comment {
color: #888a85;
}
td span.default {
color: #2e3436;
}
td span.html {
color: #888a85;
}
td span.keyword {
color: #2e3436;
font-weight: bold;
}
pre span.string {
color: #2e3436;
}
span.success, span.warning, span.danger {
margin-right: 2px;
padding-left: 10px;
padding-right: 10px;
text-align: center;
}
#classCoverageDistribution, #classComplexity {
height: 200px;
width: 475px;
}
#toplink {
position: fixed;
left: 5px;
bottom: 5px;
outline: 0;
}
svg text {
font-family: "Lucida Grande", "Lucida Sans Unicode", Verdana, Arial, Helvetica, sans-serif;
font-size: 11px;
color: #666;
fill: #666;
}
.scrollbox {
height:245px;
overflow-x:hidden;
overflow-y:scroll;
}

View File

@@ -0,0 +1,281 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Dashboard for {{full_path}}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="{{path_to_root}}_css/bootstrap.min.css" rel="stylesheet" type="text/css">
<link href="{{path_to_root}}_css/nv.d3.min.css" rel="stylesheet" type="text/css">
<link href="{{path_to_root}}_css/style.css" rel="stylesheet" type="text/css">
<link href="{{path_to_root}}_css/custom.css" rel="stylesheet" type="text/css">
</head>
<body>
<header>
<div class="container-fluid">
<div class="row">
<div class="col-md-12">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
{{breadcrumbs}}
</ol>
</nav>
</div>
</div>
</div>
</header>
<div class="container-fluid">
<div class="row">
<div class="col-md-12">
<h2>Classes</h2>
</div>
</div>
<div class="row">
<div class="col-md-6">
<h3>Coverage Distribution</h3>
<div id="classCoverageDistribution" style="height: 300px;">
<svg></svg>
</div>
</div>
<div class="col-md-6">
<h3>Complexity</h3>
<div id="classComplexity" style="height: 300px;">
<svg></svg>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<h3>Insufficient Coverage</h3>
<div class="scrollbox">
<table class="table">
<thead>
<tr>
<th>Class</th>
<th class="text-right">Coverage</th>
</tr>
</thead>
<tbody>
{{insufficient_coverage_classes}}
</tbody>
</table>
</div>
</div>
<div class="col-md-6">
<h3>Project Risks</h3>
<div class="scrollbox">
<table class="table">
<thead>
<tr>
<th>Class</th>
<th class="text-right"><abbr title="Change Risk Anti-Patterns (CRAP) Index">CRAP</abbr></th>
</tr>
</thead>
<tbody>
{{project_risks_classes}}
</tbody>
</table>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12">
<h2>Methods</h2>
</div>
</div>
<div class="row">
<div class="col-md-6">
<h3>Coverage Distribution</h3>
<div id="methodCoverageDistribution" style="height: 300px;">
<svg></svg>
</div>
</div>
<div class="col-md-6">
<h3>Complexity</h3>
<div id="methodComplexity" style="height: 300px;">
<svg></svg>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<h3>Insufficient Coverage</h3>
<div class="scrollbox">
<table class="table">
<thead>
<tr>
<th>Method</th>
<th class="text-right">Coverage</th>
</tr>
</thead>
<tbody>
{{insufficient_coverage_methods}}
</tbody>
</table>
</div>
</div>
<div class="col-md-6">
<h3>Project Risks</h3>
<div class="scrollbox">
<table class="table">
<thead>
<tr>
<th>Method</th>
<th class="text-right"><abbr title="Change Risk Anti-Patterns (CRAP) Index">CRAP</abbr></th>
</tr>
</thead>
<tbody>
{{project_risks_methods}}
</tbody>
</table>
</div>
</div>
</div>
<footer>
<hr/>
<p>
<small>Generated by <a href="https://github.com/sebastianbergmann/php-code-coverage" target="_top">php-code-coverage {{version}}</a> using {{runtime}}{{generator}} at {{date}}.</small>
</p>
</footer>
</div>
<script src="{{path_to_root}}_js/jquery.min.js" type="text/javascript"></script>
<script src="{{path_to_root}}_js/d3.min.js" type="text/javascript"></script>
<script src="{{path_to_root}}_js/nv.d3.min.js" type="text/javascript"></script>
<script type="text/javascript">
$(document).ready(function() {
nv.addGraph(function() {
var chart = nv.models.multiBarChart();
chart.tooltips(false)
.showControls(false)
.showLegend(false)
.reduceXTicks(false)
.staggerLabels(true)
.yAxis.tickFormat(d3.format('d'));
d3.select('#classCoverageDistribution svg')
.datum(getCoverageDistributionData({{class_coverage_distribution}}, "Class Coverage"))
.transition().duration(500).call(chart);
nv.utils.windowResize(chart.update);
return chart;
});
nv.addGraph(function() {
var chart = nv.models.multiBarChart();
chart.tooltips(false)
.showControls(false)
.showLegend(false)
.reduceXTicks(false)
.staggerLabels(true)
.yAxis.tickFormat(d3.format('d'));
d3.select('#methodCoverageDistribution svg')
.datum(getCoverageDistributionData({{method_coverage_distribution}}, "Method Coverage"))
.transition().duration(500).call(chart);
nv.utils.windowResize(chart.update);
return chart;
});
function getCoverageDistributionData(data, label) {
var labels = [
'0%',
'0-10%',
'10-20%',
'20-30%',
'30-40%',
'40-50%',
'50-60%',
'60-70%',
'70-80%',
'80-90%',
'90-100%',
'100%'
];
var values = [];
$.each(labels, function(key) {
values.push({x: labels[key], y: data[key]});
});
return [
{
key: label,
values: values,
color: "#4572A7"
}
];
}
nv.addGraph(function() {
var chart = nv.models.scatterChart()
.showDistX(true)
.showDistY(true)
.showLegend(false)
.forceX([0, 100]);
chart.tooltipContent(function(graph) {
return '<p>' + graph.point.class + '</p>';
});
chart.xAxis.axisLabel('Code Coverage (in percent)');
chart.yAxis.axisLabel('Cyclomatic Complexity');
d3.select('#classComplexity svg')
.datum(getComplexityData({{complexity_class}}, 'Class Complexity'))
.transition()
.duration(500)
.call(chart);
nv.utils.windowResize(chart.update);
return chart;
});
nv.addGraph(function() {
var chart = nv.models.scatterChart()
.showDistX(true)
.showDistY(true)
.showLegend(false)
.forceX([0, 100]);
chart.tooltipContent(function(graph) {
return '<p>' + graph.point.class + '</p>';
});
chart.xAxis.axisLabel('Code Coverage (in percent)');
chart.yAxis.axisLabel('Method Complexity');
d3.select('#methodComplexity svg')
.datum(getComplexityData({{complexity_method}}, 'Method Complexity'))
.transition()
.duration(500)
.call(chart);
nv.utils.windowResize(chart.update);
return chart;
});
function getComplexityData(data, label) {
var values = [];
$.each(data, function(key) {
var value = Math.round(data[key][0]*100) / 100;
values.push({
x: value,
y: data[key][1],
class: data[key][2],
size: 0.05,
shape: 'diamond'
});
});
return [
{
key: label,
values: values,
color: "#4572A7"
}
];
}
});
</script>
</body>
</html>

View File

@@ -0,0 +1,60 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Code Coverage for {{full_path}}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="{{path_to_root}}_css/bootstrap.min.css" rel="stylesheet" type="text/css">
<link href="{{path_to_root}}_css/octicons.css" rel="stylesheet" type="text/css">
<link href="{{path_to_root}}_css/style.css" rel="stylesheet" type="text/css">
<link href="{{path_to_root}}_css/custom.css" rel="stylesheet" type="text/css">
</head>
<body>
<header>
<div class="container-fluid">
<div class="row">
<div class="col-md-12">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
{{breadcrumbs}}
</ol>
</nav>
</div>
</div>
</div>
</header>
<div class="container-fluid">
<div class="table-responsive">
<table class="table table-bordered">
<thead>
<tr>
<td>&nbsp;</td>
<td colspan="9"><div align="center"><strong>Code Coverage</strong></div></td>
</tr>
<tr>
<td>&nbsp;</td>
<td colspan="3"><div align="center"><strong>Lines</strong></div></td>
<td colspan="3"><div align="center"><strong>Functions and Methods</strong></div></td>
<td colspan="3"><div align="center"><strong>Classes and Traits</strong></div></td>
</tr>
</thead>
<tbody>
{{items}}
</tbody>
</table>
</div>
<footer>
<hr/>
<h4>Legend</h4>
<p>
<span class="danger"><strong>Low</strong>: 0% to {{low_upper_bound}}%</span>
<span class="warning"><strong>Medium</strong>: {{low_upper_bound}}% to {{high_lower_bound}}%</span>
<span class="success"><strong>High</strong>: {{high_lower_bound}}% to 100%</span>
</p>
<p>
<small>Generated by <a href="https://github.com/sebastianbergmann/php-code-coverage" target="_top">php-code-coverage {{version}}</a> using {{runtime}}{{generator}} at {{date}}.</small>
</p>
</footer>
</div>
</body>
</html>

View File

@@ -0,0 +1,13 @@
<tr>
<td class="{{lines_level}}">{{icon}}{{name}}</td>
<td class="{{lines_level}} big">{{lines_bar}}</td>
<td class="{{lines_level}} small"><div align="right">{{lines_executed_percent}}</div></td>
<td class="{{lines_level}} small"><div align="right">{{lines_number}}</div></td>
<td class="{{methods_level}} big">{{methods_bar}}</td>
<td class="{{methods_level}} small"><div align="right">{{methods_tested_percent}}</div></td>
<td class="{{methods_level}} small"><div align="right">{{methods_number}}</div></td>
<td class="{{classes_level}} big">{{classes_bar}}</td>
<td class="{{classes_level}} small"><div align="right">{{classes_tested_percent}}</div></td>
<td class="{{classes_level}} small"><div align="right">{{classes_number}}</div></td>
</tr>

View File

@@ -0,0 +1,72 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Code Coverage for {{full_path}}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="{{path_to_root}}_css/bootstrap.min.css" rel="stylesheet" type="text/css">
<link href="{{path_to_root}}_css/octicons.css" rel="stylesheet" type="text/css">
<link href="{{path_to_root}}_css/style.css" rel="stylesheet" type="text/css">
<link href="{{path_to_root}}_css/custom.css" rel="stylesheet" type="text/css">
</head>
<body>
<header>
<div class="container-fluid">
<div class="row">
<div class="col-md-12">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
{{breadcrumbs}}
</ol>
</nav>
</div>
</div>
</div>
</header>
<div class="container-fluid">
<div class="table-responsive">
<table class="table table-bordered">
<thead>
<tr>
<td>&nbsp;</td>
<td colspan="10"><div align="center"><strong>Code Coverage</strong></div></td>
</tr>
<tr>
<td>&nbsp;</td>
<td colspan="3"><div align="center"><strong>Classes and Traits</strong></div></td>
<td colspan="4"><div align="center"><strong>Functions and Methods</strong></div></td>
<td colspan="3"><div align="center"><strong>Lines</strong></div></td>
</tr>
</thead>
<tbody>
{{items}}
</tbody>
</table>
</div>
<table id="code" class="table table-borderless table-condensed">
<tbody>
{{lines}}
</tbody>
</table>
<footer>
<hr/>
<h4>Legend</h4>
<p>
<span class="success"><strong>Executed</strong></span>
<span class="danger"><strong>Not Executed</strong></span>
<span class="warning"><strong>Dead Code</strong></span>
</p>
<p>
<small>Generated by <a href="https://github.com/sebastianbergmann/php-code-coverage" target="_top">php-code-coverage {{version}}</a> using {{runtime}}{{generator}} at {{date}}.</small>
</p>
<a title="Back to the top" id="toplink" href="#">
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="16" viewBox="0 0 12 16"><path fill-rule="evenodd" d="M12 11L6 5l-6 6h12z"/></svg>
</a>
</footer>
</div>
<script src="{{path_to_root}}_js/jquery.min.js" type="text/javascript"></script>
<script src="{{path_to_root}}_js/popper.min.js" type="text/javascript"></script>
<script src="{{path_to_root}}_js/bootstrap.min.js" type="text/javascript"></script>
<script src="{{path_to_root}}_js/file.js" type="text/javascript"></script>
</body>
</html>

View File

@@ -0,0 +1,14 @@
<tr>
<td class="{{classes_level}}">{{name}}</td>
<td class="{{classes_level}} big">{{classes_bar}}</td>
<td class="{{classes_level}} small"><div align="right">{{classes_tested_percent}}</div></td>
<td class="{{classes_level}} small"><div align="right">{{classes_number}}</div></td>
<td class="{{methods_level}} big">{{methods_bar}}</td>
<td class="{{methods_level}} small"><div align="right">{{methods_tested_percent}}</div></td>
<td class="{{methods_level}} small"><div align="right">{{methods_number}}</div></td>
<td class="{{methods_level}} small">{{crap}}</td>
<td class="{{lines_level}} big">{{lines_bar}}</td>
<td class="{{lines_level}} small"><div align="right">{{lines_executed_percent}}</div></td>
<td class="{{lines_level}} small"><div align="right">{{lines_number}}</div></td>
</tr>

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="16" viewBox="0 0 12 16"><path fill-rule="evenodd" d="M8.5 1H1c-.55 0-1 .45-1 1v12c0 .55.45 1 1 1h10c.55 0 1-.45 1-1V4.5L8.5 1zM11 14H1V2h7l3 3v9zM5 6.98L3.5 8.5 5 10l-.5 1L2 8.5 4.5 6l.5.98zM7.5 6L10 8.5 7.5 11l-.5-.98L8.5 8.5 7 7l.5-1z"/></svg>

After

Width:  |  Height:  |  Size: 304 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="16" viewBox="0 0 14 16"><path fill-rule="evenodd" d="M13 4H7V3c0-.66-.31-1-1-1H1c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h12c.55 0 1-.45 1-1V5c0-.55-.45-1-1-1zM6 4H1V3h5v1z"/></svg>

After

Width:  |  Height:  |  Size: 234 B

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,62 @@
$(function() {
var $window = $(window)
, $top_link = $('#toplink')
, $body = $('body, html')
, offset = $('#code').offset().top
, hidePopover = function ($target) {
$target.data('popover-hover', false);
setTimeout(function () {
if (!$target.data('popover-hover')) {
$target.popover('hide');
}
}, 300);
};
$top_link.hide().click(function(event) {
event.preventDefault();
$body.animate({scrollTop:0}, 800);
});
$window.scroll(function() {
if($window.scrollTop() > offset) {
$top_link.fadeIn();
} else {
$top_link.fadeOut();
}
}).scroll();
$('.popin')
.popover({trigger: 'manual'})
.on({
'mouseenter.popover': function () {
var $target = $(this);
var $container = $target.children().first();
$target.data('popover-hover', true);
// popover already displayed
if ($target.next('.popover').length) {
return;
}
// show the popover
$container.popover('show');
// register mouse events on the popover
$target.next('.popover:not(.popover-initialized)')
.on({
'mouseenter': function () {
$target.data('popover-hover', true);
},
'mouseleave': function () {
hidePopover($container);
}
})
.addClass('popover-initialized');
},
'mouseleave.popover': function () {
hidePopover($(this).children().first());
}
});
});

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,11 @@
<tr>
<td class="{{methods_level}}" colspan="4">{{name}}</td>
<td class="{{methods_level}} big">{{methods_bar}}</td>
<td class="{{methods_level}} small"><div align="right">{{methods_tested_percent}}</div></td>
<td class="{{methods_level}} small"><div align="right">{{methods_number}}</div></td>
<td class="{{methods_level}} small">{{crap}}</td>
<td class="{{lines_level}} big">{{lines_bar}}</td>
<td class="{{lines_level}} small"><div align="right">{{lines_executed_percent}}</div></td>
<td class="{{lines_level}} small"><div align="right">{{lines_number}}</div></td>
</tr>

View File

@@ -0,0 +1,64 @@
<?php declare(strict_types=1);
/*
* This file is part of the php-code-coverage package.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage\Report;
use SebastianBergmann\CodeCoverage\CodeCoverage;
use SebastianBergmann\CodeCoverage\RuntimeException;
/**
* Uses var_export() to write a SebastianBergmann\CodeCoverage\CodeCoverage object to a file.
*/
final class PHP
{
/**
* @throws \SebastianBergmann\CodeCoverage\RuntimeException
*/
public function process(CodeCoverage $coverage, ?string $target = null): string
{
$filter = $coverage->filter();
$buffer = \sprintf(
'<?php
$coverage = new SebastianBergmann\CodeCoverage\CodeCoverage;
$coverage->setData(%s);
$coverage->setTests(%s);
$filter = $coverage->filter();
$filter->setWhitelistedFiles(%s);
return $coverage;',
\var_export($coverage->getData(true), true),
\var_export($coverage->getTests(), true),
\var_export($filter->getWhitelistedFiles(), true)
);
if ($target !== null) {
if (!$this->createDirectory(\dirname($target))) {
throw new \RuntimeException(\sprintf('Directory "%s" was not created', \dirname($target)));
}
if (@\file_put_contents($target, $buffer) === false) {
throw new RuntimeException(
\sprintf(
'Could not write to "%s',
$target
)
);
}
}
return $buffer;
}
private function createDirectory(string $directory): bool
{
return !(!\is_dir($directory) && !@\mkdir($directory, 0777, true) && !\is_dir($directory));
}
}

View File

@@ -0,0 +1,283 @@
<?php declare(strict_types=1);
/*
* This file is part of the php-code-coverage package.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage\Report;
use SebastianBergmann\CodeCoverage\CodeCoverage;
use SebastianBergmann\CodeCoverage\Node\File;
use SebastianBergmann\CodeCoverage\Util;
/**
* Generates human readable output from a code coverage object.
*
* The output gets put into a text file our written to the CLI.
*/
final class Text
{
/**
* @var string
*/
private const COLOR_GREEN = "\x1b[30;42m";
/**
* @var string
*/
private const COLOR_YELLOW = "\x1b[30;43m";
/**
* @var string
*/
private const COLOR_RED = "\x1b[37;41m";
/**
* @var string
*/
private const COLOR_HEADER = "\x1b[1;37;40m";
/**
* @var string
*/
private const COLOR_RESET = "\x1b[0m";
/**
* @var string
*/
private const COLOR_EOL = "\x1b[2K";
/**
* @var int
*/
private $lowUpperBound;
/**
* @var int
*/
private $highLowerBound;
/**
* @var bool
*/
private $showUncoveredFiles;
/**
* @var bool
*/
private $showOnlySummary;
public function __construct(int $lowUpperBound = 50, int $highLowerBound = 90, bool $showUncoveredFiles = false, bool $showOnlySummary = false)
{
$this->lowUpperBound = $lowUpperBound;
$this->highLowerBound = $highLowerBound;
$this->showUncoveredFiles = $showUncoveredFiles;
$this->showOnlySummary = $showOnlySummary;
}
public function process(CodeCoverage $coverage, bool $showColors = false): string
{
$output = \PHP_EOL . \PHP_EOL;
$report = $coverage->getReport();
$colors = [
'header' => '',
'classes' => '',
'methods' => '',
'lines' => '',
'reset' => '',
'eol' => '',
];
if ($showColors) {
$colors['classes'] = $this->getCoverageColor(
$report->getNumTestedClassesAndTraits(),
$report->getNumClassesAndTraits()
);
$colors['methods'] = $this->getCoverageColor(
$report->getNumTestedMethods(),
$report->getNumMethods()
);
$colors['lines'] = $this->getCoverageColor(
$report->getNumExecutedLines(),
$report->getNumExecutableLines()
);
$colors['reset'] = self::COLOR_RESET;
$colors['header'] = self::COLOR_HEADER;
$colors['eol'] = self::COLOR_EOL;
}
$classes = \sprintf(
' Classes: %6s (%d/%d)',
Util::percent(
$report->getNumTestedClassesAndTraits(),
$report->getNumClassesAndTraits(),
true
),
$report->getNumTestedClassesAndTraits(),
$report->getNumClassesAndTraits()
);
$methods = \sprintf(
' Methods: %6s (%d/%d)',
Util::percent(
$report->getNumTestedMethods(),
$report->getNumMethods(),
true
),
$report->getNumTestedMethods(),
$report->getNumMethods()
);
$lines = \sprintf(
' Lines: %6s (%d/%d)',
Util::percent(
$report->getNumExecutedLines(),
$report->getNumExecutableLines(),
true
),
$report->getNumExecutedLines(),
$report->getNumExecutableLines()
);
$padding = \max(\array_map('strlen', [$classes, $methods, $lines]));
if ($this->showOnlySummary) {
$title = 'Code Coverage Report Summary:';
$padding = \max($padding, \strlen($title));
$output .= $this->format($colors['header'], $padding, $title);
} else {
$date = \date(' Y-m-d H:i:s', $_SERVER['REQUEST_TIME']);
$title = 'Code Coverage Report:';
$output .= $this->format($colors['header'], $padding, $title);
$output .= $this->format($colors['header'], $padding, $date);
$output .= $this->format($colors['header'], $padding, '');
$output .= $this->format($colors['header'], $padding, ' Summary:');
}
$output .= $this->format($colors['classes'], $padding, $classes);
$output .= $this->format($colors['methods'], $padding, $methods);
$output .= $this->format($colors['lines'], $padding, $lines);
if ($this->showOnlySummary) {
return $output . \PHP_EOL;
}
$classCoverage = [];
foreach ($report as $item) {
if (!$item instanceof File) {
continue;
}
$classes = $item->getClassesAndTraits();
foreach ($classes as $className => $class) {
$classStatements = 0;
$coveredClassStatements = 0;
$coveredMethods = 0;
$classMethods = 0;
foreach ($class['methods'] as $method) {
if ($method['executableLines'] == 0) {
continue;
}
$classMethods++;
$classStatements += $method['executableLines'];
$coveredClassStatements += $method['executedLines'];
if ($method['coverage'] == 100) {
$coveredMethods++;
}
}
$namespace = '';
if (!empty($class['package']['namespace'])) {
$namespace = '\\' . $class['package']['namespace'] . '::';
} elseif (!empty($class['package']['fullPackage'])) {
$namespace = '@' . $class['package']['fullPackage'] . '::';
}
$classCoverage[$namespace . $className] = [
'namespace' => $namespace,
'className ' => $className,
'methodsCovered' => $coveredMethods,
'methodCount' => $classMethods,
'statementsCovered' => $coveredClassStatements,
'statementCount' => $classStatements,
];
}
}
\ksort($classCoverage);
$methodColor = '';
$linesColor = '';
$resetColor = '';
foreach ($classCoverage as $fullQualifiedPath => $classInfo) {
if ($this->showUncoveredFiles || $classInfo['statementsCovered'] != 0) {
if ($showColors) {
$methodColor = $this->getCoverageColor($classInfo['methodsCovered'], $classInfo['methodCount']);
$linesColor = $this->getCoverageColor($classInfo['statementsCovered'], $classInfo['statementCount']);
$resetColor = $colors['reset'];
}
$output .= \PHP_EOL . $fullQualifiedPath . \PHP_EOL
. ' ' . $methodColor . 'Methods: ' . $this->printCoverageCounts($classInfo['methodsCovered'], $classInfo['methodCount'], 2) . $resetColor . ' '
. ' ' . $linesColor . 'Lines: ' . $this->printCoverageCounts($classInfo['statementsCovered'], $classInfo['statementCount'], 3) . $resetColor;
}
}
return $output . \PHP_EOL;
}
private function getCoverageColor(int $numberOfCoveredElements, int $totalNumberOfElements): string
{
$coverage = Util::percent(
$numberOfCoveredElements,
$totalNumberOfElements
);
if ($coverage >= $this->highLowerBound) {
return self::COLOR_GREEN;
}
if ($coverage > $this->lowUpperBound) {
return self::COLOR_YELLOW;
}
return self::COLOR_RED;
}
private function printCoverageCounts(int $numberOfCoveredElements, int $totalNumberOfElements, int $precision): string
{
$format = '%' . $precision . 's';
return Util::percent(
$numberOfCoveredElements,
$totalNumberOfElements,
true,
true
) .
' (' . \sprintf($format, $numberOfCoveredElements) . '/' .
\sprintf($format, $totalNumberOfElements) . ')';
}
private function format($color, $padding, $string): string
{
$reset = $color ? self::COLOR_RESET : '';
return $color . \str_pad($string, $padding) . $reset . \PHP_EOL;
}
}

View File

@@ -0,0 +1,81 @@
<?php declare(strict_types=1);
/*
* This file is part of the php-code-coverage package.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage\Report\Xml;
use SebastianBergmann\Environment\Runtime;
final class BuildInformation
{
/**
* @var \DOMElement
*/
private $contextNode;
public function __construct(\DOMElement $contextNode)
{
$this->contextNode = $contextNode;
}
public function setRuntimeInformation(Runtime $runtime): void
{
$runtimeNode = $this->getNodeByName('runtime');
$runtimeNode->setAttribute('name', $runtime->getName());
$runtimeNode->setAttribute('version', $runtime->getVersion());
$runtimeNode->setAttribute('url', $runtime->getVendorUrl());
$driverNode = $this->getNodeByName('driver');
if ($runtime->hasPHPDBGCodeCoverage()) {
$driverNode->setAttribute('name', 'phpdbg');
$driverNode->setAttribute('version', \constant('PHPDBG_VERSION'));
}
if ($runtime->hasXdebug()) {
$driverNode->setAttribute('name', 'xdebug');
$driverNode->setAttribute('version', \phpversion('xdebug'));
}
if ($runtime->hasPCOV()) {
$driverNode->setAttribute('name', 'pcov');
$driverNode->setAttribute('version', \phpversion('pcov'));
}
}
public function setBuildTime(\DateTime $date): void
{
$this->contextNode->setAttribute('time', $date->format('D M j G:i:s T Y'));
}
public function setGeneratorVersions(string $phpUnitVersion, string $coverageVersion): void
{
$this->contextNode->setAttribute('phpunit', $phpUnitVersion);
$this->contextNode->setAttribute('coverage', $coverageVersion);
}
private function getNodeByName(string $name): \DOMElement
{
$node = $this->contextNode->getElementsByTagNameNS(
'https://schema.phpunit.de/coverage/1.0',
$name
)->item(0);
if (!$node) {
$node = $this->contextNode->appendChild(
$this->contextNode->ownerDocument->createElementNS(
'https://schema.phpunit.de/coverage/1.0',
$name
)
);
}
return $node;
}
}

View File

@@ -0,0 +1,69 @@
<?php declare(strict_types=1);
/*
* This file is part of the php-code-coverage package.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage\Report\Xml;
use SebastianBergmann\CodeCoverage\RuntimeException;
final class Coverage
{
/**
* @var \XMLWriter
*/
private $writer;
/**
* @var \DOMElement
*/
private $contextNode;
/**
* @var bool
*/
private $finalized = false;
public function __construct(\DOMElement $context, string $line)
{
$this->contextNode = $context;
$this->writer = new \XMLWriter();
$this->writer->openMemory();
$this->writer->startElementNS(null, $context->nodeName, 'https://schema.phpunit.de/coverage/1.0');
$this->writer->writeAttribute('nr', $line);
}
/**
* @throws RuntimeException
*/
public function addTest(string $test): void
{
if ($this->finalized) {
throw new RuntimeException('Coverage Report already finalized');
}
$this->writer->startElement('covered');
$this->writer->writeAttribute('by', $test);
$this->writer->endElement();
}
public function finalize(): void
{
$this->writer->endElement();
$fragment = $this->contextNode->ownerDocument->createDocumentFragment();
$fragment->appendXML($this->writer->outputMemory());
$this->contextNode->parentNode->replaceChild(
$fragment,
$this->contextNode
);
$this->finalized = true;
}
}

View File

@@ -0,0 +1,14 @@
<?php declare(strict_types=1);
/*
* This file is part of the php-code-coverage package.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage\Report\Xml;
final class Directory extends Node
{
}

View File

@@ -0,0 +1,287 @@
<?php declare(strict_types=1);
/*
* This file is part of the php-code-coverage package.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage\Report\Xml;
use SebastianBergmann\CodeCoverage\CodeCoverage;
use SebastianBergmann\CodeCoverage\Node\AbstractNode;
use SebastianBergmann\CodeCoverage\Node\Directory as DirectoryNode;
use SebastianBergmann\CodeCoverage\Node\File as FileNode;
use SebastianBergmann\CodeCoverage\RuntimeException;
use SebastianBergmann\CodeCoverage\Version;
use SebastianBergmann\Environment\Runtime;
final class Facade
{
/**
* @var string
*/
private $target;
/**
* @var Project
*/
private $project;
/**
* @var string
*/
private $phpUnitVersion;
public function __construct(string $version)
{
$this->phpUnitVersion = $version;
}
/**
* @throws RuntimeException
*/
public function process(CodeCoverage $coverage, string $target): void
{
if (\substr($target, -1, 1) !== \DIRECTORY_SEPARATOR) {
$target .= \DIRECTORY_SEPARATOR;
}
$this->target = $target;
$this->initTargetDirectory($target);
$report = $coverage->getReport();
$this->project = new Project(
$coverage->getReport()->getName()
);
$this->setBuildInformation();
$this->processTests($coverage->getTests());
$this->processDirectory($report, $this->project);
$this->saveDocument($this->project->asDom(), 'index');
}
private function setBuildInformation(): void
{
$buildNode = $this->project->getBuildInformation();
$buildNode->setRuntimeInformation(new Runtime());
$buildNode->setBuildTime(\DateTime::createFromFormat('U', (string) $_SERVER['REQUEST_TIME']));
$buildNode->setGeneratorVersions($this->phpUnitVersion, Version::id());
}
/**
* @throws RuntimeException
*/
private function initTargetDirectory(string $directory): void
{
if (\file_exists($directory)) {
if (!\is_dir($directory)) {
throw new RuntimeException(
"'$directory' exists but is not a directory."
);
}
if (!\is_writable($directory)) {
throw new RuntimeException(
"'$directory' exists but is not writable."
);
}
} elseif (!$this->createDirectory($directory)) {
throw new RuntimeException(
"'$directory' could not be created."
);
}
}
private function processDirectory(DirectoryNode $directory, Node $context): void
{
$directoryName = $directory->getName();
if ($this->project->getProjectSourceDirectory() === $directoryName) {
$directoryName = '/';
}
$directoryObject = $context->addDirectory($directoryName);
$this->setTotals($directory, $directoryObject->getTotals());
foreach ($directory->getDirectories() as $node) {
$this->processDirectory($node, $directoryObject);
}
foreach ($directory->getFiles() as $node) {
$this->processFile($node, $directoryObject);
}
}
/**
* @throws RuntimeException
*/
private function processFile(FileNode $file, Directory $context): void
{
$fileObject = $context->addFile(
$file->getName(),
$file->getId() . '.xml'
);
$this->setTotals($file, $fileObject->getTotals());
$path = \substr(
$file->getPath(),
\strlen($this->project->getProjectSourceDirectory())
);
$fileReport = new Report($path);
$this->setTotals($file, $fileReport->getTotals());
foreach ($file->getClassesAndTraits() as $unit) {
$this->processUnit($unit, $fileReport);
}
foreach ($file->getFunctions() as $function) {
$this->processFunction($function, $fileReport);
}
foreach ($file->getCoverageData() as $line => $tests) {
if (!\is_array($tests) || \count($tests) === 0) {
continue;
}
$coverage = $fileReport->getLineCoverage((string) $line);
foreach ($tests as $test) {
$coverage->addTest($test);
}
$coverage->finalize();
}
$fileReport->getSource()->setSourceCode(
\file_get_contents($file->getPath())
);
$this->saveDocument($fileReport->asDom(), $file->getId());
}
private function processUnit(array $unit, Report $report): void
{
if (isset($unit['className'])) {
$unitObject = $report->getClassObject($unit['className']);
} else {
$unitObject = $report->getTraitObject($unit['traitName']);
}
$unitObject->setLines(
$unit['startLine'],
$unit['executableLines'],
$unit['executedLines']
);
$unitObject->setCrap((float) $unit['crap']);
$unitObject->setPackage(
$unit['package']['fullPackage'],
$unit['package']['package'],
$unit['package']['subpackage'],
$unit['package']['category']
);
$unitObject->setNamespace($unit['package']['namespace']);
foreach ($unit['methods'] as $method) {
$methodObject = $unitObject->addMethod($method['methodName']);
$methodObject->setSignature($method['signature']);
$methodObject->setLines((string) $method['startLine'], (string) $method['endLine']);
$methodObject->setCrap($method['crap']);
$methodObject->setTotals(
(string) $method['executableLines'],
(string) $method['executedLines'],
(string) $method['coverage']
);
}
}
private function processFunction(array $function, Report $report): void
{
$functionObject = $report->getFunctionObject($function['functionName']);
$functionObject->setSignature($function['signature']);
$functionObject->setLines((string) $function['startLine']);
$functionObject->setCrap($function['crap']);
$functionObject->setTotals((string) $function['executableLines'], (string) $function['executedLines'], (string) $function['coverage']);
}
private function processTests(array $tests): void
{
$testsObject = $this->project->getTests();
foreach ($tests as $test => $result) {
if ($test === 'UNCOVERED_FILES_FROM_WHITELIST') {
continue;
}
$testsObject->addTest($test, $result);
}
}
private function setTotals(AbstractNode $node, Totals $totals): void
{
$loc = $node->getLinesOfCode();
$totals->setNumLines(
$loc['loc'],
$loc['cloc'],
$loc['ncloc'],
$node->getNumExecutableLines(),
$node->getNumExecutedLines()
);
$totals->setNumClasses(
$node->getNumClasses(),
$node->getNumTestedClasses()
);
$totals->setNumTraits(
$node->getNumTraits(),
$node->getNumTestedTraits()
);
$totals->setNumMethods(
$node->getNumMethods(),
$node->getNumTestedMethods()
);
$totals->setNumFunctions(
$node->getNumFunctions(),
$node->getNumTestedFunctions()
);
}
private function getTargetDirectory(): string
{
return $this->target;
}
/**
* @throws RuntimeException
*/
private function saveDocument(\DOMDocument $document, string $name): void
{
$filename = \sprintf('%s/%s.xml', $this->getTargetDirectory(), $name);
$document->formatOutput = true;
$document->preserveWhiteSpace = false;
$this->initTargetDirectory(\dirname($filename));
$document->save($filename);
}
private function createDirectory(string $directory): bool
{
return !(!\is_dir($directory) && !@\mkdir($directory, 0777, true) && !\is_dir($directory));
}
}

View File

@@ -0,0 +1,81 @@
<?php declare(strict_types=1);
/*
* This file is part of the php-code-coverage package.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage\Report\Xml;
class File
{
/**
* @var \DOMDocument
*/
private $dom;
/**
* @var \DOMElement
*/
private $contextNode;
public function __construct(\DOMElement $context)
{
$this->dom = $context->ownerDocument;
$this->contextNode = $context;
}
public function getTotals(): Totals
{
$totalsContainer = $this->contextNode->firstChild;
if (!$totalsContainer) {
$totalsContainer = $this->contextNode->appendChild(
$this->dom->createElementNS(
'https://schema.phpunit.de/coverage/1.0',
'totals'
)
);
}
return new Totals($totalsContainer);
}
public function getLineCoverage(string $line): Coverage
{
$coverage = $this->contextNode->getElementsByTagNameNS(
'https://schema.phpunit.de/coverage/1.0',
'coverage'
)->item(0);
if (!$coverage) {
$coverage = $this->contextNode->appendChild(
$this->dom->createElementNS(
'https://schema.phpunit.de/coverage/1.0',
'coverage'
)
);
}
$lineNode = $coverage->appendChild(
$this->dom->createElementNS(
'https://schema.phpunit.de/coverage/1.0',
'line'
)
);
return new Coverage($lineNode, $line);
}
protected function getContextNode(): \DOMElement
{
return $this->contextNode;
}
protected function getDomDocument(): \DOMDocument
{
return $this->dom;
}
}

View File

@@ -0,0 +1,56 @@
<?php declare(strict_types=1);
/*
* This file is part of the php-code-coverage package.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage\Report\Xml;
final class Method
{
/**
* @var \DOMElement
*/
private $contextNode;
public function __construct(\DOMElement $context, string $name)
{
$this->contextNode = $context;
$this->setName($name);
}
public function setSignature(string $signature): void
{
$this->contextNode->setAttribute('signature', $signature);
}
public function setLines(string $start, ?string $end = null): void
{
$this->contextNode->setAttribute('start', $start);
if ($end !== null) {
$this->contextNode->setAttribute('end', $end);
}
}
public function setTotals(string $executable, string $executed, string $coverage): void
{
$this->contextNode->setAttribute('executable', $executable);
$this->contextNode->setAttribute('executed', $executed);
$this->contextNode->setAttribute('coverage', $coverage);
}
public function setCrap(string $crap): void
{
$this->contextNode->setAttribute('crap', $crap);
}
private function setName(string $name): void
{
$this->contextNode->setAttribute('name', $name);
}
}

View File

@@ -0,0 +1,87 @@
<?php declare(strict_types=1);
/*
* This file is part of the php-code-coverage package.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage\Report\Xml;
abstract class Node
{
/**
* @var \DOMDocument
*/
private $dom;
/**
* @var \DOMElement
*/
private $contextNode;
public function __construct(\DOMElement $context)
{
$this->setContextNode($context);
}
public function getDom(): \DOMDocument
{
return $this->dom;
}
public function getTotals(): Totals
{
$totalsContainer = $this->getContextNode()->firstChild;
if (!$totalsContainer) {
$totalsContainer = $this->getContextNode()->appendChild(
$this->dom->createElementNS(
'https://schema.phpunit.de/coverage/1.0',
'totals'
)
);
}
return new Totals($totalsContainer);
}
public function addDirectory(string $name): Directory
{
$dirNode = $this->getDom()->createElementNS(
'https://schema.phpunit.de/coverage/1.0',
'directory'
);
$dirNode->setAttribute('name', $name);
$this->getContextNode()->appendChild($dirNode);
return new Directory($dirNode);
}
public function addFile(string $name, string $href): File
{
$fileNode = $this->getDom()->createElementNS(
'https://schema.phpunit.de/coverage/1.0',
'file'
);
$fileNode->setAttribute('name', $name);
$fileNode->setAttribute('href', $href);
$this->getContextNode()->appendChild($fileNode);
return new File($fileNode);
}
protected function setContextNode(\DOMElement $context): void
{
$this->dom = $context->ownerDocument;
$this->contextNode = $context;
}
protected function getContextNode(): \DOMElement
{
return $this->contextNode;
}
}

View File

@@ -0,0 +1,85 @@
<?php declare(strict_types=1);
/*
* This file is part of the php-code-coverage package.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage\Report\Xml;
final class Project extends Node
{
public function __construct(string $directory)
{
$this->init();
$this->setProjectSourceDirectory($directory);
}
public function getProjectSourceDirectory(): string
{
return $this->getContextNode()->getAttribute('source');
}
public function getBuildInformation(): BuildInformation
{
$buildNode = $this->getDom()->getElementsByTagNameNS(
'https://schema.phpunit.de/coverage/1.0',
'build'
)->item(0);
if (!$buildNode) {
$buildNode = $this->getDom()->documentElement->appendChild(
$this->getDom()->createElementNS(
'https://schema.phpunit.de/coverage/1.0',
'build'
)
);
}
return new BuildInformation($buildNode);
}
public function getTests(): Tests
{
$testsNode = $this->getContextNode()->getElementsByTagNameNS(
'https://schema.phpunit.de/coverage/1.0',
'tests'
)->item(0);
if (!$testsNode) {
$testsNode = $this->getContextNode()->appendChild(
$this->getDom()->createElementNS(
'https://schema.phpunit.de/coverage/1.0',
'tests'
)
);
}
return new Tests($testsNode);
}
public function asDom(): \DOMDocument
{
return $this->getDom();
}
private function init(): void
{
$dom = new \DOMDocument;
$dom->loadXML('<?xml version="1.0" ?><phpunit xmlns="https://schema.phpunit.de/coverage/1.0"><build/><project/></phpunit>');
$this->setContextNode(
$dom->getElementsByTagNameNS(
'https://schema.phpunit.de/coverage/1.0',
'project'
)->item(0)
);
}
private function setProjectSourceDirectory(string $name): void
{
$this->getContextNode()->setAttribute('source', $name);
}
}

View File

@@ -0,0 +1,92 @@
<?php declare(strict_types=1);
/*
* This file is part of the php-code-coverage package.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage\Report\Xml;
final class Report extends File
{
public function __construct(string $name)
{
$dom = new \DOMDocument();
$dom->loadXML('<?xml version="1.0" ?><phpunit xmlns="https://schema.phpunit.de/coverage/1.0"><file /></phpunit>');
$contextNode = $dom->getElementsByTagNameNS(
'https://schema.phpunit.de/coverage/1.0',
'file'
)->item(0);
parent::__construct($contextNode);
$this->setName($name);
}
public function asDom(): \DOMDocument
{
return $this->getDomDocument();
}
public function getFunctionObject($name): Method
{
$node = $this->getContextNode()->appendChild(
$this->getDomDocument()->createElementNS(
'https://schema.phpunit.de/coverage/1.0',
'function'
)
);
return new Method($node, $name);
}
public function getClassObject($name): Unit
{
return $this->getUnitObject('class', $name);
}
public function getTraitObject($name): Unit
{
return $this->getUnitObject('trait', $name);
}
public function getSource(): Source
{
$source = $this->getContextNode()->getElementsByTagNameNS(
'https://schema.phpunit.de/coverage/1.0',
'source'
)->item(0);
if (!$source) {
$source = $this->getContextNode()->appendChild(
$this->getDomDocument()->createElementNS(
'https://schema.phpunit.de/coverage/1.0',
'source'
)
);
}
return new Source($source);
}
private function setName($name): void
{
$this->getContextNode()->setAttribute('name', \basename($name));
$this->getContextNode()->setAttribute('path', \dirname($name));
}
private function getUnitObject($tagName, $name): Unit
{
$node = $this->getContextNode()->appendChild(
$this->getDomDocument()->createElementNS(
'https://schema.phpunit.de/coverage/1.0',
$tagName
)
);
return new Unit($node, $name);
}
}

View File

@@ -0,0 +1,38 @@
<?php declare(strict_types=1);
/*
* This file is part of the php-code-coverage package.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage\Report\Xml;
use TheSeer\Tokenizer\NamespaceUri;
use TheSeer\Tokenizer\Tokenizer;
use TheSeer\Tokenizer\XMLSerializer;
final class Source
{
/** @var \DOMElement */
private $context;
public function __construct(\DOMElement $context)
{
$this->context = $context;
}
public function setSourceCode(string $source): void
{
$context = $this->context;
$tokens = (new Tokenizer())->parse($source);
$srcDom = (new XMLSerializer(new NamespaceUri($context->namespaceURI)))->toDom($tokens);
$context->parentNode->replaceChild(
$context->ownerDocument->importNode($srcDom->documentElement, true),
$context
);
}
}

View File

@@ -0,0 +1,46 @@
<?php declare(strict_types=1);
/*
* This file is part of the php-code-coverage package.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage\Report\Xml;
final class Tests
{
private $contextNode;
private $codeMap = [
-1 => 'UNKNOWN', // PHPUnit_Runner_BaseTestRunner::STATUS_UNKNOWN
0 => 'PASSED', // PHPUnit_Runner_BaseTestRunner::STATUS_PASSED
1 => 'SKIPPED', // PHPUnit_Runner_BaseTestRunner::STATUS_SKIPPED
2 => 'INCOMPLETE', // PHPUnit_Runner_BaseTestRunner::STATUS_INCOMPLETE
3 => 'FAILURE', // PHPUnit_Runner_BaseTestRunner::STATUS_FAILURE
4 => 'ERROR', // PHPUnit_Runner_BaseTestRunner::STATUS_ERROR
5 => 'RISKY', // PHPUnit_Runner_BaseTestRunner::STATUS_RISKY
6 => 'WARNING', // PHPUnit_Runner_BaseTestRunner::STATUS_WARNING
];
public function __construct(\DOMElement $context)
{
$this->contextNode = $context;
}
public function addTest(string $test, array $result): void
{
$node = $this->contextNode->appendChild(
$this->contextNode->ownerDocument->createElementNS(
'https://schema.phpunit.de/coverage/1.0',
'test'
)
);
$node->setAttribute('name', $test);
$node->setAttribute('size', $result['size']);
$node->setAttribute('result', (string) $result['status']);
$node->setAttribute('status', $this->codeMap[(int) $result['status']]);
}
}

View File

@@ -0,0 +1,140 @@
<?php declare(strict_types=1);
/*
* This file is part of the php-code-coverage package.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage\Report\Xml;
use SebastianBergmann\CodeCoverage\Util;
final class Totals
{
/**
* @var \DOMNode
*/
private $container;
/**
* @var \DOMElement
*/
private $linesNode;
/**
* @var \DOMElement
*/
private $methodsNode;
/**
* @var \DOMElement
*/
private $functionsNode;
/**
* @var \DOMElement
*/
private $classesNode;
/**
* @var \DOMElement
*/
private $traitsNode;
public function __construct(\DOMElement $container)
{
$this->container = $container;
$dom = $container->ownerDocument;
$this->linesNode = $dom->createElementNS(
'https://schema.phpunit.de/coverage/1.0',
'lines'
);
$this->methodsNode = $dom->createElementNS(
'https://schema.phpunit.de/coverage/1.0',
'methods'
);
$this->functionsNode = $dom->createElementNS(
'https://schema.phpunit.de/coverage/1.0',
'functions'
);
$this->classesNode = $dom->createElementNS(
'https://schema.phpunit.de/coverage/1.0',
'classes'
);
$this->traitsNode = $dom->createElementNS(
'https://schema.phpunit.de/coverage/1.0',
'traits'
);
$container->appendChild($this->linesNode);
$container->appendChild($this->methodsNode);
$container->appendChild($this->functionsNode);
$container->appendChild($this->classesNode);
$container->appendChild($this->traitsNode);
}
public function getContainer(): \DOMNode
{
return $this->container;
}
public function setNumLines(int $loc, int $cloc, int $ncloc, int $executable, int $executed): void
{
$this->linesNode->setAttribute('total', (string) $loc);
$this->linesNode->setAttribute('comments', (string) $cloc);
$this->linesNode->setAttribute('code', (string) $ncloc);
$this->linesNode->setAttribute('executable', (string) $executable);
$this->linesNode->setAttribute('executed', (string) $executed);
$this->linesNode->setAttribute(
'percent',
$executable === 0 ? '0' : \sprintf('%01.2F', Util::percent($executed, $executable))
);
}
public function setNumClasses(int $count, int $tested): void
{
$this->classesNode->setAttribute('count', (string) $count);
$this->classesNode->setAttribute('tested', (string) $tested);
$this->classesNode->setAttribute(
'percent',
$count === 0 ? '0' : \sprintf('%01.2F', Util::percent($tested, $count))
);
}
public function setNumTraits(int $count, int $tested): void
{
$this->traitsNode->setAttribute('count', (string) $count);
$this->traitsNode->setAttribute('tested', (string) $tested);
$this->traitsNode->setAttribute(
'percent',
$count === 0 ? '0' : \sprintf('%01.2F', Util::percent($tested, $count))
);
}
public function setNumMethods(int $count, int $tested): void
{
$this->methodsNode->setAttribute('count', (string) $count);
$this->methodsNode->setAttribute('tested', (string) $tested);
$this->methodsNode->setAttribute(
'percent',
$count === 0 ? '0' : \sprintf('%01.2F', Util::percent($tested, $count))
);
}
public function setNumFunctions(int $count, int $tested): void
{
$this->functionsNode->setAttribute('count', (string) $count);
$this->functionsNode->setAttribute('tested', (string) $tested);
$this->functionsNode->setAttribute(
'percent',
$count === 0 ? '0' : \sprintf('%01.2F', Util::percent($tested, $count))
);
}
}

View File

@@ -0,0 +1,95 @@
<?php declare(strict_types=1);
/*
* This file is part of the php-code-coverage package.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage\Report\Xml;
final class Unit
{
/**
* @var \DOMElement
*/
private $contextNode;
public function __construct(\DOMElement $context, string $name)
{
$this->contextNode = $context;
$this->setName($name);
}
public function setLines(int $start, int $executable, int $executed): void
{
$this->contextNode->setAttribute('start', (string) $start);
$this->contextNode->setAttribute('executable', (string) $executable);
$this->contextNode->setAttribute('executed', (string) $executed);
}
public function setCrap(float $crap): void
{
$this->contextNode->setAttribute('crap', (string) $crap);
}
public function setPackage(string $full, string $package, string $sub, string $category): void
{
$node = $this->contextNode->getElementsByTagNameNS(
'https://schema.phpunit.de/coverage/1.0',
'package'
)->item(0);
if (!$node) {
$node = $this->contextNode->appendChild(
$this->contextNode->ownerDocument->createElementNS(
'https://schema.phpunit.de/coverage/1.0',
'package'
)
);
}
$node->setAttribute('full', $full);
$node->setAttribute('name', $package);
$node->setAttribute('sub', $sub);
$node->setAttribute('category', $category);
}
public function setNamespace(string $namespace): void
{
$node = $this->contextNode->getElementsByTagNameNS(
'https://schema.phpunit.de/coverage/1.0',
'namespace'
)->item(0);
if (!$node) {
$node = $this->contextNode->appendChild(
$this->contextNode->ownerDocument->createElementNS(
'https://schema.phpunit.de/coverage/1.0',
'namespace'
)
);
}
$node->setAttribute('name', $namespace);
}
public function addMethod(string $name): Method
{
$node = $this->contextNode->appendChild(
$this->contextNode->ownerDocument->createElementNS(
'https://schema.phpunit.de/coverage/1.0',
'method'
)
);
return new Method($node, $name);
}
private function setName(string $name): void
{
$this->contextNode->setAttribute('name', $name);
}
}

View File

@@ -0,0 +1,40 @@
<?php declare(strict_types=1);
/*
* This file is part of the php-code-coverage package.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage;
/**
* Utility methods.
*/
final class Util
{
/**
* @return float|int|string
*/
public static function percent(float $a, float $b, bool $asString = false, bool $fixedWidth = false)
{
if ($asString && $b == 0) {
return '';
}
$percent = 100;
if ($b > 0) {
$percent = ($a / $b) * 100;
}
if ($asString) {
$format = $fixedWidth ? '%6.2F%%' : '%01.2F%%';
return \sprintf($format, $percent);
}
return $percent;
}
}

View File

@@ -0,0 +1,30 @@
<?php declare(strict_types=1);
/*
* This file is part of the php-code-coverage package.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage;
use SebastianBergmann\Version as VersionId;
final class Version
{
/**
* @var string
*/
private static $version;
public static function id(): string
{
if (self::$version === null) {
$version = new VersionId('7.0.10', \dirname(__DIR__));
self::$version = $version->getVersion();
}
return self::$version;
}
}