351 lines
10 KiB
PHP
351 lines
10 KiB
PHP
<?php declare(strict_types=1);
|
|
/*
|
|
* This file is part of phpunit/php-code-coverage.
|
|
*
|
|
* (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 const DIRECTORY_SEPARATOR;
|
|
use function count;
|
|
use function dirname;
|
|
use function file_get_contents;
|
|
use function is_array;
|
|
use function is_dir;
|
|
use function is_file;
|
|
use function is_writable;
|
|
use function phpversion;
|
|
use function sprintf;
|
|
use function strlen;
|
|
use function substr;
|
|
use DateTimeImmutable;
|
|
use SebastianBergmann\CodeCoverage\CodeCoverage;
|
|
use SebastianBergmann\CodeCoverage\Data\ProcessedClassType;
|
|
use SebastianBergmann\CodeCoverage\Data\ProcessedFunctionType;
|
|
use SebastianBergmann\CodeCoverage\Data\ProcessedTraitType;
|
|
use SebastianBergmann\CodeCoverage\Node\AbstractNode;
|
|
use SebastianBergmann\CodeCoverage\Node\Directory as DirectoryNode;
|
|
use SebastianBergmann\CodeCoverage\Node\File as FileNode;
|
|
use SebastianBergmann\CodeCoverage\PathExistsButIsNotDirectoryException;
|
|
use SebastianBergmann\CodeCoverage\Util\Filesystem;
|
|
use SebastianBergmann\CodeCoverage\Version;
|
|
use SebastianBergmann\CodeCoverage\WriteOperationFailedException;
|
|
use SebastianBergmann\CodeCoverage\XmlException;
|
|
use SebastianBergmann\Environment\Runtime;
|
|
use XMLWriter;
|
|
|
|
/**
|
|
* @phpstan-import-type TestType from CodeCoverage
|
|
*/
|
|
final class Facade
|
|
{
|
|
public const string XML_NAMESPACE = 'https://schema.phpunit.de/coverage/1.0';
|
|
private string $target;
|
|
private Project $project;
|
|
private readonly string $phpUnitVersion;
|
|
private readonly bool $includeSource;
|
|
|
|
public function __construct(string $version, bool $includeSource = true)
|
|
{
|
|
$this->phpUnitVersion = $version;
|
|
$this->includeSource = $includeSource;
|
|
}
|
|
|
|
/**
|
|
* @throws XmlException
|
|
*/
|
|
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();
|
|
|
|
$writer = new XMLWriter;
|
|
$writer->openUri($this->targetFilePath('index'));
|
|
$writer->setIndent(true);
|
|
$writer->setIndentString(' ');
|
|
$this->project = new Project(
|
|
$writer,
|
|
$coverage->getReport()->name(),
|
|
);
|
|
|
|
$this->setBuildInformation($coverage);
|
|
|
|
$this->project->startProject();
|
|
$this->processTests($coverage->getTests());
|
|
$this->processDirectory($report, $this->project);
|
|
$this->project->finalize();
|
|
}
|
|
|
|
private function setBuildInformation(CodeCoverage $coverage): void
|
|
{
|
|
if ($coverage->driverIsPcov()) {
|
|
$driverExtensionName = 'pcov';
|
|
$driverExtensionVersion = phpversion('pcov');
|
|
} elseif ($coverage->driverIsXdebug()) {
|
|
$driverExtensionName = 'xdebug';
|
|
$driverExtensionVersion = phpversion('xdebug');
|
|
} else {
|
|
// @codeCoverageIgnoreStart
|
|
$driverExtensionName = 'unknown';
|
|
$driverExtensionVersion = 'unknown';
|
|
// @codeCoverageIgnoreEnd
|
|
}
|
|
|
|
$this->project->buildInformation(
|
|
new Runtime,
|
|
new DateTimeImmutable,
|
|
$this->phpUnitVersion,
|
|
Version::id(),
|
|
$driverExtensionName,
|
|
$driverExtensionVersion,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @throws PathExistsButIsNotDirectoryException
|
|
* @throws WriteOperationFailedException
|
|
*/
|
|
private function initTargetDirectory(string $directory): void
|
|
{
|
|
if (is_file($directory)) {
|
|
// @codeCoverageIgnoreStart
|
|
if (!is_dir($directory)) {
|
|
throw new PathExistsButIsNotDirectoryException($directory);
|
|
}
|
|
|
|
if (!is_writable($directory)) {
|
|
throw new WriteOperationFailedException($directory);
|
|
}
|
|
// @codeCoverageIgnoreEnd
|
|
}
|
|
|
|
Filesystem::createDirectory($directory);
|
|
}
|
|
|
|
/**
|
|
* @throws XmlException
|
|
*/
|
|
private function processDirectory(DirectoryNode $directory, Node $context): void
|
|
{
|
|
$directoryName = $directory->name();
|
|
|
|
if ($this->project->projectSourceDirectory() === $directoryName) {
|
|
$directoryName = '/';
|
|
}
|
|
|
|
$writer = $this->project->getWriter();
|
|
$writer->startElement('directory');
|
|
$writer->writeAttribute('name', $directoryName);
|
|
$directoryObject = $context->addDirectory();
|
|
|
|
$this->setTotals($directory, $directoryObject->totals());
|
|
|
|
foreach ($directory->directories() as $node) {
|
|
$this->processDirectory($node, $directoryObject);
|
|
}
|
|
|
|
foreach ($directory->files() as $node) {
|
|
$this->processFile($node, $directoryObject);
|
|
}
|
|
$writer->endElement();
|
|
}
|
|
|
|
/**
|
|
* @throws XmlException
|
|
*/
|
|
private function processFile(FileNode $file, Directory $context): void
|
|
{
|
|
$context->getWriter()->startElement('file');
|
|
$context->getWriter()->writeAttribute('name', $file->name());
|
|
$context->getWriter()->writeAttribute('href', $file->id() . '.xml');
|
|
$context->getWriter()->writeAttribute('hash', $file->sha1());
|
|
|
|
$fileObject = $context->addFile();
|
|
|
|
$this->setTotals($file, $fileObject->totals());
|
|
|
|
$context->getWriter()->endElement();
|
|
|
|
$path = substr(
|
|
$file->pathAsString(),
|
|
strlen($this->project->projectSourceDirectory()),
|
|
);
|
|
|
|
$writer = new XMLWriter;
|
|
$writer->openUri($this->targetFilePath($file->id()));
|
|
$writer->setIndent(true);
|
|
$writer->setIndentString(' ');
|
|
$fileReport = new Report($writer, $path, $file->sha1());
|
|
|
|
$this->setTotals($file, $fileReport->totals());
|
|
|
|
foreach ($file->classesAndTraits() as $unit) {
|
|
$this->processUnit($unit, $fileReport);
|
|
}
|
|
|
|
foreach ($file->functions() as $function) {
|
|
$this->processFunction($function, $fileReport);
|
|
}
|
|
|
|
$fileReport->getWriter()->startElement('coverage');
|
|
|
|
foreach ($file->lineCoverageData() as $line => $tests) {
|
|
if (!is_array($tests) || count($tests) === 0) {
|
|
continue;
|
|
}
|
|
|
|
$coverage = $fileReport->lineCoverage((string) $line);
|
|
$coverage->finalize($tests);
|
|
}
|
|
$fileReport->getWriter()->endElement();
|
|
|
|
if ($this->includeSource) {
|
|
$fileReport->source()->setSourceCode(
|
|
file_get_contents($file->pathAsString()),
|
|
);
|
|
}
|
|
|
|
$fileReport->finalize();
|
|
}
|
|
|
|
private function processUnit(ProcessedClassType|ProcessedTraitType $unit, Report $report): void
|
|
{
|
|
if ($unit instanceof ProcessedClassType) {
|
|
$report->getWriter()->startElement('class');
|
|
|
|
$unitObject = $report->classObject(
|
|
$unit->className,
|
|
$unit->namespace,
|
|
$unit->startLine,
|
|
$unit->executableLines,
|
|
$unit->executedLines,
|
|
(float) $unit->crap,
|
|
);
|
|
} else {
|
|
$report->getWriter()->startElement('trait');
|
|
|
|
$unitObject = $report->traitObject(
|
|
$unit->traitName,
|
|
$unit->namespace,
|
|
$unit->startLine,
|
|
$unit->executableLines,
|
|
$unit->executedLines,
|
|
(float) $unit->crap,
|
|
);
|
|
}
|
|
|
|
foreach ($unit->methods as $method) {
|
|
$report->getWriter()->startElement('method');
|
|
|
|
$unitObject->addMethod(
|
|
$method->methodName,
|
|
$method->signature,
|
|
(string) $method->startLine,
|
|
(string) $method->endLine,
|
|
(string) $method->executableLines,
|
|
(string) $method->executedLines,
|
|
(string) $method->coverage,
|
|
$method->crap,
|
|
);
|
|
|
|
$report->getWriter()->endElement();
|
|
}
|
|
|
|
$report->getWriter()->endElement();
|
|
}
|
|
|
|
private function processFunction(ProcessedFunctionType $function, Report $report): void
|
|
{
|
|
$report->getWriter()->startElement('function');
|
|
|
|
$report->functionObject(
|
|
$function->functionName,
|
|
$function->signature,
|
|
(string) $function->startLine,
|
|
null,
|
|
(string) $function->executableLines,
|
|
(string) $function->executedLines,
|
|
(string) $function->coverage,
|
|
$function->crap,
|
|
);
|
|
|
|
$report->getWriter()->endElement();
|
|
}
|
|
|
|
/**
|
|
* @param array<string, TestType> $tests
|
|
*/
|
|
private function processTests(array $tests): void
|
|
{
|
|
$this->project->getWriter()->startElement('tests');
|
|
|
|
$testsObject = $this->project->tests();
|
|
|
|
foreach ($tests as $test => $result) {
|
|
$testsObject->addTest($test, $result);
|
|
}
|
|
|
|
$this->project->getWriter()->endElement();
|
|
}
|
|
|
|
private function setTotals(AbstractNode $node, Totals $totals): void
|
|
{
|
|
$totals->getWriter()->startElement('totals');
|
|
|
|
$loc = $node->linesOfCode();
|
|
|
|
$totals->setNumLines(
|
|
$loc->linesOfCode(),
|
|
$loc->commentLinesOfCode(),
|
|
$loc->nonCommentLinesOfCode(),
|
|
$node->numberOfExecutableLines(),
|
|
$node->numberOfExecutedLines(),
|
|
);
|
|
|
|
$totals->setNumMethods(
|
|
$node->numberOfMethods(),
|
|
$node->numberOfTestedMethods(),
|
|
);
|
|
|
|
$totals->setNumFunctions(
|
|
$node->numberOfFunctions(),
|
|
$node->numberOfTestedFunctions(),
|
|
);
|
|
|
|
$totals->setNumClasses(
|
|
$node->numberOfClasses(),
|
|
$node->numberOfTestedClasses(),
|
|
);
|
|
|
|
$totals->setNumTraits(
|
|
$node->numberOfTraits(),
|
|
$node->numberOfTestedTraits(),
|
|
);
|
|
|
|
$totals->getWriter()->endElement();
|
|
}
|
|
|
|
private function targetDirectory(): string
|
|
{
|
|
return $this->target;
|
|
}
|
|
|
|
private function targetFilePath(string $name): string
|
|
{
|
|
$filename = sprintf('%s/%s.xml', $this->targetDirectory(), $name);
|
|
|
|
$this->initTargetDirectory(dirname($filename));
|
|
|
|
return $filename;
|
|
}
|
|
}
|