diff --git a/src/CodeCoverage.php b/src/CodeCoverage.php index df6fedc6..d37577a6 100644 --- a/src/CodeCoverage.php +++ b/src/CodeCoverage.php @@ -29,6 +29,10 @@ use SebastianBergmann\CodeCoverage\StaticAnalysis\CachingFileAnalyser; use SebastianBergmann\CodeCoverage\StaticAnalysis\FileAnalyser; use SebastianBergmann\CodeCoverage\StaticAnalysis\ParsingFileAnalyser; +use SebastianBergmann\CodeCoverage\Test\Target\MapBuilder; +use SebastianBergmann\CodeCoverage\Test\Target\Mapper; +use SebastianBergmann\CodeCoverage\Test\Target\TargetCollection; +use SebastianBergmann\CodeCoverage\Test\Target\ValidationResult; use SebastianBergmann\CodeCoverage\Test\TestSize\TestSize; use SebastianBergmann\CodeCoverage\Test\TestStatus\TestStatus; use SebastianBergmann\CodeUnitReverseLookup\Wizard; @@ -47,6 +51,7 @@ final class CodeCoverage private readonly Driver $driver; private readonly Filter $filter; private readonly Wizard $wizard; + private ?Mapper $targetMapper = null; private bool $checkForUnintentionallyCoveredCode = false; private bool $ignoreDeprecatedCode = false; private ?string $currentId = null; @@ -348,6 +353,11 @@ public function detectsDeadCode(): bool return $this->driver->detectsDeadCode(); } + public function validate(TargetCollection $targets): ValidationResult + { + return $targets->validate($this->targetMapper()); + } + /** * @throws ReflectionException * @throws UnintentionallyCoveredCodeException @@ -566,6 +576,19 @@ private function processUnintentionallyCoveredUnits(array $unintentionallyCovere return $processed; } + private function targetMapper(): Mapper + { + if ($this->targetMapper !== null) { + return $this->targetMapper; + } + + $this->targetMapper = new Mapper( + (new MapBuilder)->build($this->filter, $this->analyser()), + ); + + return $this->targetMapper; + } + private function analyser(): FileAnalyser { if ($this->analyser !== null) { diff --git a/src/Target/TargetCollection.php b/src/Target/TargetCollection.php index ef6e32ac..9914cf85 100644 --- a/src/Target/TargetCollection.php +++ b/src/Target/TargetCollection.php @@ -10,6 +10,7 @@ namespace SebastianBergmann\CodeCoverage\Test\Target; use function count; +use function implode; use Countable; use IteratorAggregate; @@ -67,4 +68,23 @@ public function getIterator(): TargetCollectionIterator { return new TargetCollectionIterator($this); } + + public function validate(Mapper $mapper): ValidationResult + { + $errors = []; + + foreach ($this->targets as $target) { + try { + $mapper->mapTarget($target); + } catch (InvalidCodeCoverageTargetException $e) { + $errors[] = $e->getMessage(); + } + } + + if ($errors === []) { + return ValidationResult::success(); + } + + return ValidationResult::failure(implode("\n", $errors)); + } } diff --git a/src/Target/ValidationFailure.php b/src/Target/ValidationFailure.php new file mode 100644 index 00000000..e43791f5 --- /dev/null +++ b/src/Target/ValidationFailure.php @@ -0,0 +1,46 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace SebastianBergmann\CodeCoverage\Test\Target; + +/** + * @immutable + * + * @no-named-arguments Parameter names are not covered by the backward compatibility promise for phpunit/php-code-coverage + */ +final readonly class ValidationFailure extends ValidationResult +{ + /** + * @var non-empty-string + */ + private string $message; + + /** + * @param non-empty-string $message + * + * @noinspection PhpMissingParentConstructorInspection + */ + protected function __construct(string $message) + { + $this->message = $message; + } + + public function isFailure(): true + { + return true; + } + + /** + * @return non-empty-string + */ + public function message(): string + { + return $this->message; + } +} diff --git a/src/Target/ValidationResult.php b/src/Target/ValidationResult.php new file mode 100644 index 00000000..7dc0cc5a --- /dev/null +++ b/src/Target/ValidationResult.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace SebastianBergmann\CodeCoverage\Test\Target; + +/** + * @immutable + * + * @no-named-arguments Parameter names are not covered by the backward compatibility promise for phpunit/php-code-coverage + */ +abstract readonly class ValidationResult +{ + public static function success(): ValidationSuccess + { + return new ValidationSuccess; + } + + /** + * @param non-empty-string $message + */ + public static function failure(string $message): ValidationFailure + { + return new ValidationFailure($message); + } + + protected function __construct() + { + } + + /** + * @phpstan-assert-if-true ValidationSuccess $this + */ + public function isSuccess(): bool + { + return false; + } + + /** + * @phpstan-assert-if-true ValidationFailure $this + */ + public function isFailure(): bool + { + return false; + } +} diff --git a/src/Target/ValidationSuccess.php b/src/Target/ValidationSuccess.php new file mode 100644 index 00000000..1dffd0d2 --- /dev/null +++ b/src/Target/ValidationSuccess.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace SebastianBergmann\CodeCoverage\Test\Target; + +/** + * @immutable + * + * @no-named-arguments Parameter names are not covered by the backward compatibility promise for phpunit/php-code-coverage + */ +final readonly class ValidationSuccess extends ValidationResult +{ + public function isSuccess(): true + { + return true; + } +} diff --git a/tests/tests/Target/ValidationResultTest.php b/tests/tests/Target/ValidationResultTest.php new file mode 100644 index 00000000..66806457 --- /dev/null +++ b/tests/tests/Target/ValidationResultTest.php @@ -0,0 +1,36 @@ + + * + * 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 PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\Small; +use PHPUnit\Framework\TestCase; + +#[CoversClass(ValidationResult::class)] +#[CoversClass(ValidationSuccess::class)] +#[CoversClass(ValidationFailure::class)] +#[Small] +final class ValidationResultTest extends TestCase +{ + public function testCanBeSuccess(): void + { + $this->assertTrue(ValidationResult::success()->isSuccess()); + $this->assertFalse(ValidationResult::success()->isFailure()); + } + + public function testCanBeFailure(): void + { + $message = 'message'; + + $this->assertTrue(ValidationResult::failure($message)->isFailure()); + $this->assertFalse(ValidationResult::failure($message)->isSuccess()); + $this->assertSame($message, ValidationResult::failure($message)->message()); + } +}