Skip to content

Commit d6e6ecd

Browse files
Initial work on mapping code coverage targets to source locations
1 parent a5946cf commit d6e6ecd

File tree

3 files changed

+222
-0
lines changed

3 files changed

+222
-0
lines changed
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php declare(strict_types=1);
2+
/*
3+
* This file is part of phpunit/php-code-coverage.
4+
*
5+
* (c) Sebastian Bergmann <sebastian@phpunit.de>
6+
*
7+
* For the full copyright and license information, please view the LICENSE
8+
* file that was distributed with this source code.
9+
*/
10+
namespace SebastianBergmann\CodeCoverage;
11+
12+
use function sprintf;
13+
use RuntimeException;
14+
15+
final class InvalidCodeCoverageTargetException extends RuntimeException implements Exception
16+
{
17+
/**
18+
* @param non-empty-string $target
19+
*/
20+
public function __construct(string $target)
21+
{
22+
parent::__construct(
23+
sprintf(
24+
'%s is not a valid target for code coverage',
25+
$target,
26+
),
27+
);
28+
}
29+
}

src/Target/Mapper.php

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
<?php declare(strict_types=1);
2+
/*
3+
* This file is part of phpunit/php-code-coverage.
4+
*
5+
* (c) Sebastian Bergmann <sebastian@phpunit.de>
6+
*
7+
* For the full copyright and license information, please view the LICENSE
8+
* file that was distributed with this source code.
9+
*/
10+
namespace SebastianBergmann\CodeCoverage\Test\Target;
11+
12+
use function array_merge;
13+
use function array_unique;
14+
use function assert;
15+
use function sort;
16+
use SebastianBergmann\CodeCoverage\InvalidCodeCoverageTargetException;
17+
18+
/**
19+
* @immutable
20+
*
21+
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for phpunit/php-code-coverage
22+
*
23+
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
24+
*/
25+
final readonly class Mapper
26+
{
27+
/**
28+
* @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>>}
29+
*/
30+
private array $map;
31+
32+
/**
33+
* @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
34+
*/
35+
public function __construct(array $map)
36+
{
37+
$this->map = $map;
38+
}
39+
40+
/**
41+
* @return array<non-empty-string, list<positive-int>>
42+
*/
43+
public function map(TargetCollection $targets): array
44+
{
45+
$result = [];
46+
47+
foreach ($targets as $target) {
48+
foreach ($this->mapTarget($target) as $file => $lines) {
49+
if (!isset($result[$file])) {
50+
$result[$file] = $lines;
51+
52+
continue;
53+
}
54+
55+
$result[$file] = array_unique(array_merge($result[$file], $lines));
56+
57+
sort($result[$file]);
58+
}
59+
}
60+
61+
return $result;
62+
}
63+
64+
/**
65+
* @throws InvalidCodeCoverageTargetException
66+
*
67+
* @return array<non-empty-string, list<positive-int>>
68+
*/
69+
private function mapTarget(Target $target): array
70+
{
71+
if ($target->isClass()) {
72+
assert($target instanceof Class_);
73+
74+
if (!isset($this->map['classes'][$target->className()])) {
75+
throw new InvalidCodeCoverageTargetException('Class ' . $target->className());
76+
}
77+
78+
return $this->map['classes'][$target->className()];
79+
}
80+
}
81+
}

tests/tests/Target/MapperTest.php

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
<?php declare(strict_types=1);
2+
/*
3+
* This file is part of phpunit/php-code-coverage.
4+
*
5+
* (c) Sebastian Bergmann <sebastian@phpunit.de>
6+
*
7+
* For the full copyright and license information, please view the LICENSE
8+
* file that was distributed with this source code.
9+
*/
10+
namespace SebastianBergmann\CodeCoverage\Test\Target;
11+
12+
use function array_keys;
13+
use function range;
14+
use function realpath;
15+
use PHPUnit\Framework\Attributes\CoversClass;
16+
use PHPUnit\Framework\Attributes\DataProvider;
17+
use PHPUnit\Framework\Attributes\Small;
18+
use PHPUnit\Framework\Attributes\TestDox;
19+
use PHPUnit\Framework\TestCase;
20+
use SebastianBergmann\CodeCoverage\Filter;
21+
use SebastianBergmann\CodeCoverage\InvalidCodeCoverageTargetException;
22+
use SebastianBergmann\CodeCoverage\StaticAnalysis\ParsingFileAnalyser;
23+
24+
#[CoversClass(Mapper::class)]
25+
#[Small]
26+
final class MapperTest extends TestCase
27+
{
28+
/**
29+
* @return non-empty-list<array{0: non-empty-string, 1: array<non-empty-string, non-empty-list<positive-int>>, 2: TargetCollection}>
30+
*/
31+
public static function provider(): array
32+
{
33+
$file = realpath(__DIR__ . '/../../_files/source_with_interfaces_classes_traits_functions.php');
34+
35+
return [
36+
[
37+
'single class',
38+
[
39+
$file => range(33, 52),
40+
],
41+
TargetCollection::fromArray(
42+
[
43+
Target::forClass('SebastianBergmann\\CodeCoverage\\StaticAnalysis\\ChildClass'),
44+
],
45+
),
46+
],
47+
];
48+
}
49+
50+
/**
51+
* @return non-empty-list<array{0: non-empty-string, 1: non-empty-string, 2: TargetCollection}>
52+
*/
53+
public static function invalidProvider(): array
54+
{
55+
return [
56+
[
57+
'single class',
58+
'Class SebastianBergmann\CodeCoverage\StaticAnalysis\ChildClass is not a valid target for code coverage',
59+
TargetCollection::fromArray(
60+
[
61+
Target::forClass('SebastianBergmann\\CodeCoverage\\StaticAnalysis\\ChildClass'),
62+
],
63+
),
64+
],
65+
];
66+
}
67+
68+
/**
69+
* @param array<non-empty-string, non-empty-list<positive-int>> $expected
70+
*/
71+
#[DataProvider('provider')]
72+
#[TestDox('Maps TargetCollection with $description to source locations')]
73+
public function testMapsTargetValueObjectsToSourceLocations(string $description, array $expected, TargetCollection $targets): void
74+
{
75+
$this->assertSame(
76+
$expected,
77+
$this->mapper(array_keys($expected))->map($targets),
78+
);
79+
}
80+
81+
#[DataProvider('invalidProvider')]
82+
#[TestDox('Cannot map $description that does not exist to source locations')]
83+
public function testCannotMapInvalidTargets(string $description, string $exceptionMessage, TargetCollection $targets): void
84+
{
85+
$this->expectException(InvalidCodeCoverageTargetException::class);
86+
$this->expectExceptionMessage($exceptionMessage);
87+
88+
$this->mapper([])->map($targets);
89+
}
90+
91+
/**
92+
* @param list<non-empty-string> $files
93+
*/
94+
private function mapper(array $files): Mapper
95+
{
96+
return new Mapper($this->map($files));
97+
}
98+
99+
/**
100+
* @param list<non-empty-string> $files
101+
*
102+
* @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>>}
103+
*/
104+
private function map(array $files): array
105+
{
106+
$filter = new Filter;
107+
108+
$filter->includeFiles($files);
109+
110+
return (new MapBuilder)->build($filter, new ParsingFileAnalyser(false, false));
111+
}
112+
}

0 commit comments

Comments
 (0)