Skip to content

Commit

Permalink
Initial work on mapping code coverage targets to source locations
Browse files Browse the repository at this point in the history
  • Loading branch information
sebastianbergmann committed Oct 17, 2024
1 parent 519ec4d commit 02c166f
Show file tree
Hide file tree
Showing 3 changed files with 222 additions and 0 deletions.
29 changes: 29 additions & 0 deletions src/Exception/InvalidCodeCoverageTargetException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php declare(strict_types=1);
/*
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage;

use function sprintf;
use RuntimeException;

final class InvalidCodeCoverageTargetException extends RuntimeException implements Exception
{
/**
* @param non-empty-string $target
*/
public function __construct(string $target)
{
parent::__construct(
sprintf(
'%s is not a valid target for code coverage',
$target,
),
);
}
}
81 changes: 81 additions & 0 deletions src/Target/Mapper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<?php declare(strict_types=1);
/*
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage\Test\Target;

use function array_merge;
use function array_unique;
use function assert;
use function sort;
use SebastianBergmann\CodeCoverage\InvalidCodeCoverageTargetException;

/**
* @immutable
*
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for phpunit/php-code-coverage
*
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*/
final readonly class Mapper
{
/**
* @var array{namespaces: array<non-empty-string, list<positive-int>>, classes: array<non-empty-string, list<positive-int>>, classesThatExtendClass: array<non-empty-string, list<positive-int>>, classesThatImplementInterface: array<non-empty-string, list<positive-int>>, traits: array<non-empty-string, list<positive-int>>, methods: array<non-empty-string, list<positive-int>>, functions: array<non-empty-string, list<positive-int>>}
*/
private array $map;

/**
* @param array{namespaces: array<non-empty-string, list<positive-int>>, classes: array<non-empty-string, list<positive-int>>, classesThatExtendClass: array<non-empty-string, list<positive-int>>, classesThatImplementInterface: array<non-empty-string, list<positive-int>>, traits: array<non-empty-string, list<positive-int>>, methods: array<non-empty-string, list<positive-int>>, functions: array<non-empty-string, list<positive-int>>} $map
*/
public function __construct(array $map)
{
$this->map = $map;
}

/**
* @return array<non-empty-string, list<positive-int>>
*/
public function map(TargetCollection $targets): array
{
$result = [];

foreach ($targets as $target) {
foreach ($this->mapTarget($target) as $file => $lines) {
if (!isset($result[$file])) {
$result[$file] = $lines;

continue;
}

$result[$file] = array_unique(array_merge($result[$file], $lines));

sort($result[$file]);
}
}

return $result;
}

/**
* @throws InvalidCodeCoverageTargetException
*
* @return array<non-empty-string, list<positive-int>>
*/
private function mapTarget(Target $target): array
{
if ($target->isClass()) {

Check failure on line 71 in src/Target/Mapper.php

View workflow job for this annotation

GitHub Actions / Static Analysis

Method SebastianBergmann\CodeCoverage\Test\Target\Mapper::mapTarget() should return array<string, array<int, int<1, max>>> but return statement is missing.
assert($target instanceof Class_);

if (!isset($this->map['classes'][$target->className()])) {
throw new InvalidCodeCoverageTargetException('Class ' . $target->className());
}

return $this->map['classes'][$target->className()];

Check failure on line 78 in src/Target/Mapper.php

View workflow job for this annotation

GitHub Actions / Static Analysis

Method SebastianBergmann\CodeCoverage\Test\Target\Mapper::mapTarget() should return array<non-empty-string, array<int, int<1, max>>> but returns array<int, int<1, max>>.
}
}
}
112 changes: 112 additions & 0 deletions tests/tests/Target/MapperTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
<?php declare(strict_types=1);
/*
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage\Test\Target;

use function array_keys;
use function range;
use function realpath;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Small;
use PHPUnit\Framework\Attributes\TestDox;
use PHPUnit\Framework\TestCase;
use SebastianBergmann\CodeCoverage\Filter;
use SebastianBergmann\CodeCoverage\InvalidCodeCoverageTargetException;
use SebastianBergmann\CodeCoverage\StaticAnalysis\ParsingFileAnalyser;

#[CoversClass(Mapper::class)]
#[Small]
final class MapperTest extends TestCase
{
/**
* @return non-empty-list<array{0: non-empty-string, 1: array<non-empty-string, non-empty-list<positive-int>>, 2: TargetCollection}>
*/
public static function provider(): array
{
$file = realpath(__DIR__ . '/../../_files/source_with_interfaces_classes_traits_functions.php');

return [
[
'single class',
[
$file => range(33, 52),
],
TargetCollection::fromArray(
[
Target::forClass('SebastianBergmann\\CodeCoverage\\StaticAnalysis\\ChildClass'),
],
),
],
];
}

/**
* @return non-empty-list<array{0: non-empty-string, 1: non-empty-string, 2: TargetCollection}>
*/
public static function invalidProvider(): array
{
return [
[
'single class',
'Class SebastianBergmann\CodeCoverage\StaticAnalysis\ChildClass is not a valid target for code coverage',
TargetCollection::fromArray(
[
Target::forClass('SebastianBergmann\\CodeCoverage\\StaticAnalysis\\ChildClass'),
],
),
],
];
}

/**
* @param array<non-empty-string, non-empty-list<positive-int>> $expected
*/
#[DataProvider('provider')]
#[TestDox('Maps TargetCollection with $description to source locations')]
public function testMapsTargetValueObjectsToSourceLocations(string $description, array $expected, TargetCollection $targets): void
{
$this->assertSame(
$expected,
$this->mapper(array_keys($expected))->map($targets),
);
}

#[DataProvider('invalidProvider')]
#[TestDox('Cannot map $description that does not exist to source locations')]
public function testCannotMapInvalidTargets(string $description, string $exceptionMessage, TargetCollection $targets): void
{
$this->expectException(InvalidCodeCoverageTargetException::class);
$this->expectExceptionMessage($exceptionMessage);

$this->mapper([])->map($targets);
}

/**
* @param list<non-empty-string> $files
*/
private function mapper(array $files): Mapper
{
return new Mapper($this->map($files));
}

/**
* @param list<non-empty-string> $files
*
* @return array{namespaces: array<non-empty-string, list<positive-int>>, classes: array<non-empty-string, list<positive-int>>, classesThatExtendClass: array<non-empty-string, list<positive-int>>, classesThatImplementInterface: array<non-empty-string, list<positive-int>>, traits: array<non-empty-string, list<positive-int>>, methods: array<non-empty-string, list<positive-int>>, functions: array<non-empty-string, list<positive-int>>}
*/
private function map(array $files): array
{
$filter = new Filter;

$filter->includeFiles($files);

return (new MapBuilder)->build($filter, new ParsingFileAnalyser(false, false));
}
}

0 comments on commit 02c166f

Please sign in to comment.