fix:repo cleanup

This commit is contained in:
2025-11-08 20:09:07 +00:00
parent f1da93791d
commit dedcc73567
1204 changed files with 121800 additions and 16971 deletions
-101
View File
@@ -1,101 +0,0 @@
# ChangeLog
All notable changes are documented in this file using the [Keep a CHANGELOG](http://keepachangelog.com/) principles.
## [11.0.11] - 2025-08-27
### Changed
* [#1085](https://github.com/sebastianbergmann/php-code-coverage/pull/1085): Improve performance by skipping empty lines after filter has been applied
## [11.0.10] - 2025-06-18
### Changed
* Changed CSS for HTML report to not use common ligatures as this sometimes lead to hard-to-read code
* Updated Bootstrap to version 5.3.6 for HTML report
## [11.0.9] - 2025-02-25
### Changed
* Changed version identifier for static analysis cache from "MD5 over source code" to `Version::id()`
* The `SebastianBergmann\CodeCoverage\Filter::includeUncoveredFiles()` and `SebastianBergmann\CodeCoverage\Filter::excludeUncoveredFiles()` methods are no longer deprecated
### Fixed
* [#1063](https://github.com/sebastianbergmann/php-code-coverage/issues/1063): HTML report highlights argument named `fn` differently than other named arguments
## [11.0.8] - 2024-12-11
### Changed
* [#1054](https://github.com/sebastianbergmann/php-code-coverage/pull/1054): Use click event for toggling "tests covering this line" popover in HTML report
## [11.0.7] - 2024-10-09
### Changed
* [#1037](https://github.com/sebastianbergmann/php-code-coverage/pull/1037): Upgrade Bootstrap to version 5.3.3 for HTML report
* [#1046](https://github.com/sebastianbergmann/php-code-coverage/pull/1046): CSS fixes for HTML report
### Deprecated
* The `SebastianBergmann\CodeCoverage\Filter::includeUncoveredFiles()`, `SebastianBergmann\CodeCoverage\Filter::excludeUncoveredFiles()`, and `SebastianBergmann\CodeCoverage\Filter::excludeFile()` methods have been deprecated
## [11.0.6] - 2024-08-22
### Changed
* Updated dependencies (so that users that install using Composer's `--prefer-lowest` CLI option also get recent versions)
## [11.0.5] - 2024-07-03
### Changed
* This project now uses PHPStan instead of Psalm for static analysis
## [11.0.4] - 2024-06-29
### Fixed
* [#967](https://github.com/sebastianbergmann/php-code-coverage/issues/967): Identification of executable lines for `match` expressions does not work correctly
## [11.0.3] - 2024-03-12
### Fixed
* [#1033](https://github.com/sebastianbergmann/php-code-coverage/issues/1033): `@codeCoverageIgnore` annotation does not work on `enum`
## [11.0.2] - 2024-03-09
### Changed
* [#1032](https://github.com/sebastianbergmann/php-code-coverage/pull/1032): Pad lines in code coverage report only when colors are shown
## [11.0.1] - 2024-03-02
### Changed
* Do not use implicitly nullable parameters
## [11.0.0] - 2024-02-02
### Removed
* The `SebastianBergmann\CodeCoverage\Filter::includeDirectory()`, `SebastianBergmann\CodeCoverage\Filter::excludeDirectory()`, and `SebastianBergmann\CodeCoverage\Filter::excludeFile()` methods have been removed
* This component now requires PHP-Parser 5
* This component is no longer supported on PHP 8.1
[11.0.11]: https://github.com/sebastianbergmann/php-code-coverage/compare/11.0.10...11.0.11
[11.0.10]: https://github.com/sebastianbergmann/php-code-coverage/compare/11.0.9...11.0.10
[11.0.9]: https://github.com/sebastianbergmann/php-code-coverage/compare/11.0.8...11.0.9
[11.0.8]: https://github.com/sebastianbergmann/php-code-coverage/compare/11.0.7...11.0.8
[11.0.7]: https://github.com/sebastianbergmann/php-code-coverage/compare/11.0.6...11.0.7
[11.0.6]: https://github.com/sebastianbergmann/php-code-coverage/compare/11.0.5...11.0.6
[11.0.5]: https://github.com/sebastianbergmann/php-code-coverage/compare/11.0.4...11.0.5
[11.0.4]: https://github.com/sebastianbergmann/php-code-coverage/compare/11.0.3...11.0.4
[11.0.3]: https://github.com/sebastianbergmann/php-code-coverage/compare/11.0.2...11.0.3
[11.0.2]: https://github.com/sebastianbergmann/php-code-coverage/compare/11.0.1...11.0.2
[11.0.1]: https://github.com/sebastianbergmann/php-code-coverage/compare/11.0.0...11.0.1
[11.0.0]: https://github.com/sebastianbergmann/php-code-coverage/compare/10.1...11.0.0
+11 -12
View File
@@ -22,29 +22,28 @@
},
"config": {
"platform": {
"php": "8.2.0"
"php": "8.3.0"
},
"optimize-autoloader": true,
"sort-packages": true
},
"prefer-stable": true,
"require": {
"php": ">=8.2",
"php": ">=8.3",
"ext-dom": "*",
"ext-libxml": "*",
"ext-xmlwriter": "*",
"nikic/php-parser": "^5.4.0",
"phpunit/php-file-iterator": "^5.1.0",
"phpunit/php-text-template": "^4.0.1",
"sebastian/code-unit-reverse-lookup": "^4.0.1",
"sebastian/complexity": "^4.0.1",
"sebastian/environment": "^7.2.0",
"sebastian/lines-of-code": "^3.0.1",
"sebastian/version": "^5.0.2",
"nikic/php-parser": "^5.6.1",
"phpunit/php-file-iterator": "^6.0",
"phpunit/php-text-template": "^5.0",
"sebastian/complexity": "^5.0",
"sebastian/environment": "^8.0.3",
"sebastian/lines-of-code": "^4.0",
"sebastian/version": "^6.0",
"theseer/tokenizer": "^1.2.3"
},
"require-dev": {
"phpunit/phpunit": "^11.5.2"
"phpunit/phpunit": "^12.3.7"
},
"suggest": {
"ext-pcov": "PHP extension that provides line coverage",
@@ -62,7 +61,7 @@
},
"extra": {
"branch-alias": {
"dev-main": "11.0.x-dev"
"dev-main": "12.4.x-dev"
}
}
}
+122 -81
View File
@@ -14,11 +14,9 @@ use function array_diff_key;
use function array_flip;
use function array_keys;
use function array_merge;
use function array_merge_recursive;
use function array_unique;
use function count;
use function explode;
use function is_array;
use function is_file;
use function sort;
use ReflectionClass;
@@ -27,59 +25,76 @@ use SebastianBergmann\CodeCoverage\Data\RawCodeCoverageData;
use SebastianBergmann\CodeCoverage\Driver\Driver;
use SebastianBergmann\CodeCoverage\Node\Builder;
use SebastianBergmann\CodeCoverage\Node\Directory;
use SebastianBergmann\CodeCoverage\StaticAnalysis\CachingFileAnalyser;
use SebastianBergmann\CodeCoverage\StaticAnalysis\CachingSourceAnalyser;
use SebastianBergmann\CodeCoverage\StaticAnalysis\FileAnalyser;
use SebastianBergmann\CodeCoverage\StaticAnalysis\ParsingFileAnalyser;
use SebastianBergmann\CodeCoverage\StaticAnalysis\ParsingSourceAnalyser;
use SebastianBergmann\CodeCoverage\Test\Target\MapBuilder;
use SebastianBergmann\CodeCoverage\Test\Target\Mapper;
use SebastianBergmann\CodeCoverage\Test\Target\TargetCollection;
use SebastianBergmann\CodeCoverage\Test\Target\TargetCollectionValidator;
use SebastianBergmann\CodeCoverage\Test\Target\ValidationResult;
use SebastianBergmann\CodeCoverage\Test\TestSize\TestSize;
use SebastianBergmann\CodeCoverage\Test\TestStatus\TestStatus;
use SebastianBergmann\CodeUnitReverseLookup\Wizard;
/**
* Provides collection functionality for PHP code coverage information.
*
* @phpstan-type TestType = array{
* size: string,
* status: string,
* }
* @phpstan-type TestType array{size: string, status: string}
* @phpstan-type TargetedLines array<non-empty-string, list<positive-int>>
*/
final class CodeCoverage
{
private const UNCOVERED_FILES = 'UNCOVERED_FILES';
private const string UNCOVERED_FILES = 'UNCOVERED_FILES';
private readonly Driver $driver;
private readonly Filter $filter;
private readonly Wizard $wizard;
private ?FileAnalyser $analyser = null;
private ?Mapper $targetMapper = null;
private ?string $cacheDirectory = null;
private bool $checkForUnintentionallyCoveredCode = false;
private bool $collectBranchAndPathCoverage = false;
private bool $includeUncoveredFiles = true;
private bool $ignoreDeprecatedCode = false;
private ?string $currentId = null;
private ?TestSize $currentSize = null;
private ProcessedCodeCoverageData $data;
private bool $useAnnotationsForIgnoringCode = true;
/**
* @var array<string,list<int>>
*/
private array $linesToBeIgnored = [];
/**
* @var array<string, TestType>
*/
private array $tests = [];
private bool $useAnnotationsForIgnoringCode = true;
/**
* @var list<class-string>
*/
private array $parentClassesExcludedFromUnintentionallyCoveredCodeCheck = [];
private ?FileAnalyser $analyser = null;
private ?string $cacheDirectory = null;
private ?Directory $cachedReport = null;
private ?string $currentId = null;
private ?TestSize $currentSize = null;
private ProcessedCodeCoverageData $data;
/**
* @var array<string, TestType>
*/
private array $tests = [];
private ?Directory $cachedReport = null;
public function __construct(Driver $driver, Filter $filter)
{
$this->driver = $driver;
$this->filter = $filter;
$this->data = new ProcessedCodeCoverageData;
$this->wizard = new Wizard;
}
public function __serialize(): array
{
$prefix = "\x00" . self::class . "\x00";
return [
// Configuration
$prefix . 'cacheDirectory' => $this->cacheDirectory,
$prefix . 'checkForUnintentionallyCoveredCode' => $this->checkForUnintentionallyCoveredCode,
$prefix . 'includeUncoveredFiles' => $this->includeUncoveredFiles,
$prefix . 'ignoreDeprecatedCode' => $this->ignoreDeprecatedCode,
$prefix . 'parentClassesExcludedFromUnintentionallyCoveredCodeCheck' => $this->parentClassesExcludedFromUnintentionallyCoveredCodeCheck,
$prefix . 'useAnnotationsForIgnoringCode' => $this->useAnnotationsForIgnoringCode,
$prefix . 'filter' => $this->filter,
// Data
$prefix . 'data' => $this->data,
$prefix . 'tests' => $this->tests,
];
}
/**
@@ -174,19 +189,11 @@ final class CodeCoverage
$this->cachedReport = null;
}
/**
* @param array<string,list<int>> $linesToBeIgnored
*/
public function stop(bool $append = true, ?TestStatus $status = null, array|false $linesToBeCovered = [], array $linesToBeUsed = [], array $linesToBeIgnored = []): RawCodeCoverageData
public function stop(bool $append = true, ?TestStatus $status = null, null|false|TargetCollection $covers = null, ?TargetCollection $uses = null): RawCodeCoverageData
{
$data = $this->driver->stop();
$this->linesToBeIgnored = array_merge_recursive(
$this->linesToBeIgnored,
$linesToBeIgnored,
);
$this->append($data, null, $append, $status, $linesToBeCovered, $linesToBeUsed, $linesToBeIgnored);
$this->append($data, null, $append, $status, $covers, $uses);
$this->currentId = null;
$this->currentSize = null;
@@ -196,13 +203,11 @@ final class CodeCoverage
}
/**
* @param array<string,list<int>> $linesToBeIgnored
*
* @throws ReflectionException
* @throws TestIdMissingException
* @throws UnintentionallyCoveredCodeException
*/
public function append(RawCodeCoverageData $rawData, ?string $id = null, bool $append = true, ?TestStatus $status = null, array|false $linesToBeCovered = [], array $linesToBeUsed = [], array $linesToBeIgnored = []): void
public function append(RawCodeCoverageData $rawData, ?string $id = null, bool $append = true, ?TestStatus $status = null, null|false|TargetCollection $covers = null, ?TargetCollection $uses = null): void
{
if ($id === null) {
$id = $this->currentId;
@@ -212,24 +217,32 @@ final class CodeCoverage
throw new TestIdMissingException;
}
$this->cachedReport = null;
if ($status === null) {
$status = TestStatus::unknown();
}
if ($covers === null) {
$covers = TargetCollection::fromArray([]);
}
if ($uses === null) {
$uses = TargetCollection::fromArray([]);
}
$size = $this->currentSize;
if ($size === null) {
$size = TestSize::unknown();
}
$this->cachedReport = null;
$this->applyFilter($rawData);
$this->applyExecutableLinesFilter($rawData);
if ($this->useAnnotationsForIgnoringCode) {
$this->applyIgnoredLinesFilter($rawData, $linesToBeIgnored);
$this->applyIgnoredLinesFilter($rawData);
}
$this->data->initializeUnseenData($rawData);
@@ -242,6 +255,17 @@ final class CodeCoverage
return;
}
$linesToBeCovered = false;
$linesToBeUsed = [];
if ($covers !== false) {
$linesToBeCovered = $this->targetMapper()->mapTargets($covers);
}
if ($linesToBeCovered !== false) {
$linesToBeUsed = $this->targetMapper()->mapTargets($uses);
}
$this->applyCoversAndUsesFilter(
$rawData,
$linesToBeCovered,
@@ -249,7 +273,7 @@ final class CodeCoverage
$size,
);
if (empty($rawData->lineCoverage())) {
if ($rawData->lineCoverage() === []) {
return;
}
@@ -360,24 +384,31 @@ final class CodeCoverage
public function enableBranchAndPathCoverage(): void
{
$this->driver->enableBranchAndPathCoverage();
$this->collectBranchAndPathCoverage = true;
}
public function disableBranchAndPathCoverage(): void
{
$this->driver->disableBranchAndPathCoverage();
$this->collectBranchAndPathCoverage = false;
}
public function collectsBranchAndPathCoverage(): bool
{
return $this->driver->collectsBranchAndPathCoverage();
return $this->collectBranchAndPathCoverage;
}
public function detectsDeadCode(): bool
public function validate(TargetCollection $targets): ValidationResult
{
return $this->driver->detectsDeadCode();
return (new TargetCollectionValidator)->validate($this->targetMapper(), $targets);
}
/**
* @param false|TargetedLines $linesToBeCovered
* @param TargetedLines $linesToBeUsed
*
* @throws ReflectionException
* @throws UnintentionallyCoveredCodeException
*/
@@ -389,7 +420,7 @@ final class CodeCoverage
return;
}
if (empty($linesToBeCovered)) {
if ($linesToBeCovered === []) {
return;
}
@@ -404,11 +435,9 @@ final class CodeCoverage
$rawData->removeCoverageDataForFile($fileWithNoCoverage);
}
if (is_array($linesToBeCovered)) {
foreach ($linesToBeCovered as $fileToBeCovered => $includedLines) {
$rawData->keepLineCoverageDataOnlyForLines($fileToBeCovered, $includedLines);
$rawData->keepFunctionCoverageDataOnlyForLines($fileToBeCovered, $includedLines);
}
foreach ($linesToBeCovered as $fileToBeCovered => $includedLines) {
$rawData->keepLineCoverageDataOnlyForLines($fileToBeCovered, $includedLines);
$rawData->keepFunctionCoverageDataOnlyForLines($fileToBeCovered, $includedLines);
}
}
@@ -432,7 +461,7 @@ final class CodeCoverage
continue;
}
$linesToBranchMap = $this->analyser()->executableLinesIn($filename);
$linesToBranchMap = $this->analyser()->analyse($filename)->executableLines();
$data->keepLineCoverageDataOnlyForLines(
$filename,
@@ -446,26 +475,16 @@ final class CodeCoverage
}
}
/**
* @param array<string,list<int>> $linesToBeIgnored
*/
private function applyIgnoredLinesFilter(RawCodeCoverageData $data, array $linesToBeIgnored): void
private function applyIgnoredLinesFilter(RawCodeCoverageData $data): void
{
foreach (array_keys($data->lineCoverage()) as $filename) {
if (!$this->filter->isFile($filename)) {
continue;
}
if (isset($linesToBeIgnored[$filename])) {
$data->removeCoverageDataForLines(
$filename,
$linesToBeIgnored[$filename],
);
}
$data->removeCoverageDataForLines(
$filename,
$this->analyser()->ignoredLinesFor($filename),
$this->analyser()->analyse($filename)->ignoredLines(),
);
}
}
@@ -488,13 +507,15 @@ final class CodeCoverage
$this->analyser(),
),
self::UNCOVERED_FILES,
linesToBeIgnored: $this->linesToBeIgnored,
);
}
}
}
/**
* @param TargetedLines $linesToBeCovered
* @param TargetedLines $linesToBeUsed
*
* @throws ReflectionException
* @throws UnintentionallyCoveredCodeException
*/
@@ -510,20 +531,26 @@ final class CodeCoverage
foreach ($data->lineCoverage() as $file => $_data) {
foreach ($_data as $line => $flag) {
if ($flag === 1 && !isset($allowedLines[$file][$line])) {
$unintentionallyCoveredUnits[] = $this->wizard->lookup($file, $line);
$unintentionallyCoveredUnits[] = $this->targetMapper->lookup($file, $line);
}
}
}
$unintentionallyCoveredUnits = $this->processUnintentionallyCoveredUnits($unintentionallyCoveredUnits);
if (!empty($unintentionallyCoveredUnits)) {
if ($unintentionallyCoveredUnits !== []) {
throw new UnintentionallyCoveredCodeException(
$unintentionallyCoveredUnits,
);
}
}
/**
* @param TargetedLines $linesToBeCovered
* @param TargetedLines $linesToBeUsed
*
* @return TargetedLines
*/
private function getAllowedLines(array $linesToBeCovered, array $linesToBeUsed): array
{
$allowedLines = [];
@@ -606,26 +633,40 @@ final class CodeCoverage
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) {
return $this->analyser;
}
$this->analyser = new ParsingFileAnalyser(
$sourceAnalyser = new ParsingSourceAnalyser;
if ($this->cachesStaticAnalysis()) {
$sourceAnalyser = new CachingSourceAnalyser(
$this->cacheDirectory,
$sourceAnalyser,
);
}
$this->analyser = new FileAnalyser(
$sourceAnalyser,
$this->useAnnotationsForIgnoringCode,
$this->ignoreDeprecatedCode,
);
if ($this->cachesStaticAnalysis()) {
$this->analyser = new CachingFileAnalyser(
$this->cacheDirectory,
$this->analyser,
$this->useAnnotationsForIgnoringCode,
$this->ignoreDeprecatedCode,
);
}
return $this->analyser;
}
}
@@ -17,13 +17,31 @@ use function count;
use function is_array;
use function ksort;
use SebastianBergmann\CodeCoverage\Driver\Driver;
use SebastianBergmann\CodeCoverage\Driver\XdebugDriver;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*
* @phpstan-import-type XdebugFunctionCoverageType from \SebastianBergmann\CodeCoverage\Driver\XdebugDriver
* @phpstan-import-type XdebugFunctionCoverageType from XdebugDriver
*
* @phpstan-type TestIdType = string
* @phpstan-type TestIdType string
* @phpstan-type FunctionCoverageDataType array{
* branches: array<int, array{
* op_start: int,
* op_end: int,
* line_start: int,
* line_end: int,
* hit: list<TestIdType>,
* out: array<int, int>,
* out_hit: array<int, int>,
* }>,
* paths: array<int, array{
* path: array<int, int>,
* hit: list<TestIdType>,
* }>,
* hit: list<TestIdType>
* }
* @phpstan-type FunctionCoverageType array<string, array<string, FunctionCoverageDataType>>
*/
final class ProcessedCodeCoverageData
{
@@ -40,22 +58,7 @@ final class ProcessedCodeCoverageData
* Maintains base format of raw data (@see https://xdebug.org/docs/code_coverage), but each 'hit' entry is an array
* of testcase ids.
*
* @var array<string, array<string, array{
* branches: array<int, array{
* op_start: int,
* op_end: int,
* line_start: int,
* line_end: int,
* hit: list<TestIdType>,
* out: array<int, int>,
* out_hit: array<int, int>,
* }>,
* paths: array<int, array{
* path: array<int, int>,
* hit: list<TestIdType>,
* }>,
* hit: list<TestIdType>
* }>>
* @var FunctionCoverageType
*/
private array $functionCoverage = [];
@@ -216,6 +219,8 @@ final class ProcessedCodeCoverageData
* 4 = the line has been tested
*
* During a merge, a higher number is better.
*
* @return 1|2|3|4
*/
private function priorityForLine(array $data, int $line): int
{
@@ -237,7 +242,7 @@ final class ProcessedCodeCoverageData
/**
* For a function we have never seen before, copy all data over and simply init the 'hit' array.
*
* @param XdebugFunctionCoverageType $functionData
* @param FunctionCoverageDataType|XdebugFunctionCoverageType $functionData
*/
private function initPreviouslyUnseenFunction(string $file, string $functionName, array $functionData): void
{
@@ -257,7 +262,7 @@ final class ProcessedCodeCoverageData
* Techniques such as mocking and where the contents of a file are different vary during tests (e.g. compiling
* containers) mean that the functions inside a file cannot be relied upon to be static.
*
* @param XdebugFunctionCoverageType $functionData
* @param FunctionCoverageDataType|XdebugFunctionCoverageType $functionData
*/
private function initPreviouslySeenFunction(string $file, string $functionName, array $functionData): void
{
@@ -14,6 +14,7 @@ use function array_diff_key;
use function array_flip;
use function array_intersect;
use function array_intersect_key;
use function array_map;
use function count;
use function explode;
use function file_get_contents;
@@ -25,14 +26,15 @@ use function str_ends_with;
use function str_starts_with;
use function trim;
use SebastianBergmann\CodeCoverage\Driver\Driver;
use SebastianBergmann\CodeCoverage\Driver\XdebugDriver;
use SebastianBergmann\CodeCoverage\StaticAnalysis\FileAnalyser;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*
* @phpstan-import-type XdebugFunctionsCoverageType from \SebastianBergmann\CodeCoverage\Driver\XdebugDriver
* @phpstan-import-type XdebugCodeCoverageWithoutPathCoverageType from \SebastianBergmann\CodeCoverage\Driver\XdebugDriver
* @phpstan-import-type XdebugCodeCoverageWithPathCoverageType from \SebastianBergmann\CodeCoverage\Driver\XdebugDriver
* @phpstan-import-type XdebugFunctionsCoverageType from XdebugDriver
* @phpstan-import-type XdebugCodeCoverageWithoutPathCoverageType from XdebugDriver
* @phpstan-import-type XdebugCodeCoverageWithPathCoverageType from XdebugDriver
*/
final class RawCodeCoverageData
{
@@ -86,11 +88,10 @@ final class RawCodeCoverageData
public static function fromUncoveredFile(string $filename, FileAnalyser $analyser): self
{
$lineCoverage = [];
foreach ($analyser->executableLinesIn($filename) as $line => $branch) {
$lineCoverage[$line] = Driver::LINE_NOT_EXECUTED;
}
$lineCoverage = array_map(
static fn (): int => Driver::LINE_NOT_EXECUTED,
$analyser->analyse($filename)->executableLines(),
);
return new self([$filename => $lineCoverage], []);
}
@@ -211,7 +212,7 @@ final class RawCodeCoverageData
*/
public function removeCoverageDataForLines(string $filename, array $lines): void
{
if (empty($lines)) {
if ($lines === []) {
return;
}
@@ -258,6 +259,9 @@ final class RawCodeCoverageData
}
}
/**
* @return array<int>
*/
private function getEmptyLinesForFile(string $filename): array
{
if (!isset(self::$emptyLineCache[$filename])) {
+5 -49
View File
@@ -12,7 +12,6 @@ namespace SebastianBergmann\CodeCoverage\Driver;
use function sprintf;
use SebastianBergmann\CodeCoverage\BranchAndPathCoverageNotSupportedException;
use SebastianBergmann\CodeCoverage\Data\RawCodeCoverageData;
use SebastianBergmann\CodeCoverage\DeadCodeDetectionNotSupportedException;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
@@ -20,41 +19,30 @@ use SebastianBergmann\CodeCoverage\DeadCodeDetectionNotSupportedException;
abstract class Driver
{
/**
* @var int
*
* @see http://xdebug.org/docs/code_coverage
*/
public const LINE_NOT_EXECUTABLE = -2;
public const int LINE_NOT_EXECUTABLE = -2;
/**
* @var int
*
* @see http://xdebug.org/docs/code_coverage
*/
public const LINE_NOT_EXECUTED = -1;
public const int LINE_NOT_EXECUTED = -1;
/**
* @var int
*
* @see http://xdebug.org/docs/code_coverage
*/
public const LINE_EXECUTED = 1;
public const int LINE_EXECUTED = 1;
/**
* @var int
*
* @see http://xdebug.org/docs/code_coverage
*/
public const BRANCH_NOT_HIT = 0;
public const int BRANCH_NOT_HIT = 0;
/**
* @var int
*
* @see http://xdebug.org/docs/code_coverage
*/
public const BRANCH_HIT = 1;
public const int BRANCH_HIT = 1;
private bool $collectBranchAndPathCoverage = false;
private bool $detectDeadCode = false;
public function canCollectBranchAndPathCoverage(): bool
{
@@ -88,38 +76,6 @@ abstract class Driver
$this->collectBranchAndPathCoverage = false;
}
public function canDetectDeadCode(): bool
{
return false;
}
public function detectsDeadCode(): bool
{
return $this->detectDeadCode;
}
/**
* @throws DeadCodeDetectionNotSupportedException
*/
public function enableDeadCodeDetection(): void
{
if (!$this->canDetectDeadCode()) {
throw new DeadCodeDetectionNotSupportedException(
sprintf(
'%s does not support dead code detection',
$this->nameAndVersion(),
),
);
}
$this->detectDeadCode = true;
}
public function disableDeadCodeDetection(): void
{
$this->detectDeadCode = false;
}
abstract public function nameAndVersion(): string;
abstract public function start(): void;
@@ -38,6 +38,9 @@ final class PcovDriver extends Driver
$this->filter = $filter;
}
/**
* @codeCoverageIgnore
*/
public function start(): void
{
start();
@@ -47,10 +50,11 @@ final class PcovDriver extends Driver
{
stop();
// @codeCoverageIgnoreStart
$filesToCollectCoverageFor = waiting();
$collected = [];
if ($filesToCollectCoverageFor) {
if ($filesToCollectCoverageFor !== []) {
if (!$this->filter->isEmpty()) {
$filesToCollectCoverageFor = array_intersect($filesToCollectCoverageFor, $this->filter->files());
}
@@ -61,6 +65,7 @@ final class PcovDriver extends Driver
}
return RawCodeCoverageData::fromXdebugWithoutPathCoverage($collected);
// @codeCoverageIgnoreEnd
}
public function nameAndVersion(): string
@@ -21,6 +21,7 @@ final class Selector
* @throws PcovNotAvailableException
* @throws XdebugNotAvailableException
* @throws XdebugNotEnabledException
* @throws XdebugVersionNotSupportedException
*/
public function forLineCoverage(Filter $filter): Driver
{
@@ -31,11 +32,7 @@ final class Selector
}
if ($runtime->hasXdebug()) {
$driver = new XdebugDriver($filter);
$driver->enableDeadCodeDetection();
return $driver;
return new XdebugDriver($filter);
}
throw new NoCodeCoverageDriverAvailableException;
@@ -45,13 +42,13 @@ final class Selector
* @throws NoCodeCoverageDriverWithPathCoverageSupportAvailableException
* @throws XdebugNotAvailableException
* @throws XdebugNotEnabledException
* @throws XdebugVersionNotSupportedException
*/
public function forLineAndPathCoverage(Filter $filter): Driver
{
if ((new Runtime)->hasXdebug()) {
$driver = new XdebugDriver($filter);
$driver->enableDeadCodeDetection();
$driver->enableBranchAndPathCoverage();
return $driver;
@@ -14,11 +14,8 @@ use const XDEBUG_CC_DEAD_CODE;
use const XDEBUG_CC_UNUSED;
use const XDEBUG_FILTER_CODE_COVERAGE;
use const XDEBUG_PATH_INCLUDE;
use function explode;
use function extension_loaded;
use function getenv;
use function in_array;
use function ini_get;
use function phpversion;
use function version_compare;
use function xdebug_get_code_coverage;
@@ -34,8 +31,8 @@ use SebastianBergmann\CodeCoverage\Filter;
*
* @see https://xdebug.org/docs/code_coverage#xdebug_get_code_coverage
*
* @phpstan-type XdebugLinesCoverageType = array<int, int>
* @phpstan-type XdebugBranchCoverageType = array{
* @phpstan-type XdebugLinesCoverageType array<int, int>
* @phpstan-type XdebugBranchCoverageType array{
* op_start: int,
* op_end: int,
* line_start: int,
@@ -44,32 +41,32 @@ use SebastianBergmann\CodeCoverage\Filter;
* out: array<int, int>,
* out_hit: array<int, int>,
* }
* @phpstan-type XdebugPathCoverageType = array{
* @phpstan-type XdebugPathCoverageType array{
* path: array<int, int>,
* hit: int,
* }
* @phpstan-type XdebugFunctionCoverageType = array{
* @phpstan-type XdebugFunctionCoverageType array{
* branches: array<int, XdebugBranchCoverageType>,
* paths: array<int, XdebugPathCoverageType>,
* }
* @phpstan-type XdebugFunctionsCoverageType = array<string, XdebugFunctionCoverageType>
* @phpstan-type XdebugPathAndBranchesCoverageType = array{
* @phpstan-type XdebugFunctionsCoverageType array<string, XdebugFunctionCoverageType>
* @phpstan-type XdebugPathAndBranchesCoverageType array{
* lines: XdebugLinesCoverageType,
* functions: XdebugFunctionsCoverageType,
* }
* @phpstan-type XdebugCodeCoverageWithoutPathCoverageType = array<string, XdebugLinesCoverageType>
* @phpstan-type XdebugCodeCoverageWithPathCoverageType = array<string, XdebugPathAndBranchesCoverageType>
* @phpstan-type XdebugCodeCoverageWithoutPathCoverageType array<string, XdebugLinesCoverageType>
* @phpstan-type XdebugCodeCoverageWithPathCoverageType array<string, XdebugPathAndBranchesCoverageType>
*/
final class XdebugDriver extends Driver
{
/**
* @throws XdebugNotAvailableException
* @throws XdebugNotEnabledException
* @throws XdebugVersionNotSupportedException
*/
public function __construct(Filter $filter)
{
$this->ensureXdebugIsAvailable();
$this->ensureXdebugCodeCoverageFeatureIsEnabled();
if (!$filter->isEmpty()) {
xdebug_set_filter(
@@ -85,18 +82,9 @@ final class XdebugDriver extends Driver
return true;
}
public function canDetectDeadCode(): bool
{
return true;
}
public function start(): void
{
$flags = XDEBUG_CC_UNUSED;
if ($this->detectsDeadCode() || $this->collectsBranchAndPathCoverage()) {
$flags |= XDEBUG_CC_DEAD_CODE;
}
$flags = XDEBUG_CC_UNUSED | XDEBUG_CC_DEAD_CODE;
if ($this->collectsBranchAndPathCoverage()) {
$flags |= XDEBUG_CC_BRANCH_CHECK;
@@ -127,35 +115,20 @@ final class XdebugDriver extends Driver
/**
* @throws XdebugNotAvailableException
* @throws XdebugNotEnabledException
* @throws XdebugVersionNotSupportedException
*/
private function ensureXdebugIsAvailable(): void
{
if (!extension_loaded('xdebug')) {
throw new XdebugNotAvailableException;
}
}
/**
* @throws XdebugNotEnabledException
*/
private function ensureXdebugCodeCoverageFeatureIsEnabled(): void
{
if (version_compare(phpversion('xdebug'), '3.1', '>=')) {
if (!in_array('coverage', xdebug_info('mode'), true)) {
throw new XdebugNotEnabledException;
}
return;
if (!version_compare(phpversion('xdebug'), '3.1', '>=')) {
throw new XdebugVersionNotSupportedException(phpversion('xdebug'));
}
$mode = getenv('XDEBUG_MODE');
if ($mode === false || $mode === '') {
$mode = ini_get('xdebug.mode');
}
if ($mode === false ||
!in_array('coverage', explode(',', $mode), true)) {
if (!in_array('coverage', xdebug_info('mode'), true)) {
throw new XdebugNotEnabledException;
}
}
@@ -1,16 +0,0 @@
<?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;
use RuntimeException;
final class DeadCodeDetectionNotSupportedException extends RuntimeException implements Exception
{
}
@@ -7,11 +7,10 @@
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage\Driver;
namespace SebastianBergmann\CodeCoverage;
use function sprintf;
use RuntimeException;
use SebastianBergmann\CodeCoverage\Exception;
final class PathExistsButIsNotDirectoryException extends RuntimeException implements Exception
{
@@ -7,11 +7,10 @@
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage\Driver;
namespace SebastianBergmann\CodeCoverage;
use function sprintf;
use RuntimeException;
use SebastianBergmann\CodeCoverage\Exception;
final class WriteOperationFailedException extends RuntimeException implements Exception
{
+1 -1
View File
@@ -88,6 +88,6 @@ final class Filter
public function isEmpty(): bool
{
return empty($this->files);
return $this->files === [];
}
}
@@ -15,20 +15,24 @@ use function str_ends_with;
use function str_replace;
use function substr;
use Countable;
use SebastianBergmann\CodeCoverage\StaticAnalysis\LinesOfCode;
use SebastianBergmann\CodeCoverage\Util\Percentage;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*
* @phpstan-import-type LinesOfCodeType from \SebastianBergmann\CodeCoverage\StaticAnalysis\FileAnalyser
* @phpstan-import-type ProcessedFunctionType from \SebastianBergmann\CodeCoverage\Node\File
* @phpstan-import-type ProcessedClassType from \SebastianBergmann\CodeCoverage\Node\File
* @phpstan-import-type ProcessedTraitType from \SebastianBergmann\CodeCoverage\Node\File
* @phpstan-import-type ProcessedFunctionType from File
* @phpstan-import-type ProcessedClassType from File
* @phpstan-import-type ProcessedTraitType from File
*/
abstract class AbstractNode implements Countable
{
private readonly string $name;
private string $pathAsString;
/**
* @var non-empty-list<self>
*/
private array $pathAsArray;
private readonly ?AbstractNode $parent;
private string $id;
@@ -61,6 +65,9 @@ abstract class AbstractNode implements Countable
return $this->pathAsString;
}
/**
* @return non-empty-list<self>
*/
public function pathAsArray(): array
{
return $this->pathAsArray;
@@ -171,6 +178,24 @@ abstract class AbstractNode implements Countable
return $this->numberOfTestedFunctions() + $this->numberOfTestedMethods();
}
/**
* @return non-negative-int
*/
public function cyclomaticComplexity(): int
{
$ccn = 0;
foreach ($this->classesAndTraits() as $classLike) {
$ccn += $classLike['ccn'];
}
foreach ($this->functions() as $function) {
$ccn += $function['ccn'];
}
return $ccn;
}
/**
* @return array<string, ProcessedClassType>
*/
@@ -186,10 +211,7 @@ abstract class AbstractNode implements Countable
*/
abstract public function functions(): array;
/**
* @return LinesOfCodeType
*/
abstract public function linesOfCode(): array;
abstract public function linesOfCode(): LinesOfCode;
abstract public function numberOfExecutableLines(): int;
+12 -9
View File
@@ -28,11 +28,11 @@ use SebastianBergmann\CodeCoverage\StaticAnalysis\FileAnalyser;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*
* @phpstan-import-type TestType from \SebastianBergmann\CodeCoverage\CodeCoverage
* @phpstan-import-type TestType from CodeCoverage
*/
final class Builder
final readonly class Builder
{
private readonly FileAnalyser $analyser;
private FileAnalyser $analyser;
public function __construct(FileAnalyser $analyser)
{
@@ -70,6 +70,8 @@ final class Builder
$filename = $root->pathAsString() . DIRECTORY_SEPARATOR . $key;
if (is_file($filename)) {
$analysisResult = $this->analyser->analyse($filename);
$root->addFile(
new File(
$key,
@@ -77,10 +79,10 @@ final class Builder
$value['lineCoverage'],
$value['functionCoverage'],
$tests,
$this->analyser->classesIn($filename),
$this->analyser->traitsIn($filename),
$this->analyser->functionsIn($filename),
$this->analyser->linesOfCodeFor($filename),
$analysisResult->classes(),
$analysisResult->traits(),
$analysisResult->functions(),
$analysisResult->linesOfCode(),
),
);
}
@@ -201,7 +203,7 @@ final class Builder
*/
private function reducePaths(ProcessedCodeCoverageData $coverage): string
{
if (empty($coverage->coveredFiles())) {
if ($coverage->coveredFiles() === []) {
return '.';
}
@@ -223,9 +225,10 @@ final class Builder
$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])) {
if ($paths[$i][0] === '') {
$paths[$i][0] = DIRECTORY_SEPARATOR;
}
}
@@ -14,10 +14,10 @@ use function sprintf;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*/
final class CrapIndex
final readonly class CrapIndex
{
private readonly int $cyclomaticComplexity;
private readonly float $codeCoverage;
private int $cyclomaticComplexity;
private float $codeCoverage;
public function __construct(int $cyclomaticComplexity, float $codeCoverage)
{
+51 -20
View File
@@ -14,11 +14,16 @@ use function assert;
use function count;
use IteratorAggregate;
use RecursiveIteratorIterator;
use SebastianBergmann\CodeCoverage\StaticAnalysis\LinesOfCode;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
* @template-implements IteratorAggregate<int, AbstractNode>
*
* @phpstan-import-type LinesOfCodeType from \SebastianBergmann\CodeCoverage\StaticAnalysis\FileAnalyser
* @phpstan-import-type ProcessedFunctionType from File
* @phpstan-import-type ProcessedClassType from File
* @phpstan-import-type ProcessedTraitType from File
*
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*/
final class Directory extends AbstractNode implements IteratorAggregate
{
@@ -35,15 +40,23 @@ final class Directory extends AbstractNode implements IteratorAggregate
/**
* @var list<File>
*/
private array $files = [];
private ?array $classes = null;
private ?array $traits = null;
private ?array $functions = null;
private array $files = [];
/**
* @var null|LinesOfCodeType
* @var ?array<string, ProcessedClassType>
*/
private ?array $linesOfCode = null;
private ?array $classes = null;
/**
* @var ?array<string, ProcessedTraitType>
*/
private ?array $traits = null;
/**
* @var ?array<string, ProcessedFunctionType>
*/
private ?array $functions = null;
private ?LinesOfCode $linesOfCode = null;
private int $numFiles = -1;
private int $numExecutableLines = -1;
private int $numExecutedLines = -1;
@@ -73,6 +86,9 @@ final class Directory extends AbstractNode implements IteratorAggregate
return $this->numFiles;
}
/**
* @return RecursiveIteratorIterator<Iterator<AbstractNode>>
*/
public function getIterator(): RecursiveIteratorIterator
{
return new RecursiveIteratorIterator(
@@ -102,21 +118,33 @@ final class Directory extends AbstractNode implements IteratorAggregate
$this->numExecutedLines = -1;
}
/**
* @return list<Directory>
*/
public function directories(): array
{
return $this->directories;
}
/**
* @return list<File>
*/
public function files(): array
{
return $this->files;
}
/**
* @return list<Directory|File>
*/
public function children(): array
{
return $this->children;
}
/**
* @return array<string, ProcessedClassType>
*/
public function classes(): array
{
if ($this->classes === null) {
@@ -133,6 +161,9 @@ final class Directory extends AbstractNode implements IteratorAggregate
return $this->classes;
}
/**
* @return array<string, ProcessedTraitType>
*/
public function traits(): array
{
if ($this->traits === null) {
@@ -149,6 +180,9 @@ final class Directory extends AbstractNode implements IteratorAggregate
return $this->traits;
}
/**
* @return array<string, ProcessedFunctionType>
*/
public function functions(): array
{
if ($this->functions === null) {
@@ -165,25 +199,22 @@ final class Directory extends AbstractNode implements IteratorAggregate
return $this->functions;
}
/**
* @return LinesOfCodeType
*/
public function linesOfCode(): array
public function linesOfCode(): LinesOfCode
{
if ($this->linesOfCode === null) {
$this->linesOfCode = [
'linesOfCode' => 0,
'commentLinesOfCode' => 0,
'nonCommentLinesOfCode' => 0,
];
$linesOfCode = 0;
$commentLinesOfCode = 0;
$nonCommentLinesOfCode = 0;
foreach ($this->children as $child) {
$childLinesOfCode = $child->linesOfCode();
$this->linesOfCode['linesOfCode'] += $childLinesOfCode['linesOfCode'];
$this->linesOfCode['commentLinesOfCode'] += $childLinesOfCode['commentLinesOfCode'];
$this->linesOfCode['nonCommentLinesOfCode'] += $childLinesOfCode['nonCommentLinesOfCode'];
$linesOfCode += $childLinesOfCode->linesOfCode();
$commentLinesOfCode += $childLinesOfCode->commentLinesOfCode();
$nonCommentLinesOfCode += $childLinesOfCode->nonCommentLinesOfCode();
}
$this->linesOfCode = new LinesOfCode($linesOfCode, $commentLinesOfCode, $nonCommentLinesOfCode);
}
return $this->linesOfCode;
+89 -85
View File
@@ -12,18 +12,21 @@ namespace SebastianBergmann\CodeCoverage\Node;
use function array_filter;
use function count;
use function range;
use SebastianBergmann\CodeCoverage\CodeCoverage;
use SebastianBergmann\CodeCoverage\StaticAnalysis\AnalysisResult;
use SebastianBergmann\CodeCoverage\StaticAnalysis\Class_;
use SebastianBergmann\CodeCoverage\StaticAnalysis\Function_;
use SebastianBergmann\CodeCoverage\StaticAnalysis\LinesOfCode;
use SebastianBergmann\CodeCoverage\StaticAnalysis\Method;
use SebastianBergmann\CodeCoverage\StaticAnalysis\Trait_;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*
* @phpstan-import-type CodeUnitFunctionType from \SebastianBergmann\CodeCoverage\StaticAnalysis\CodeUnitFindingVisitor
* @phpstan-import-type CodeUnitMethodType from \SebastianBergmann\CodeCoverage\StaticAnalysis\CodeUnitFindingVisitor
* @phpstan-import-type CodeUnitClassType from \SebastianBergmann\CodeCoverage\StaticAnalysis\CodeUnitFindingVisitor
* @phpstan-import-type CodeUnitTraitType from \SebastianBergmann\CodeCoverage\StaticAnalysis\CodeUnitFindingVisitor
* @phpstan-import-type LinesOfCodeType from \SebastianBergmann\CodeCoverage\StaticAnalysis\FileAnalyser
* @phpstan-import-type LinesType from \SebastianBergmann\CodeCoverage\StaticAnalysis\FileAnalyser
* @phpstan-import-type TestType from CodeCoverage
* @phpstan-import-type LinesType from AnalysisResult
*
* @phpstan-type ProcessedFunctionType = array{
* @phpstan-type ProcessedFunctionType array{
* functionName: string,
* namespace: string,
* signature: string,
@@ -40,7 +43,7 @@ use function range;
* crap: int|string,
* link: string
* }
* @phpstan-type ProcessedMethodType = array{
* @phpstan-type ProcessedMethodType array{
* methodName: string,
* visibility: string,
* signature: string,
@@ -57,7 +60,7 @@ use function range;
* crap: int|string,
* link: string
* }
* @phpstan-type ProcessedClassType = array{
* @phpstan-type ProcessedClassType array{
* className: string,
* namespace: string,
* methods: array<string, ProcessedMethodType>,
@@ -73,7 +76,7 @@ use function range;
* crap: int|string,
* link: string
* }
* @phpstan-type ProcessedTraitType = array{
* @phpstan-type ProcessedTraitType array{
* traitName: string,
* namespace: string,
* methods: array<string, ProcessedMethodType>,
@@ -97,6 +100,10 @@ final class File extends AbstractNode
*/
private array $lineCoverageData;
private array $functionCoverageData;
/**
* @var array<string, TestType>
*/
private readonly array $testData;
private int $numExecutableLines = 0;
private int $numExecutedLines = 0;
@@ -119,11 +126,7 @@ final class File extends AbstractNode
* @var array<string, ProcessedFunctionType>
*/
private array $functions = [];
/**
* @var LinesOfCodeType
*/
private readonly array $linesOfCode;
private readonly LinesOfCode $linesOfCode;
private ?int $numClasses = null;
private int $numTestedClasses = 0;
private ?int $numTraits = null;
@@ -133,18 +136,18 @@ final class File extends AbstractNode
private ?int $numTestedFunctions = null;
/**
* @var array<int, array|array{0: CodeUnitClassType, 1: string}|array{0: CodeUnitFunctionType}|array{0: CodeUnitTraitType, 1: string}>
* @var array<int, array|array{0: Class_, 1: string}|array{0: Function_}|array{0: Trait_, 1: string}>
*/
private array $codeUnitsByLine = [];
/**
* @param array<int, ?list<non-empty-string>> $lineCoverageData
* @param array<string, CodeUnitClassType> $classes
* @param array<string, CodeUnitTraitType> $traits
* @param array<string, CodeUnitFunctionType> $functions
* @param LinesOfCodeType $linesOfCode
* @param array<string, TestType> $testData
* @param array<string, Class_> $classes
* @param array<string, Trait_> $traits
* @param array<string, Function_> $functions
*/
public function __construct(string $name, AbstractNode $parent, array $lineCoverageData, array $functionCoverageData, array $testData, array $classes, array $traits, array $functions, array $linesOfCode)
public function __construct(string $name, AbstractNode $parent, array $lineCoverageData, array $functionCoverageData, array $testData, array $classes, array $traits, array $functions, LinesOfCode $linesOfCode)
{
parent::__construct($name, $parent);
@@ -174,6 +177,9 @@ final class File extends AbstractNode
return $this->functionCoverageData;
}
/**
* @return array<string, TestType>
*/
public function testData(): array
{
return $this->testData;
@@ -203,7 +209,7 @@ final class File extends AbstractNode
return $this->functions;
}
public function linesOfCode(): array
public function linesOfCode(): LinesOfCode
{
return $this->linesOfCode;
}
@@ -360,13 +366,13 @@ final class File extends AbstractNode
}
/**
* @param array<string, CodeUnitClassType> $classes
* @param array<string, CodeUnitTraitType> $traits
* @param array<string, CodeUnitFunctionType> $functions
* @param array<string, Class_> $classes
* @param array<string, Trait_> $traits
* @param array<string, Function_> $functions
*/
private function calculateStatistics(array $classes, array $traits, array $functions): void
{
foreach (range(1, $this->linesOfCode['linesOfCode']) as $lineNumber) {
foreach (range(1, $this->linesOfCode->linesOfCode()) as $lineNumber) {
$this->codeUnitsByLine[$lineNumber] = [];
}
@@ -374,7 +380,7 @@ final class File extends AbstractNode
$this->processTraits($traits);
$this->processFunctions($functions);
foreach (range(1, $this->linesOfCode['linesOfCode']) as $lineNumber) {
foreach (range(1, $this->linesOfCode->linesOfCode()) as $lineNumber) {
if (isset($this->lineCoverageData[$lineNumber])) {
foreach ($this->codeUnitsByLine[$lineNumber] as &$codeUnit) {
$codeUnit['executableLines']++;
@@ -398,24 +404,24 @@ final class File extends AbstractNode
foreach ($this->traits as &$trait) {
foreach ($trait['methods'] as &$method) {
$methodLineCoverage = $method['executableLines'] ? ($method['executedLines'] / $method['executableLines']) * 100 : 100;
$methodBranchCoverage = $method['executableBranches'] ? ($method['executedBranches'] / $method['executableBranches']) * 100 : 0;
$methodPathCoverage = $method['executablePaths'] ? ($method['executedPaths'] / $method['executablePaths']) * 100 : 0;
$methodLineCoverage = $method['executableLines'] > 0 ? ($method['executedLines'] / $method['executableLines']) * 100 : 100;
$methodBranchCoverage = $method['executableBranches'] > 0 ? ($method['executedBranches'] / $method['executableBranches']) * 100 : 0;
$methodPathCoverage = $method['executablePaths'] > 0 ? ($method['executedPaths'] / $method['executablePaths']) * 100 : 0;
$method['coverage'] = $methodBranchCoverage ?: $methodLineCoverage;
$method['crap'] = (new CrapIndex($method['ccn'], $methodPathCoverage ?: $methodLineCoverage))->asString();
$method['coverage'] = $methodBranchCoverage > 0 ? $methodBranchCoverage : $methodLineCoverage;
$method['crap'] = (new CrapIndex($method['ccn'], $methodPathCoverage > 0 ? $methodPathCoverage : $methodLineCoverage))->asString();
$trait['ccn'] += $method['ccn'];
}
unset($method);
$traitLineCoverage = $trait['executableLines'] ? ($trait['executedLines'] / $trait['executableLines']) * 100 : 100;
$traitBranchCoverage = $trait['executableBranches'] ? ($trait['executedBranches'] / $trait['executableBranches']) * 100 : 0;
$traitPathCoverage = $trait['executablePaths'] ? ($trait['executedPaths'] / $trait['executablePaths']) * 100 : 0;
$traitLineCoverage = $trait['executableLines'] > 0 ? ($trait['executedLines'] / $trait['executableLines']) * 100 : 100;
$traitBranchCoverage = $trait['executableBranches'] > 0 ? ($trait['executedBranches'] / $trait['executableBranches']) * 100 : 0;
$traitPathCoverage = $trait['executablePaths'] > 0 ? ($trait['executedPaths'] / $trait['executablePaths']) * 100 : 0;
$trait['coverage'] = $traitBranchCoverage ?: $traitLineCoverage;
$trait['crap'] = (new CrapIndex($trait['ccn'], $traitPathCoverage ?: $traitLineCoverage))->asString();
$trait['coverage'] = $traitBranchCoverage > 0 ? $traitBranchCoverage : $traitLineCoverage;
$trait['crap'] = (new CrapIndex($trait['ccn'], $traitPathCoverage > 0 ? $traitPathCoverage : $traitLineCoverage))->asString();
if ($trait['executableLines'] > 0 && $trait['coverage'] === 100) {
$this->numTestedClasses++;
@@ -426,24 +432,24 @@ final class File extends AbstractNode
foreach ($this->classes as &$class) {
foreach ($class['methods'] as &$method) {
$methodLineCoverage = $method['executableLines'] ? ($method['executedLines'] / $method['executableLines']) * 100 : 100;
$methodBranchCoverage = $method['executableBranches'] ? ($method['executedBranches'] / $method['executableBranches']) * 100 : 0;
$methodPathCoverage = $method['executablePaths'] ? ($method['executedPaths'] / $method['executablePaths']) * 100 : 0;
$methodLineCoverage = $method['executableLines'] > 0 ? ($method['executedLines'] / $method['executableLines']) * 100 : 100;
$methodBranchCoverage = $method['executableBranches'] > 0 ? ($method['executedBranches'] / $method['executableBranches']) * 100 : 0;
$methodPathCoverage = $method['executablePaths'] > 0 ? ($method['executedPaths'] / $method['executablePaths']) * 100 : 0;
$method['coverage'] = $methodBranchCoverage ?: $methodLineCoverage;
$method['crap'] = (new CrapIndex($method['ccn'], $methodPathCoverage ?: $methodLineCoverage))->asString();
$method['coverage'] = $methodBranchCoverage > 0 ? $methodBranchCoverage : $methodLineCoverage;
$method['crap'] = (new CrapIndex($method['ccn'], $methodPathCoverage > 0 ? $methodPathCoverage : $methodLineCoverage))->asString();
$class['ccn'] += $method['ccn'];
}
unset($method);
$classLineCoverage = $class['executableLines'] ? ($class['executedLines'] / $class['executableLines']) * 100 : 100;
$classBranchCoverage = $class['executableBranches'] ? ($class['executedBranches'] / $class['executableBranches']) * 100 : 0;
$classPathCoverage = $class['executablePaths'] ? ($class['executedPaths'] / $class['executablePaths']) * 100 : 0;
$classLineCoverage = $class['executableLines'] > 0 ? ($class['executedLines'] / $class['executableLines']) * 100 : 100;
$classBranchCoverage = $class['executableBranches'] > 0 ? ($class['executedBranches'] / $class['executableBranches']) * 100 : 0;
$classPathCoverage = $class['executablePaths'] > 0 ? ($class['executedPaths'] / $class['executablePaths']) * 100 : 0;
$class['coverage'] = $classBranchCoverage ?: $classLineCoverage;
$class['crap'] = (new CrapIndex($class['ccn'], $classPathCoverage ?: $classLineCoverage))->asString();
$class['coverage'] = $classBranchCoverage > 0 ? $classBranchCoverage : $classLineCoverage;
$class['crap'] = (new CrapIndex($class['ccn'], $classPathCoverage > 0 ? $classPathCoverage : $classLineCoverage))->asString();
if ($class['executableLines'] > 0 && $class['coverage'] === 100) {
$this->numTestedClasses++;
@@ -453,12 +459,12 @@ final class File extends AbstractNode
unset($class);
foreach ($this->functions as &$function) {
$functionLineCoverage = $function['executableLines'] ? ($function['executedLines'] / $function['executableLines']) * 100 : 100;
$functionBranchCoverage = $function['executableBranches'] ? ($function['executedBranches'] / $function['executableBranches']) * 100 : 0;
$functionPathCoverage = $function['executablePaths'] ? ($function['executedPaths'] / $function['executablePaths']) * 100 : 0;
$functionLineCoverage = $function['executableLines'] > 0 ? ($function['executedLines'] / $function['executableLines']) * 100 : 100;
$functionBranchCoverage = $function['executableBranches'] > 0 ? ($function['executedBranches'] / $function['executableBranches']) * 100 : 0;
$functionPathCoverage = $function['executablePaths'] > 0 ? ($function['executedPaths'] / $function['executablePaths']) * 100 : 0;
$function['coverage'] = $functionBranchCoverage ?: $functionLineCoverage;
$function['crap'] = (new CrapIndex($function['ccn'], $functionPathCoverage ?: $functionLineCoverage))->asString();
$function['coverage'] = $functionBranchCoverage > 0 ? $functionBranchCoverage : $functionLineCoverage;
$function['crap'] = (new CrapIndex($function['ccn'], $functionPathCoverage > 0 ? $functionPathCoverage : $functionLineCoverage))->asString();
if ($function['coverage'] === 100) {
$this->numTestedFunctions++;
@@ -467,7 +473,7 @@ final class File extends AbstractNode
}
/**
* @param array<string, CodeUnitClassType> $classes
* @param array<string, Class_> $classes
*/
private function processClasses(array $classes): void
{
@@ -476,9 +482,9 @@ final class File extends AbstractNode
foreach ($classes as $className => $class) {
$this->classes[$className] = [
'className' => $className,
'namespace' => $class['namespace'],
'namespace' => $class->namespace(),
'methods' => [],
'startLine' => $class['startLine'],
'startLine' => $class->startLine(),
'executableLines' => 0,
'executedLines' => 0,
'executableBranches' => 0,
@@ -488,11 +494,11 @@ final class File extends AbstractNode
'ccn' => 0,
'coverage' => 0,
'crap' => 0,
'link' => $link . $class['startLine'],
'link' => $link . $class->startLine(),
];
foreach ($class['methods'] as $methodName => $method) {
$methodData = $this->newMethod($className, $methodName, $method, $link);
foreach ($class->methods() as $methodName => $method) {
$methodData = $this->newMethod($className, $method, $link);
$this->classes[$className]['methods'][$methodName] = $methodData;
$this->classes[$className]['executableBranches'] += $methodData['executableBranches'];
@@ -505,7 +511,7 @@ final class File extends AbstractNode
$this->numExecutablePaths += $methodData['executablePaths'];
$this->numExecutedPaths += $methodData['executedPaths'];
foreach (range($method['startLine'], $method['endLine']) as $lineNumber) {
foreach (range($method->startLine(), $method->endLine()) as $lineNumber) {
$this->codeUnitsByLine[$lineNumber] = [
&$this->classes[$className],
&$this->classes[$className]['methods'][$methodName],
@@ -516,7 +522,7 @@ final class File extends AbstractNode
}
/**
* @param array<string, CodeUnitTraitType> $traits
* @param array<string, Trait_> $traits
*/
private function processTraits(array $traits): void
{
@@ -525,9 +531,9 @@ final class File extends AbstractNode
foreach ($traits as $traitName => $trait) {
$this->traits[$traitName] = [
'traitName' => $traitName,
'namespace' => $trait['namespace'],
'namespace' => $trait->namespace(),
'methods' => [],
'startLine' => $trait['startLine'],
'startLine' => $trait->startLine(),
'executableLines' => 0,
'executedLines' => 0,
'executableBranches' => 0,
@@ -537,11 +543,11 @@ final class File extends AbstractNode
'ccn' => 0,
'coverage' => 0,
'crap' => 0,
'link' => $link . $trait['startLine'],
'link' => $link . $trait->startLine(),
];
foreach ($trait['methods'] as $methodName => $method) {
$methodData = $this->newMethod($traitName, $methodName, $method, $link);
foreach ($trait->methods() as $methodName => $method) {
$methodData = $this->newMethod($traitName, $method, $link);
$this->traits[$traitName]['methods'][$methodName] = $methodData;
$this->traits[$traitName]['executableBranches'] += $methodData['executableBranches'];
@@ -554,7 +560,7 @@ final class File extends AbstractNode
$this->numExecutablePaths += $methodData['executablePaths'];
$this->numExecutedPaths += $methodData['executedPaths'];
foreach (range($method['startLine'], $method['endLine']) as $lineNumber) {
foreach (range($method->startLine(), $method->endLine()) as $lineNumber) {
$this->codeUnitsByLine[$lineNumber] = [
&$this->traits[$traitName],
&$this->traits[$traitName]['methods'][$methodName],
@@ -565,7 +571,7 @@ final class File extends AbstractNode
}
/**
* @param array<string, CodeUnitFunctionType> $functions
* @param array<string, Function_> $functions
*/
private function processFunctions(array $functions): void
{
@@ -574,23 +580,23 @@ final class File extends AbstractNode
foreach ($functions as $functionName => $function) {
$this->functions[$functionName] = [
'functionName' => $functionName,
'namespace' => $function['namespace'],
'signature' => $function['signature'],
'startLine' => $function['startLine'],
'endLine' => $function['endLine'],
'namespace' => $function->namespace(),
'signature' => $function->signature(),
'startLine' => $function->startLine(),
'endLine' => $function->endLine(),
'executableLines' => 0,
'executedLines' => 0,
'executableBranches' => 0,
'executedBranches' => 0,
'executablePaths' => 0,
'executedPaths' => 0,
'ccn' => $function['ccn'],
'ccn' => $function->cyclomaticComplexity(),
'coverage' => 0,
'crap' => 0,
'link' => $link . $function['startLine'],
'link' => $link . $function->startLine(),
];
foreach (range($function['startLine'], $function['endLine']) as $lineNumber) {
foreach (range($function->startLine(), $function->endLine()) as $lineNumber) {
$this->codeUnitsByLine[$lineNumber] = [&$this->functions[$functionName]];
}
@@ -634,31 +640,29 @@ final class File extends AbstractNode
}
/**
* @param CodeUnitMethodType $method
*
* @return ProcessedMethodType
*/
private function newMethod(string $className, string $methodName, array $method, string $link): array
private function newMethod(string $className, Method $method, string $link): array
{
$methodData = [
'methodName' => $methodName,
'visibility' => $method['visibility'],
'signature' => $method['signature'],
'startLine' => $method['startLine'],
'endLine' => $method['endLine'],
'methodName' => $method->name(),
'visibility' => $method->visibility()->value,
'signature' => $method->signature(),
'startLine' => $method->startLine(),
'endLine' => $method->endLine(),
'executableLines' => 0,
'executedLines' => 0,
'executableBranches' => 0,
'executedBranches' => 0,
'executablePaths' => 0,
'executedPaths' => 0,
'ccn' => $method['ccn'],
'ccn' => $method->cyclomaticComplexity(),
'coverage' => 0,
'crap' => 0,
'link' => $link . $method['startLine'],
'link' => $link . $method->startLine(),
];
$key = $className . '->' . $methodName;
$key = $className . '->' . $method->name();
if (isset($this->functionCoverageData[$key]['branches'])) {
$methodData['executableBranches'] = count(
+5 -21
View File
@@ -9,10 +9,13 @@
*/
namespace SebastianBergmann\CodeCoverage\Node;
use function assert;
use function count;
use RecursiveIterator;
/**
* @template-implements RecursiveIterator<int, AbstractNode>
*
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*/
final class Iterator implements RecursiveIterator
@@ -29,57 +32,38 @@ final class Iterator implements RecursiveIterator
$this->nodes = $node->children();
}
/**
* 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.
*/
public function getChildren(): self
{
assert($this->nodes[$this->position] instanceof Directory);
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;
+14 -18
View File
@@ -10,31 +10,31 @@
namespace SebastianBergmann\CodeCoverage\Report;
use function count;
use function dirname;
use function file_put_contents;
use function is_string;
use function ksort;
use function max;
use function range;
use function str_contains;
use function time;
use DOMDocument;
use SebastianBergmann\CodeCoverage\CodeCoverage;
use SebastianBergmann\CodeCoverage\Driver\WriteOperationFailedException;
use SebastianBergmann\CodeCoverage\Node\File;
use SebastianBergmann\CodeCoverage\Util\Filesystem;
use SebastianBergmann\CodeCoverage\Util\Xml;
use SebastianBergmann\CodeCoverage\WriteOperationFailedException;
final class Clover
{
/**
* @param null|non-empty-string $target
* @param null|non-empty-string $name
*
* @throws WriteOperationFailedException
*/
public function process(CodeCoverage $coverage, ?string $target = null, ?string $name = null): string
{
$time = (string) time();
$xmlDocument = new DOMDocument('1.0', 'UTF-8');
$xmlDocument->formatOutput = true;
$xmlDocument = new DOMDocument('1.0', 'UTF-8');
$xmlCoverage = $xmlDocument->createElement('coverage');
$xmlCoverage->setAttribute('generated', $time);
@@ -79,6 +79,7 @@ final class Clover
}
foreach ($class['methods'] as $methodName => $method) {
/** @phpstan-ignore equal.notAllowed */
if ($method['executableLines'] == 0) {
continue;
}
@@ -87,6 +88,7 @@ final class Clover
$classStatements += $method['executableLines'];
$coveredClassStatements += $method['executedLines'];
/** @phpstan-ignore equal.notAllowed */
if ($method['coverage'] == 100) {
$coveredMethods++;
}
@@ -168,8 +170,8 @@ final class Clover
$linesOfCode = $item->linesOfCode();
$xmlMetrics = $xmlDocument->createElement('metrics');
$xmlMetrics->setAttribute('loc', (string) $linesOfCode['linesOfCode']);
$xmlMetrics->setAttribute('ncloc', (string) $linesOfCode['nonCommentLinesOfCode']);
$xmlMetrics->setAttribute('loc', (string) $linesOfCode->linesOfCode());
$xmlMetrics->setAttribute('ncloc', (string) $linesOfCode->nonCommentLinesOfCode());
$xmlMetrics->setAttribute('classes', (string) $item->numberOfClassesAndTraits());
$xmlMetrics->setAttribute('methods', (string) $item->numberOfMethods());
$xmlMetrics->setAttribute('coveredmethods', (string) $item->numberOfTestedMethods());
@@ -201,8 +203,8 @@ final class Clover
$xmlMetrics = $xmlDocument->createElement('metrics');
$xmlMetrics->setAttribute('files', (string) count($report));
$xmlMetrics->setAttribute('loc', (string) $linesOfCode['linesOfCode']);
$xmlMetrics->setAttribute('ncloc', (string) $linesOfCode['nonCommentLinesOfCode']);
$xmlMetrics->setAttribute('loc', (string) $linesOfCode->linesOfCode());
$xmlMetrics->setAttribute('ncloc', (string) $linesOfCode->nonCommentLinesOfCode());
$xmlMetrics->setAttribute('classes', (string) $report->numberOfClassesAndTraits());
$xmlMetrics->setAttribute('methods', (string) $report->numberOfMethods());
$xmlMetrics->setAttribute('coveredmethods', (string) $report->numberOfTestedMethods());
@@ -214,16 +216,10 @@ final class Clover
$xmlMetrics->setAttribute('coveredelements', (string) ($report->numberOfTestedMethods() + $report->numberOfExecutedLines() + $report->numberOfExecutedBranches()));
$xmlProject->appendChild($xmlMetrics);
$buffer = $xmlDocument->saveXML();
$buffer = Xml::asString($xmlDocument);
if ($target !== null) {
if (!str_contains($target, '://')) {
Filesystem::createDirectory(dirname($target));
}
if (@file_put_contents($target, $buffer) === false) {
throw new WriteOperationFailedException($target);
}
Filesystem::write($target, $buffer);
}
return $buffer;
+11 -18
View File
@@ -12,22 +12,22 @@ namespace SebastianBergmann\CodeCoverage\Report;
use const DIRECTORY_SEPARATOR;
use function basename;
use function count;
use function dirname;
use function file_put_contents;
use function preg_match;
use function range;
use function str_contains;
use function str_replace;
use function time;
use DOMImplementation;
use SebastianBergmann\CodeCoverage\CodeCoverage;
use SebastianBergmann\CodeCoverage\Driver\WriteOperationFailedException;
use SebastianBergmann\CodeCoverage\Node\File;
use SebastianBergmann\CodeCoverage\Util\Filesystem;
use SebastianBergmann\CodeCoverage\Util\Xml;
use SebastianBergmann\CodeCoverage\WriteOperationFailedException;
final class Cobertura
{
/**
* @param null|non-empty-string $target
*
* @throws WriteOperationFailedException
*/
public function process(CodeCoverage $coverage, ?string $target = null): string
@@ -44,10 +44,9 @@ final class Cobertura
'http://cobertura.sourceforge.net/xml/coverage-04.dtd',
);
$document = $implementation->createDocument('', '', $documentType);
$document->xmlVersion = '1.0';
$document->encoding = 'UTF-8';
$document->formatOutput = true;
$document = $implementation->createDocument('', '', $documentType);
$document->xmlVersion = '1.0';
$document->encoding = 'UTF-8';
$coverageElement = $document->createElement('coverage');
@@ -153,7 +152,7 @@ final class Cobertura
$linesValid = $method['executableLines'];
$linesCovered = $method['executedLines'];
$lineRate = $linesValid === 0 ? 0 : ($linesCovered / $linesValid);
$lineRate = $linesCovered / $linesValid;
$branchesValid = $method['executableBranches'];
$branchesCovered = $method['executedBranches'];
@@ -228,7 +227,7 @@ final class Cobertura
$linesValid = $function['executableLines'];
$linesCovered = $function['executedLines'];
$lineRate = $linesValid === 0 ? 0 : ($linesCovered / $linesValid);
$lineRate = $linesCovered / $linesValid;
$functionsLinesValid += $linesValid;
$functionsLinesCovered += $linesCovered;
@@ -289,16 +288,10 @@ final class Cobertura
$coverageElement->setAttribute('complexity', (string) $complexity);
$buffer = $document->saveXML();
$buffer = Xml::asString($document);
if ($target !== null) {
if (!str_contains($target, '://')) {
Filesystem::createDirectory(dirname($target));
}
if (@file_put_contents($target, $buffer) === false) {
throw new WriteOperationFailedException($target);
}
Filesystem::write($target, $buffer);
}
return $buffer;
+11 -17
View File
@@ -10,21 +10,19 @@
namespace SebastianBergmann\CodeCoverage\Report;
use function date;
use function dirname;
use function file_put_contents;
use function htmlspecialchars;
use function is_string;
use function round;
use function str_contains;
use DOMDocument;
use SebastianBergmann\CodeCoverage\CodeCoverage;
use SebastianBergmann\CodeCoverage\Driver\WriteOperationFailedException;
use SebastianBergmann\CodeCoverage\Node\File;
use SebastianBergmann\CodeCoverage\Util\Filesystem;
use SebastianBergmann\CodeCoverage\Util\Xml;
use SebastianBergmann\CodeCoverage\WriteOperationFailedException;
final class Crap4j
final readonly class Crap4j
{
private readonly int $threshold;
private int $threshold;
public function __construct(int $threshold = 30)
{
@@ -32,12 +30,14 @@ final class Crap4j
}
/**
* @param null|non-empty-string $target
* @param null|non-empty-string $name
*
* @throws WriteOperationFailedException
*/
public function process(CodeCoverage $coverage, ?string $target = null, ?string $name = null): string
{
$document = new DOMDocument('1.0', 'UTF-8');
$document->formatOutput = true;
$document = new DOMDocument('1.0', 'UTF-8');
$root = $document->createElement('crap_result');
$document->appendChild($root);
@@ -83,7 +83,7 @@ final class Crap4j
$methodNode = $document->createElement('method');
if (!empty($class['namespace'])) {
if ($class['namespace'] !== '') {
$namespace = $class['namespace'];
}
@@ -119,16 +119,10 @@ final class Crap4j
$root->appendChild($stats);
$root->appendChild($methodsNode);
$buffer = $document->saveXML();
$buffer = Xml::asString($document);
if ($target !== null) {
if (!str_contains($target, '://')) {
Filesystem::createDirectory(dirname($target));
}
if (@file_put_contents($target, $buffer) === false) {
throw new WriteOperationFailedException($target);
}
Filesystem::write($target, $buffer);
}
return $buffer;
@@ -12,13 +12,13 @@ namespace SebastianBergmann\CodeCoverage\Report\Html;
/**
* @immutable
*/
final class Colors
final readonly class Colors
{
private readonly string $successLow;
private readonly string $successMedium;
private readonly string $successHigh;
private readonly string $warning;
private readonly string $danger;
private string $successLow;
private string $successMedium;
private string $successHigh;
private string $warning;
private string $danger;
public static function default(): self
{
@@ -15,9 +15,9 @@ use SebastianBergmann\CodeCoverage\InvalidArgumentException;
/**
* @immutable
*/
final class CustomCssFile
final readonly class CustomCssFile
{
private readonly string $path;
private string $path;
public static function default(): self
{
@@ -22,13 +22,13 @@ use SebastianBergmann\CodeCoverage\Util\Filesystem;
use SebastianBergmann\Template\Exception;
use SebastianBergmann\Template\Template;
final class Facade
final readonly class Facade
{
private readonly string $templatePath;
private readonly string $generator;
private readonly Colors $colors;
private readonly Thresholds $thresholds;
private readonly CustomCssFile $customCssFile;
private string $templatePath;
private string $generator;
private Colors $colors;
private Thresholds $thresholds;
private CustomCssFile $customCssFile;
public function __construct(string $generator = '', ?Colors $colors = null, ?Thresholds $thresholds = null, ?CustomCssFile $customCssFile = null)
{
@@ -41,16 +41,17 @@ final class Facade
public function process(CodeCoverage $coverage, string $target): void
{
$target = $this->directory($target);
$report = $coverage->getReport();
$date = date('D M j G:i:s T Y');
$target = $this->directory($target);
$report = $coverage->getReport();
$date = date('D M j G:i:s T Y');
$hasBranchCoverage = $coverage->getData(true)->functionCoverage() !== [];
$dashboard = new Dashboard(
$this->templatePath,
$this->generator,
$date,
$this->thresholds,
$coverage->collectsBranchAndPathCoverage(),
$hasBranchCoverage,
);
$directory = new Directory(
@@ -58,7 +59,7 @@ final class Facade
$this->generator,
$date,
$this->thresholds,
$coverage->collectsBranchAndPathCoverage(),
$hasBranchCoverage,
);
$file = new File(
@@ -66,7 +67,7 @@ final class Facade
$this->generator,
$date,
$this->thresholds,
$coverage->collectsBranchAndPathCoverage(),
$hasBranchCoverage,
);
$directory->render($report, $target . 'index.html');
@@ -97,8 +98,8 @@ final class Facade
{
$dir = $this->directory($target . '_css');
copy($this->templatePath . 'css/billboard.min.css', $dir . 'billboard.min.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->customCssFile->path(), $dir . 'custom.css');
copy($this->templatePath . 'css/octicons.css', $dir . 'octicons.css');
@@ -107,10 +108,9 @@ final class Facade
copy($this->templatePath . 'icons/file-directory.svg', $dir . 'file-directory.svg');
$dir = $this->directory($target . '_js');
copy($this->templatePath . 'js/billboard.pkgd.min.js', $dir . 'billboard.pkgd.min.js');
copy($this->templatePath . 'js/bootstrap.bundle.min.js', $dir . 'bootstrap.bundle.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');
}
@@ -130,12 +130,14 @@ final class Facade
try {
$template->renderTo($this->directory($target . '_css') . 'style.css');
// @codeCoverageIgnoreStart
} catch (Exception $e) {
throw new FileCouldNotBeWrittenException(
$e->getMessage(),
$e->getCode(),
$e,
);
// @codeCoverageIgnoreEnd
}
}
@@ -44,6 +44,9 @@ abstract class Renderer
$this->hasBranchCoverage = $hasBranchCoverage;
}
/**
* @param array<non-empty-string, float|int|string> $data
*/
protected function renderItemTemplate(Template $template, array $data): string
{
$numSeparator = '&nbsp;/&nbsp;';
@@ -171,8 +174,8 @@ abstract class Renderer
'version' => $this->version,
'runtime' => $this->runtimeString(),
'generator' => $this->generator,
'low_upper_bound' => $this->thresholds->lowUpperBound(),
'high_lower_bound' => $this->thresholds->highLowerBound(),
'low_upper_bound' => (string) $this->thresholds->lowUpperBound(),
'high_lower_bound' => (string) $this->thresholds->highLowerBound(),
],
);
}
@@ -10,21 +10,27 @@
namespace SebastianBergmann\CodeCoverage\Report\Html;
use function array_values;
use function arsort;
use function asort;
use function assert;
use function count;
use function explode;
use function floor;
use function json_encode;
use function sprintf;
use function str_replace;
use function uasort;
use function usort;
use SebastianBergmann\CodeCoverage\FileCouldNotBeWrittenException;
use SebastianBergmann\CodeCoverage\Node\AbstractNode;
use SebastianBergmann\CodeCoverage\Node\Directory as DirectoryNode;
use SebastianBergmann\CodeCoverage\Node\File as FileNode;
use SebastianBergmann\Template\Exception;
use SebastianBergmann\Template\Template;
/**
* @phpstan-import-type ProcessedClassType from FileNode
* @phpstan-import-type ProcessedTraitType from FileNode
*
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*/
final class Dashboard extends Renderer
@@ -81,7 +87,9 @@ final class Dashboard extends Renderer
}
/**
* Returns the data for the Class/Method Complexity charts.
* @param array<string, ProcessedClassType|ProcessedTraitType> $classes
*
* @return array{class: non-empty-string, method: non-empty-string}
*/
private function complexity(array $classes, string $baseLink): array
{
@@ -96,33 +104,39 @@ final class Dashboard extends Renderer
$result['method'][] = [
$method['coverage'],
$method['ccn'],
sprintf(
'<a href="%s">%s</a>',
str_replace($baseLink, '', $method['link']),
$methodName,
),
str_replace($baseLink, '', $method['link']),
$methodName,
$method['crap'],
];
}
$result['class'][] = [
$class['coverage'],
$class['ccn'],
sprintf(
'<a href="%s">%s</a>',
str_replace($baseLink, '', $class['link']),
$className,
),
str_replace($baseLink, '', $class['link']),
$className,
$class['crap'],
];
}
return [
'class' => json_encode($result['class']),
'method' => json_encode($result['method']),
];
usort($result['class'], static fn (mixed $a, mixed $b) => ($a[0] <=> $b[0]));
usort($result['method'], static fn (mixed $a, mixed $b) => ($a[0] <=> $b[0]));
$class = json_encode($result['class']);
assert($class !== false);
$method = json_encode($result['method']);
assert($method !== false);
return ['class' => $class, 'method' => $method];
}
/**
* Returns the data for the Class / Method Coverage Distribution chart.
* @param array<string, ProcessedClassType|ProcessedTraitType> $classes
*
* @return array{class: non-empty-string, method: non-empty-string}
*/
private function coverageDistribution(array $classes): array
{
@@ -181,14 +195,21 @@ final class Dashboard extends Renderer
}
}
return [
'class' => json_encode(array_values($result['class'])),
'method' => json_encode(array_values($result['method'])),
];
$class = json_encode(array_values($result['class']));
assert($class !== false);
$method = json_encode(array_values($result['method']));
assert($method !== false);
return ['class' => $class, 'method' => $method];
}
/**
* Returns the classes / methods with insufficient coverage.
* @param array<string, ProcessedClassType|ProcessedTraitType> $classes
*
* @return array{class: string, method: string}
*/
private function insufficientCoverage(array $classes, string $baseLink): array
{
@@ -242,7 +263,9 @@ final class Dashboard extends Renderer
}
/**
* Returns the project risks according to the CRAP index.
* @param array<string, ProcessedClassType|ProcessedTraitType> $classes
*
* @return array{class: string, method: string}
*/
private function projectRisks(array $classes, string $baseLink): array
{
@@ -259,37 +282,47 @@ final class Dashboard extends Renderer
$key = $className . '::' . $methodName;
}
$methodRisks[$key] = $method['crap'];
$methodRisks[$key] = $method;
}
}
if ($class['coverage'] < $this->thresholds->highLowerBound() &&
$class['ccn'] > count($class['methods'])) {
$classRisks[$className] = $class['crap'];
$classRisks[$className] = $class;
}
}
arsort($classRisks);
arsort($methodRisks);
uasort($classRisks, static function (array $a, array $b)
{
return ((int) ($a['crap']) <=> (int) ($b['crap'])) * -1;
});
uasort($methodRisks, static function (array $a, array $b)
{
return ((int) ($a['crap']) <=> (int) ($b['crap'])) * -1;
});
foreach ($classRisks as $className => $crap) {
foreach ($classRisks as $className => $class) {
$result['class'] .= sprintf(
' <tr><td><a href="%s">%s</a></td><td class="text-right">%d</td></tr>' . "\n",
' <tr><td><a href="%s">%s</a></td><td class="text-right">%.1f%%</td><td class="text-right">%d</td><td class="text-right">%d</td></tr>' . "\n",
str_replace($baseLink, '', $classes[$className]['link']),
$className,
$crap,
$class['coverage'],
$class['ccn'],
$class['crap'],
);
}
foreach ($methodRisks as $methodName => $crap) {
foreach ($methodRisks as $methodName => $methodVals) {
[$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",
' <tr><td><a href="%s"><abbr title="%s">%s</abbr></a></td><td class="text-right">%.1f%%</td><td class="text-right">%d</td><td class="text-right">%d</td></tr>' . "\n",
str_replace($baseLink, '', $classes[$class]['methods'][$method]['link']),
$methodName,
$method,
$crap,
$methodVals['coverage'],
$methodVals['ccn'],
$methodVals['crap'],
);
}
@@ -109,6 +109,11 @@ use SebastianBergmann\Template\Exception;
use SebastianBergmann\Template\Template;
/**
* @phpstan-import-type ProcessedClassType from FileNode
* @phpstan-import-type ProcessedTraitType from FileNode
* @phpstan-import-type ProcessedMethodType from FileNode
* @phpstan-import-type ProcessedFunctionType from FileNode
*
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*/
final class File extends Renderer
@@ -116,7 +121,7 @@ final class File extends Renderer
/**
* @var array<int,true>
*/
private const KEYWORD_TOKENS = [
private const array KEYWORD_TOKENS = [
T_ABSTRACT => true,
T_ARRAY => true,
T_AS => true,
@@ -186,8 +191,13 @@ final class File extends Renderer
T_YIELD => true,
T_YIELD_FROM => true,
];
private const int HTML_SPECIAL_CHARS_FLAGS = ENT_COMPAT | ENT_HTML401 | ENT_SUBSTITUTE;
/**
* @var array<non-empty-string, list<string>>
*/
private static array $formattedSourceCache = [];
private int $htmlSpecialCharsFlags = ENT_COMPAT | ENT_HTML401 | ENT_SUBSTITUTE;
public function render(FileNode $node, string $file): void
{
@@ -315,11 +325,14 @@ final class File extends Renderer
return $items;
}
/**
* @param array<string, ProcessedClassType|ProcessedTraitType> $items
*/
private function renderTraitOrClassItems(array $items, Template $template, Template $methodItemTemplate): string
{
$buffer = '';
if (empty($items)) {
if ($items === []) {
return $buffer;
}
@@ -419,9 +432,12 @@ final class File extends Renderer
return $buffer;
}
/**
* @param array<string, ProcessedFunctionType> $functions
*/
private function renderFunctionItems(array $functions, Template $template): string
{
if (empty($functions)) {
if ($functions === []) {
return '';
}
@@ -437,6 +453,9 @@ final class File extends Renderer
return $buffer;
}
/**
* @param ProcessedFunctionType|ProcessedMethodType $item
*/
private function renderFunctionOrMethodItem(Template $template, array $item, string $indent = ''): string
{
$numMethods = 0;
@@ -477,7 +496,7 @@ final class File extends Renderer
'%s<a href="#%d"><abbr title="%s">%s</abbr></a>',
$indent,
$item['startLine'],
htmlspecialchars($item['signature'], $this->htmlSpecialCharsFlags),
htmlspecialchars($item['signature'], self::HTML_SPECIAL_CHARS_FLAGS),
$item['functionName'] ?? $item['methodName'],
),
'numMethods' => $numMethods,
@@ -518,7 +537,7 @@ final class File extends Renderer
$popoverTitle = '';
if (array_key_exists($i, $coverageData)) {
$numTests = ($coverageData[$i] ? count($coverageData[$i]) : 0);
$numTests = ($coverageData[$i] !== null ? count($coverageData[$i]) : 0);
if ($coverageData[$i] === null) {
$trClass = 'warning';
@@ -551,11 +570,11 @@ final class File extends Renderer
$popover = '';
if (!empty($popoverTitle)) {
if ($popoverTitle !== '') {
$popover = sprintf(
' data-bs-title="%s" data-bs-content="%s" data-bs-placement="top" data-bs-html="true"',
$popoverTitle,
htmlspecialchars($popoverContent, $this->htmlSpecialCharsFlags),
htmlspecialchars($popoverContent, self::HTML_SPECIAL_CHARS_FLAGS),
);
}
@@ -580,7 +599,6 @@ final class File extends Renderer
$lineData = [];
/** @var int $line */
foreach (array_keys($codeLines) as $line) {
$lineData[$line + 1] = [
'includedInBranches' => 0,
@@ -642,7 +660,7 @@ final class File extends Renderer
$popover = sprintf(
' data-bs-title="%s" data-bs-content="%s" data-bs-placement="top" data-bs-html="true"',
$popoverTitle,
htmlspecialchars($popoverContent, $this->htmlSpecialCharsFlags),
htmlspecialchars($popoverContent, self::HTML_SPECIAL_CHARS_FLAGS),
);
}
@@ -667,7 +685,6 @@ final class File extends Renderer
$lineData = [];
/** @var int $line */
foreach (array_keys($codeLines) as $line) {
$lineData[$line + 1] = [
'includedInPaths' => [],
@@ -732,7 +749,7 @@ final class File extends Renderer
$popover = sprintf(
' data-bs-title="%s" data-bs-content="%s" data-bs-placement="top" data-bs-html="true"',
$popoverTitle,
htmlspecialchars($popoverContent, $this->htmlSpecialCharsFlags),
htmlspecialchars($popoverContent, self::HTML_SPECIAL_CHARS_FLAGS),
);
}
@@ -769,7 +786,7 @@ final class File extends Renderer
}
if ($branchStructure !== '') { // don't show empty branches
$branches .= '<h5 class="structure-heading"><a name="' . htmlspecialchars($methodName, $this->htmlSpecialCharsFlags) . '">' . $this->abbreviateMethodName($methodName) . '</a></h5>' . "\n";
$branches .= '<h5 class="structure-heading"><a name="' . htmlspecialchars($methodName, self::HTML_SPECIAL_CHARS_FLAGS) . '">' . $this->abbreviateMethodName($methodName) . '</a></h5>' . "\n";
$branches .= $branchStructure;
}
}
@@ -779,6 +796,9 @@ final class File extends Renderer
return $branchesTemplate->render();
}
/**
* @param list<string> $codeLines
*/
private function renderBranchLines(array $branch, array $codeLines, array $testData): string
{
$linesTemplate = new Template($this->templatePath . 'lines.html.dist', '{{', '}}');
@@ -826,11 +846,11 @@ final class File extends Renderer
$popover = '';
if (!empty($popoverTitle)) {
if ($popoverTitle !== '') {
$popover = sprintf(
' data-bs-title="%s" data-bs-content="%s" data-bs-placement="top" data-bs-html="true"',
$popoverTitle,
htmlspecialchars($popoverContent, $this->htmlSpecialCharsFlags),
htmlspecialchars($popoverContent, self::HTML_SPECIAL_CHARS_FLAGS),
);
}
@@ -875,7 +895,7 @@ final class File extends Renderer
}
if ($pathStructure !== '') {
$paths .= '<h5 class="structure-heading"><a name="' . htmlspecialchars($methodName, $this->htmlSpecialCharsFlags) . '">' . $this->abbreviateMethodName($methodName) . '</a></h5>' . "\n";
$paths .= '<h5 class="structure-heading"><a name="' . htmlspecialchars($methodName, self::HTML_SPECIAL_CHARS_FLAGS) . '">' . $this->abbreviateMethodName($methodName) . '</a></h5>' . "\n";
$paths .= $pathStructure;
}
}
@@ -885,6 +905,9 @@ final class File extends Renderer
return $pathsTemplate->render();
}
/**
* @param list<string> $codeLines
*/
private function renderPathLines(array $path, array $branches, array $codeLines, array $testData): string
{
$linesTemplate = new Template($this->templatePath . 'lines.html.dist', '{{', '}}');
@@ -941,11 +964,11 @@ final class File extends Renderer
$popover = '';
if (!empty($popoverTitle)) {
if ($popoverTitle !== '') {
$popover = sprintf(
' data-bs-title="%s" data-bs-content="%s" data-bs-placement="top" data-bs-html="true"',
$popoverTitle,
htmlspecialchars($popoverContent, $this->htmlSpecialCharsFlags),
htmlspecialchars($popoverContent, self::HTML_SPECIAL_CHARS_FLAGS),
);
}
@@ -966,7 +989,7 @@ final class File extends Renderer
{
$template->setVar(
[
'lineNumber' => $lineNumber,
'lineNumber' => (string) $lineNumber,
'lineContent' => $lineContent,
'class' => $class,
'popover' => $popover,
@@ -976,6 +999,11 @@ final class File extends Renderer
return $template->render();
}
/**
* @param non-empty-string $file
*
* @return list<string>
*/
private function loadFile(string $file): array
{
if (isset(self::$formattedSourceCache[$file])) {
@@ -996,14 +1024,14 @@ final class File extends Renderer
if ($token === '"' && $tokens[$j - 1] !== '\\') {
$result[$i] .= sprintf(
'<span class="string">%s</span>',
htmlspecialchars($token, $this->htmlSpecialCharsFlags),
htmlspecialchars($token, self::HTML_SPECIAL_CHARS_FLAGS),
);
$stringFlag = !$stringFlag;
} else {
$result[$i] .= sprintf(
'<span class="keyword">%s</span>',
htmlspecialchars($token, $this->htmlSpecialCharsFlags),
htmlspecialchars($token, self::HTML_SPECIAL_CHARS_FLAGS),
);
}
@@ -1015,7 +1043,7 @@ final class File extends Renderer
$value = str_replace(
["\t", ' '],
['&nbsp;&nbsp;&nbsp;&nbsp;', '&nbsp;'],
htmlspecialchars($value, $this->htmlSpecialCharsFlags),
htmlspecialchars($value, self::HTML_SPECIAL_CHARS_FLAGS),
);
if ($value === "\n") {
@@ -1114,7 +1142,7 @@ final class File extends Renderer
return sprintf(
'<li%s>%s</li>',
$testCSS,
htmlspecialchars($test, $this->htmlSpecialCharsFlags),
htmlspecialchars($test, self::HTML_SPECIAL_CHARS_FLAGS),
);
}
File diff suppressed because one or more lines are too long
@@ -1,13 +1,76 @@
:root {
--phpunit-breadcrumbs: var(--bs-gray-200);
--phpunit-success-bar: #28a745;
--phpunit-success-high: {{success-high}};
--phpunit-success-medium: {{success-medium}};
--phpunit-success-low: {{success-low}};
--phpunit-warning: {{warning}};
--phpunit-warning-bar: #ffc107;
--phpunit-danger: {{danger}};
--phpunit-danger-bar: #dc3545;
/* Implementing an auto-selection of dark/light theme via: https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/light-dark */
color-scheme: light dark;
/* PHPUnit ligh/dark colors */
--phpunit-breadcrumbs: light-dark(var(--bs-gray-200), var(--bs-gray-800));
--phpunit-success-bar: light-dark(#28a745 ,#1f8135);
--phpunit-success-high: light-dark(#99cb84, #3d5c4e);
--phpunit-success-medium: light-dark(#c3e3b5,#3c6051);
--phpunit-success-low: light-dark(#dff0d8, #2d4431);
--phpunit-warning: light-dark(#fcf8e3, #3e3408);
--phpunit-warning-bar: light-dark(#ffc107 ,#c19406);
--phpunit-danger: light-dark(#f2dede, #42221e);
--phpunit-danger-bar: light-dark(#dc3545, #a62633);
/* Bootstrap v5.3 default colors (ligth, dark) */
--bs-body-bg-rgb: light-dark((255, 255, 255), (33, 37, 41));
--bs-body-bg: light-dark(#fff, #212529);
--bs-body-color-rgb: light-dark(33, 37, 41, 222, 226, 230);
--bs-body-color: light-dark(#212529, #dee2e6);
--bs-border-color-translucent: light-dark(rgba(0, 0, 0, 0.175), rgba(255, 255, 255, 0.15));
--bs-border-color: light-dark(#dee2e6, #495057);
--bs-code-color: light-dark(#d63384, #e685b5);
--bs-danger-bg-subtle: light-dark(#f8d7da, #2c0b0e);
--bs-danger-border-subtle: light-dark(#f1aeb5, #842029);
--bs-danger-text-emphasis: light-dark(#58151c, #ea868f);
--bs-dark-bg-subtle: light-dark(#ced4da, #1a1d20);
--bs-dark-border-subtle: light-dark(#adb5bd, #343a40);
--bs-dark-text-emphasis: light-dark(#495057, #dee2e6);
--bs-emphasis-color-rgb: light-dark((0, 0, 0), (255, 255, 255));
--bs-emphasis-color: light-dark(#000, #fff);
--bs-form-invalid-border-color: light-dark(#dc3545, #ea868f);
--bs-form-invalid-color: light-dark(#dc3545, #ea868f);
--bs-form-valid-border-color: light-dark(#198754, #75b798);
--bs-form-valid-color: light-dark(#198754, #75b798);
--bs-highlight-bg: light-dark(#fff3cd, #664d03);
--bs-highlight-color: light-dark(#212529, #dee2e6);
--bs-info-bg-subtle: light-dark(#cff4fc, #032830);
--bs-info-border-subtle: light-dark(#9eeaf9, #087990);
--bs-info-text-emphasis: light-dark(#055160, #6edff6);
--bs-light-bg-subtle: light-dark(#fcfcfd, #343a40);
--bs-light-border-subtle: light-dark(#e9ecef, #495057);
--bs-light-text-emphasis: light-dark(#495057, #f8f9fa);
--bs-link-color-rgb: light-dark((13, 110, 253), (110, 168, 254));
--bs-link-color: light-dark(#0d6efd, #6ea8fe);
--bs-link-hover-color-rgb: light-dark((10, 88, 202), (139, 185, 254));
--bs-link-hover-color: light-dark(#0a58ca, #8bb9fe);
--bs-primary-bg-subtle: light-dark(#cfe2ff, #031633);
--bs-primary-border-subtle: light-dark(#9ec5fe, #084298);
--bs-primary-text-emphasis: light-dark(#052c65, #6ea8fe);
--bs-secondary-bg-rgb: light-dark((233, 236, 239), (52, 58, 64));
--bs-secondary-bg-subtle: light-dark(#e2e3e5, #161719);
--bs-secondary-bg: light-dark(#e9ecef, #343a40);
--bs-secondary-border-subtle: light-dark(#c4c8cb, #41464b);
--bs-secondary-color-rgb: light-dark((33, 37, 41), (222, 226, 230));
--bs-secondary-color: light-dark(rgba(33, 37, 41, 0.75), rgba(222, 226, 230, 0.75));
--bs-secondary-text-emphasis: light-dark(#2b2f32, #a7acb1);
--bs-success-bg-subtle: light-dark(#d1e7dd, #051b11);
--bs-success-border-subtle: light-dark(#a3cfbb, #0f5132);
--bs-success-text-emphasis: light-dark(#0a3622, #75b798);
--bs-tertiary-bg-rgb: light-dark(248, 249, 250, 43, 48, 53);
--bs-tertiary-bg: light-dark(#f8f9fa, #2b3035);
--bs-tertiary-color-rgb: light-dark((33, 37, 41), (222, 226, 230));
--bs-tertiary-color: light-dark(rgba(33, 37, 41, 0.5), rgba(222, 226, 230, 0.5));
--bs-warning-bg-subtle: light-dark(#fff3cd, #332701);
--bs-warning-border-subtle: light-dark(#ffe69c, #997404);
--bs-warning-text-emphasis: light-dark(#664d03, #ffda6a);
}
@media (prefers-color-scheme: dark) {
/* Invert icon's colors on dark mode to improve readability */
img.octicon { filter: invert(1); }
}
body {
@@ -198,4 +261,4 @@ table#code td:first-of-type a {
.progress-bar.bg-danger {
background-color: var(--phpunit-danger-bar) !important;
}
}
@@ -5,7 +5,7 @@
<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?v={{version}}" rel="stylesheet" type="text/css">
<link href="{{path_to_root}}_css/nv.d3.min.css?v={{version}}" rel="stylesheet" type="text/css">
<link href="{{path_to_root}}_css/billboard.min.css?v={{version}}" rel="stylesheet" type="text/css">
<link href="{{path_to_root}}_css/style.css?v={{version}}" rel="stylesheet" type="text/css">
<link href="{{path_to_root}}_css/custom.css" rel="stylesheet" type="text/css">
</head>
@@ -32,7 +32,7 @@
<div class="row">
<div class="col-md-6">
<h3>Coverage Distribution</h3>
<div id="classCoverageDistribution" style="height: 300px;">
<div id="classesCoverageDistribution" style="height: 300px;">
<svg></svg>
</div>
</div>
@@ -45,7 +45,7 @@
</div>
<div class="row">
<div class="col-md-6">
<h3>Insufficient Coverage</h3>
<h3 style="margin-top: 2rem;">Insufficient Coverage</h3>
<div class="scrollbox">
<table class="table">
<thead>
@@ -61,12 +61,14 @@
</div>
</div>
<div class="col-md-6">
<h3>Project Risks</h3>
<h3 style="margin-top: 2rem;">Project Risks</h3>
<div class="scrollbox">
<table class="table">
<thead>
<tr>
<th>Class</th>
<th class="text-right">Coverage</th>
<th class="text-right">Complexity</th>
<th class="text-right"><abbr title="Change Risk Anti-Patterns (CRAP) Index">CRAP</abbr></th>
</tr>
</thead>
@@ -79,13 +81,13 @@
</div>
<div class="row">
<div class="col-md-12">
<h2>Methods</h2>
<h2 style="margin-top: 3rem;">Methods</h2>
</div>
</div>
<div class="row">
<div class="col-md-6">
<h3>Coverage Distribution</h3>
<div id="methodCoverageDistribution" style="height: 300px;">
<div id="methodsCoverageDistribution" style="height: 300px;">
<svg></svg>
</div>
</div>
@@ -98,7 +100,7 @@
</div>
<div class="row">
<div class="col-md-6">
<h3>Insufficient Coverage</h3>
<h3 style="margin-top: 2rem;">Insufficient Coverage</h3>
<div class="scrollbox">
<table class="table">
<thead>
@@ -114,12 +116,14 @@
</div>
</div>
<div class="col-md-6">
<h3>Project Risks</h3>
<h3 style="margin-top: 2rem;">Project Risks</h3>
<div class="scrollbox">
<table class="table">
<thead>
<tr>
<th>Method</th>
<th class="text-right">Coverage</th>
<th class="text-right">Complexity</th>
<th class="text-right"><abbr title="Change Risk Anti-Patterns (CRAP) Index">CRAP</abbr></th>
</tr>
</thead>
@@ -137,145 +141,159 @@
</p>
</footer>
</div>
<script src="{{path_to_root}}_js/jquery.min.js?v={{version}}" type="text/javascript"></script>
<script src="{{path_to_root}}_js/d3.min.js?v={{version}}" type="text/javascript"></script>
<script src="{{path_to_root}}_js/nv.d3.min.js?v={{version}}" 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;
<script src="{{path_to_root}}_js/billboard.pkgd.min.js?v={{version}}" type="text/javascript"></script>
<script type="application/json" id="complexity_class">{{complexity_class}}</script>
<script type="application/json" id="complexity_method">{{complexity_method}}</script>
<script type="text/javascript" defer>
const barLabels = [
'0%',
'0-10%',
'10-20%',
'20-30%',
'30-40%',
'40-50%',
'50-60%',
'60-70%',
'70-80%',
'80-90%',
'90-100%',
'100%'
];
const barConfig = (name, fullName, values) => ({
axis: {
x: {
type: "category",
categories: barLabels,
},
y: {
label: {
text: `#${name}`,
position: 'outer-top',
},
},
},
bindto: `#${name}CoverageDistribution`,
data: {
columns: [
[fullName].concat(values),
],
colors: {
[fullName]: "rgba(69, 114, 167, 0.75)",
},
type: "bar",
},
bar: {
width: {
ratio: 0.9,
},
},
grid: {
focus: {
show: false,
},
x: {
show: true,
},
y: {
show: true,
},
},
legend: {
show: false,
},
tooltip: {
contents: function (data) {
return `<table class="bb-tooltip"><tbody>
<tr><th colspan="2">Coverage ${barLabels[data[0].x]}</th></tr>
<tr><td class="value">${data[0].value} ${name}</td></tr>
</tbody></table>`;
},
grouped: false,
},
});
bb.generate(
barConfig('classes', "Class Coverage", {{class_coverage_distribution}})
);
bb.generate(
barConfig('methods', "Method Coverage", {{method_coverage_distribution}})
);
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;
const scatterConfig = (name, complexityData) => ({
axis: {
x: {
label: {
text: 'Code Coverage (in percent)',
position: 'outer-right',
},
tick: {
values: [0, 20, 40, 60, 80, 100],
},
},
y: {
label: {
text: 'Cyclomatic Complexity',
position: 'outer-top',
},
min: 0,
padding: {
bottom: 0,
top: 5,
},
},
},
bindto: `#${name}Complexity`,
data: {
columns: [
["complexity_x"].concat(complexityData.map(d => d[0])),
["complexity"].concat(complexityData.map(d => d[1])),
],
onclick: function(data, element) {
window.location = complexityData[data.index][2];
},
type: "scatter",
xs: {
"complexity": "complexity_x",
},
},
grid: {
focus: {
show: true,
y: true,
},
x: {
show: true,
},
y: {
show: true,
},
},
legend: {
show: false,
},
tooltip: {
contents: function (data) {
const coverage = Math.round(data[0].x);
const complexity = data[0].value;
const targetName = complexityData[data[0].index][3];
const crap = complexityData[data[0].index][4];
return `<table class="bb-tooltip"><tbody>
<tr><th colspan="2">${targetName}</th></tr>
<tr class="bb-tooltip-name-complexity"><td>Coverage</td><td class="name">${coverage}%</td></tr>
<tr class="bb-tooltip-name-complexity"><td>Complexity</td><td class="value">${complexity}</td></tr>
<tr class="bb-tooltip-name-complexity"><td>Crap</td><td class="value">${crap}</td></tr>
</tbody></table>`;
},
grouped: false,
},
});
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"
}
];
}
});
const classComplexityData = JSON.parse(document.getElementById('complexity_class').textContent);
bb.generate(
scatterConfig("class", classComplexityData)
);
const methodComplexityData = JSON.parse(document.getElementById('complexity_method').textContent);
bb.generate(
scatterConfig("method", methodComplexityData),
);
</script>
</body>
</html>
@@ -5,7 +5,7 @@
<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?v={{version}}" rel="stylesheet" type="text/css">
<link href="{{path_to_root}}_css/nv.d3.min.css?v={{version}}" rel="stylesheet" type="text/css">
<link href="{{path_to_root}}_css/billboard.min.css?v={{version}}" rel="stylesheet" type="text/css">
<link href="{{path_to_root}}_css/style.css?v={{version}}" rel="stylesheet" type="text/css">
<link href="{{path_to_root}}_css/custom.css" rel="stylesheet" type="text/css">
</head>
@@ -32,7 +32,7 @@
<div class="row">
<div class="col-md-6">
<h3>Coverage Distribution</h3>
<div id="classCoverageDistribution" style="height: 300px;">
<div id="classesCoverageDistribution" style="height: 300px;">
<svg></svg>
</div>
</div>
@@ -45,7 +45,7 @@
</div>
<div class="row">
<div class="col-md-6">
<h3>Insufficient Coverage</h3>
<h3 style="margin-top: 2rem;">Insufficient Coverage</h3>
<div class="scrollbox">
<table class="table">
<thead>
@@ -61,7 +61,7 @@
</div>
</div>
<div class="col-md-6">
<h3>Project Risks</h3>
<h3 style="margin-top: 2rem;">Project Risks</h3>
<div class="scrollbox">
<table class="table">
<thead>
@@ -79,13 +79,13 @@
</div>
<div class="row">
<div class="col-md-12">
<h2>Methods</h2>
<h2 style="margin-top: 3rem;">Methods</h2>
</div>
</div>
<div class="row">
<div class="col-md-6">
<h3>Coverage Distribution</h3>
<div id="methodCoverageDistribution" style="height: 300px;">
<div id="methodsCoverageDistribution" style="height: 300px;">
<svg></svg>
</div>
</div>
@@ -98,7 +98,7 @@
</div>
<div class="row">
<div class="col-md-6">
<h3>Insufficient Coverage</h3>
<h3 style="margin-top: 2rem;">Insufficient Coverage</h3>
<div class="scrollbox">
<table class="table">
<thead>
@@ -114,7 +114,7 @@
</div>
</div>
<div class="col-md-6">
<h3>Project Risks</h3>
<h3 style="margin-top: 2rem;">Project Risks</h3>
<div class="scrollbox">
<table class="table">
<thead>
@@ -137,145 +137,157 @@
</p>
</footer>
</div>
<script src="{{path_to_root}}_js/jquery.min.js?v={{version}}" type="text/javascript"></script>
<script src="{{path_to_root}}_js/d3.min.js?v={{version}}" type="text/javascript"></script>
<script src="{{path_to_root}}_js/nv.d3.min.js?v={{version}}" 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;
<script src="_js/billboard.pkgd.min.js?v={{version}}" type="text/javascript"></script>
<script type="application/json" id="complexity_class">{{complexity_class}}</script>
<script type="application/json" id="complexity_method">{{complexity_method}}</script>
<script type="text/javascript" defer>
const barLabels = [
'0%',
'0-10%',
'10-20%',
'20-30%',
'30-40%',
'40-50%',
'50-60%',
'60-70%',
'70-80%',
'80-90%',
'90-100%',
'100%'
];
const barConfig = (name, fullName, values) => ({
axis: {
x: {
type: "category",
categories: barLabels,
},
y: {
label: {
text: `#${name}`,
position: 'outer-top',
},
},
},
bindto: `#${name}CoverageDistribution`,
data: {
columns: [
[fullName].concat(values),
],
colors: {
[fullName]: "rgba(69, 114, 167, 0.75)",
},
type: "bar",
},
bar: {
width: {
ratio: 0.9,
},
},
grid: {
focus: {
show: false,
},
x: {
show: true,
},
y: {
show: true,
},
},
legend: {
show: false,
},
tooltip: {
contents: function (data) {
return `<table class="bb-tooltip"><tbody>
<tr><th colspan="2">Coverage ${barLabels[data[0].x]}</th></tr>
<tr><td class="value">${data[0].value} ${name}</td></tr>
</tbody></table>`;
},
grouped: false,
},
});
bb.generate(
barConfig('classes', "Class Coverage", {{class_coverage_distribution}})
);
bb.generate(
barConfig('methods', "Method Coverage", {{method_coverage_distribution}})
);
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;
const scatterConfig = (name, complexityData) => ({
axis: {
x: {
label: {
text: 'Code Coverage (in percent)',
position: 'outer-right',
},
tick: {
values: [0, 20, 40, 60, 80, 100],
},
},
y: {
label: {
text: 'Cyclomatic Complexity',
position: 'outer-top',
},
min: 0,
padding: {
bottom: 0,
top: 5,
},
},
},
bindto: `#${name}Complexity`,
data: {
columns: [
["complexity_x"].concat(complexityData.map(d => d[0])),
["complexity"].concat(complexityData.map(d => d[1])),
],
onclick: function(data, element) {
window.location = complexityData[data.index][2];
},
type: "scatter",
xs: {
"complexity": "complexity_x",
},
},
grid: {
focus: {
show: true,
y: true,
},
x: {
show: true,
},
y: {
show: true,
},
},
legend: {
show: false,
},
tooltip: {
contents: function (data) {
const coverage = Math.round(data[0].x);
const complexity = data[0].value;
const targetName = complexityData[data[0].index][3];
return `<table class="bb-tooltip"><tbody>
<tr><th colspan="2">${targetName}</th></tr>
<tr class="bb-tooltip-name-complexity"><td>Coverage</td><td class="name">${coverage}%</td></tr>
<tr class="bb-tooltip-name-complexity"><td>Complexity</td><td class="value">${complexity}</td></tr>
</tbody></table>`;
},
grouped: false,
},
});
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"
}
];
}
});
const classComplexityData = JSON.parse(document.getElementById('complexity_class').textContent);
bb.generate(
scatterConfig("class", classComplexityData)
);
const methodComplexityData = JSON.parse(document.getElementById('complexity_method').textContent);
bb.generate(
scatterConfig("method", methodComplexityData),
);
</script>
</body>
</html>
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+7 -11
View File
@@ -10,16 +10,18 @@
namespace SebastianBergmann\CodeCoverage\Report;
use const PHP_EOL;
use function dirname;
use function file_put_contents;
use function serialize;
use function str_contains;
use SebastianBergmann\CodeCoverage\CodeCoverage;
use SebastianBergmann\CodeCoverage\Driver\WriteOperationFailedException;
use SebastianBergmann\CodeCoverage\Util\Filesystem;
use SebastianBergmann\CodeCoverage\WriteOperationFailedException;
final class PHP
{
/**
* @param null|non-empty-string $target
*
* @throws WriteOperationFailedException
*/
public function process(CodeCoverage $coverage, ?string $target = null): string
{
$coverage->clearCache();
@@ -28,13 +30,7 @@ final class PHP
return \unserialize(<<<'END_OF_COVERAGE_SERIALIZATION'" . PHP_EOL . serialize($coverage) . PHP_EOL . 'END_OF_COVERAGE_SERIALIZATION' . PHP_EOL . ');';
if ($target !== null) {
if (!str_contains($target, '://')) {
Filesystem::createDirectory(dirname($target));
}
if (@file_put_contents($target, $buffer) === false) {
throw new WriteOperationFailedException($target);
}
Filesystem::write($target, $buffer);
}
return $buffer;
+9 -25
View File
@@ -23,30 +23,11 @@ use SebastianBergmann\CodeCoverage\Util\Percentage;
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";
private const string COLOR_GREEN = "\x1b[30;42m";
private const string COLOR_YELLOW = "\x1b[30;43m";
private const string COLOR_RED = "\x1b[37;41m";
private const string COLOR_HEADER = "\x1b[1;37;40m";
private const string COLOR_RESET = "\x1b[0m";
private readonly Thresholds $thresholds;
private readonly bool $showUncoveredFiles;
private readonly bool $showOnlySummary;
@@ -60,7 +41,7 @@ final class Text
public function process(CodeCoverage $coverage, bool $showColors = false): string
{
$hasBranchCoverage = !empty($coverage->getData(true)->functionCoverage());
$hasBranchCoverage = $coverage->getData(true)->functionCoverage() !== [];
$output = PHP_EOL . PHP_EOL;
$report = $coverage->getReport();
@@ -210,6 +191,7 @@ final class Text
$classMethods = 0;
foreach ($class['methods'] as $method) {
/** @phpstan-ignore equal.notAllowed */
if ($method['executableLines'] == 0) {
continue;
}
@@ -222,6 +204,7 @@ final class Text
$classExecutablePaths += $method['executablePaths'];
$classExecutedPaths += $method['executedPaths'];
/** @phpstan-ignore equal.notAllowed */
if ($method['coverage'] == 100) {
$coveredMethods++;
}
@@ -251,6 +234,7 @@ final class Text
$resetColor = '';
foreach ($classCoverage as $fullQualifiedPath => $classInfo) {
/** @phpstan-ignore notEqual.notAllowed */
if ($this->showUncoveredFiles || $classInfo['statementsCovered'] != 0) {
if ($showColors) {
$methodColor = $this->coverageColor($classInfo['methodsCovered'], $classInfo['methodCount']);
@@ -14,10 +14,10 @@ use SebastianBergmann\CodeCoverage\InvalidArgumentException;
/**
* @immutable
*/
final class Thresholds
final readonly class Thresholds
{
private readonly int $lowUpperBound;
private readonly int $highLowerBound;
private int $lowUpperBound;
private int $highLowerBound;
public static function default(): self
{
@@ -18,9 +18,9 @@ use SebastianBergmann\Environment\Runtime;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*/
final class BuildInformation
final readonly class BuildInformation
{
private readonly DOMElement $contextNode;
private DOMElement $contextNode;
public function __construct(DOMElement $contextNode)
{
@@ -66,7 +66,7 @@ final class BuildInformation
$name,
)->item(0);
if (!$node) {
if ($node === null) {
$node = $this->contextNode->appendChild(
$this->contextNode->ownerDocument->createElementNS(
'https://schema.phpunit.de/coverage/1.0',
@@ -28,7 +28,7 @@ final class Coverage
$this->writer = new XMLWriter;
$this->writer->openMemory();
$this->writer->startElementNS(null, $context->nodeName, 'https://schema.phpunit.de/coverage/1.0');
$this->writer->startElementNs(null, $context->nodeName, 'https://schema.phpunit.de/coverage/1.0');
$this->writer->writeAttribute('nr', $line);
}
@@ -38,7 +38,9 @@ final class Coverage
public function addTest(string $test): void
{
if ($this->finalized) {
// @codeCoverageIgnoreStart
throw new ReportAlreadyFinalizedException;
// @codeCoverageIgnoreEnd
}
$this->writer->startElement('covered');
@@ -10,34 +10,37 @@
namespace SebastianBergmann\CodeCoverage\Report\Xml;
use const DIRECTORY_SEPARATOR;
use const PHP_EOL;
use function count;
use function dirname;
use function file_get_contents;
use function file_put_contents;
use function is_array;
use function is_dir;
use function is_file;
use function is_writable;
use function libxml_clear_errors;
use function libxml_get_errors;
use function libxml_use_internal_errors;
use function sprintf;
use function strlen;
use function substr;
use DateTimeImmutable;
use DOMDocument;
use SebastianBergmann\CodeCoverage\CodeCoverage;
use SebastianBergmann\CodeCoverage\Driver\PathExistsButIsNotDirectoryException;
use SebastianBergmann\CodeCoverage\Driver\WriteOperationFailedException;
use SebastianBergmann\CodeCoverage\Node\AbstractNode;
use SebastianBergmann\CodeCoverage\Node\Directory as DirectoryNode;
use SebastianBergmann\CodeCoverage\Node\File;
use SebastianBergmann\CodeCoverage\Node\File as FileNode;
use SebastianBergmann\CodeCoverage\Util\Filesystem as DirectoryUtil;
use SebastianBergmann\CodeCoverage\PathExistsButIsNotDirectoryException;
use SebastianBergmann\CodeCoverage\Util\Filesystem;
use SebastianBergmann\CodeCoverage\Util\Xml;
use SebastianBergmann\CodeCoverage\Version;
use SebastianBergmann\CodeCoverage\WriteOperationFailedException;
use SebastianBergmann\CodeCoverage\XmlException;
use SebastianBergmann\Environment\Runtime;
/**
* @phpstan-import-type ProcessedClassType from File
* @phpstan-import-type ProcessedTraitType from File
* @phpstan-import-type ProcessedFunctionType from File
* @phpstan-import-type TestType from CodeCoverage
*/
final class Facade
{
private string $target;
@@ -89,6 +92,7 @@ final class Facade
private function initTargetDirectory(string $directory): void
{
if (is_file($directory)) {
// @codeCoverageIgnoreStart
if (!is_dir($directory)) {
throw new PathExistsButIsNotDirectoryException($directory);
}
@@ -96,9 +100,10 @@ final class Facade
if (!is_writable($directory)) {
throw new WriteOperationFailedException($directory);
}
// @codeCoverageIgnoreEnd
}
DirectoryUtil::createDirectory($directory);
Filesystem::createDirectory($directory);
}
/**
@@ -175,6 +180,9 @@ final class Facade
$this->saveDocument($fileReport->asDom(), $file->id());
}
/**
* @param ProcessedClassType|ProcessedTraitType $unit
*/
private function processUnit(array $unit, Report $report): void
{
if (isset($unit['className'])) {
@@ -205,6 +213,9 @@ final class Facade
}
}
/**
* @param ProcessedFunctionType $function
*/
private function processFunction(array $function, Report $report): void
{
$functionObject = $report->functionObject($function['functionName']);
@@ -215,6 +226,9 @@ final class Facade
$functionObject->setTotals((string) $function['executableLines'], (string) $function['executedLines'], (string) $function['coverage']);
}
/**
* @param array<string, TestType> $tests
*/
private function processTests(array $tests): void
{
$testsObject = $this->project->tests();
@@ -229,9 +243,9 @@ final class Facade
$loc = $node->linesOfCode();
$totals->setNumLines(
$loc['linesOfCode'],
$loc['commentLinesOfCode'],
$loc['nonCommentLinesOfCode'],
$loc->linesOfCode(),
$loc->commentLinesOfCode(),
$loc->nonCommentLinesOfCode(),
$node->numberOfExecutableLines(),
$node->numberOfExecutedLines(),
);
@@ -269,36 +283,8 @@ final class Facade
{
$filename = sprintf('%s/%s.xml', $this->targetDirectory(), $name);
$document->formatOutput = true;
$document->preserveWhiteSpace = false;
$this->initTargetDirectory(dirname($filename));
file_put_contents($filename, $this->documentAsString($document));
}
/**
* @throws XmlException
*
* @see https://bugs.php.net/bug.php?id=79191
*/
private function documentAsString(DOMDocument $document): string
{
$xmlErrorHandling = libxml_use_internal_errors(true);
$xml = $document->saveXML();
if ($xml === false) {
$message = 'Unable to generate the XML';
foreach (libxml_get_errors() as $error) {
$message .= PHP_EOL . $error->message;
}
throw new XmlException($message);
}
libxml_clear_errors();
libxml_use_internal_errors($xmlErrorHandling);
return $xml;
Filesystem::write($filename, Xml::asString($document));
}
}
@@ -9,6 +9,7 @@
*/
namespace SebastianBergmann\CodeCoverage\Report\Xml;
use function assert;
use DOMDocument;
use DOMElement;
@@ -30,7 +31,7 @@ class File
{
$totalsContainer = $this->contextNode->firstChild;
if (!$totalsContainer) {
if ($totalsContainer === null) {
$totalsContainer = $this->contextNode->appendChild(
$this->dom->createElementNS(
'https://schema.phpunit.de/coverage/1.0',
@@ -39,6 +40,8 @@ class File
);
}
assert($totalsContainer instanceof DOMElement);
return new Totals($totalsContainer);
}
@@ -49,7 +52,7 @@ class File
'coverage',
)->item(0);
if (!$coverage) {
if ($coverage === null) {
$coverage = $this->contextNode->appendChild(
$this->dom->createElementNS(
'https://schema.phpunit.de/coverage/1.0',
@@ -65,6 +68,8 @@ class File
),
);
assert($lineNode instanceof DOMElement);
return new Coverage($lineNode, $line);
}
@@ -14,9 +14,9 @@ use DOMElement;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*/
final class Method
final readonly class Method
{
private readonly DOMElement $contextNode;
private DOMElement $contextNode;
public function __construct(DOMElement $context, string $name)
{
@@ -9,6 +9,7 @@
*/
namespace SebastianBergmann\CodeCoverage\Report\Xml;
use function assert;
use DOMDocument;
use DOMElement;
@@ -34,7 +35,7 @@ abstract class Node
{
$totalsContainer = $this->contextNode()->firstChild;
if (!$totalsContainer) {
if ($totalsContainer === null) {
$totalsContainer = $this->contextNode()->appendChild(
$this->dom->createElementNS(
'https://schema.phpunit.de/coverage/1.0',
@@ -43,6 +44,8 @@ abstract class Node
);
}
assert($totalsContainer instanceof DOMElement);
return new Totals($totalsContainer);
}
@@ -9,13 +9,18 @@
*/
namespace SebastianBergmann\CodeCoverage\Report\Xml;
use function assert;
use DOMDocument;
use DOMElement;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*/
final class Project extends Node
{
/**
* @phpstan-ignore constructor.missingParentCall
*/
public function __construct(string $directory)
{
$this->init();
@@ -34,7 +39,7 @@ final class Project extends Node
'build',
)->item(0);
if (!$buildNode) {
if ($buildNode === null) {
$buildNode = $this->dom()->documentElement->appendChild(
$this->dom()->createElementNS(
'https://schema.phpunit.de/coverage/1.0',
@@ -43,6 +48,8 @@ final class Project extends Node
);
}
assert($buildNode instanceof DOMElement);
return new BuildInformation($buildNode);
}
@@ -53,7 +60,7 @@ final class Project extends Node
'tests',
)->item(0);
if (!$testsNode) {
if ($testsNode === null) {
$testsNode = $this->contextNode()->appendChild(
$this->dom()->createElementNS(
'https://schema.phpunit.de/coverage/1.0',
@@ -62,6 +69,8 @@ final class Project extends Node
);
}
assert($testsNode instanceof DOMElement);
return new Tests($testsNode);
}
@@ -9,9 +9,11 @@
*/
namespace SebastianBergmann\CodeCoverage\Report\Xml;
use function assert;
use function basename;
use function dirname;
use DOMDocument;
use DOMElement;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
@@ -38,7 +40,7 @@ final class Report extends File
return $this->dom();
}
public function functionObject($name): Method
public function functionObject(string $name): Method
{
$node = $this->contextNode()->appendChild(
$this->dom()->createElementNS(
@@ -47,15 +49,17 @@ final class Report extends File
),
);
assert($node instanceof DOMElement);
return new Method($node, $name);
}
public function classObject($name): Unit
public function classObject(string $name): Unit
{
return $this->unitObject('class', $name);
}
public function traitObject($name): Unit
public function traitObject(string $name): Unit
{
return $this->unitObject('trait', $name);
}
@@ -67,7 +71,7 @@ final class Report extends File
'source',
)->item(0);
if (!$source) {
if ($source === null) {
$source = $this->contextNode()->appendChild(
$this->dom()->createElementNS(
'https://schema.phpunit.de/coverage/1.0',
@@ -76,6 +80,8 @@ final class Report extends File
);
}
assert($source instanceof DOMElement);
return new Source($source);
}
@@ -85,7 +91,7 @@ final class Report extends File
$this->contextNode()->setAttribute('path', dirname($name));
}
private function unitObject(string $tagName, $name): Unit
private function unitObject(string $tagName, string $name): Unit
{
$node = $this->contextNode()->appendChild(
$this->dom()->createElementNS(
@@ -94,6 +100,8 @@ final class Report extends File
),
);
assert($node instanceof DOMElement);
return new Unit($node, $name);
}
}
@@ -17,9 +17,9 @@ use TheSeer\Tokenizer\XMLSerializer;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*/
final class Source
final readonly class Source
{
private readonly DOMElement $context;
private DOMElement $context;
public function __construct(DOMElement $context)
{
@@ -11,15 +11,16 @@ namespace SebastianBergmann\CodeCoverage\Report\Xml;
use function assert;
use DOMElement;
use SebastianBergmann\CodeCoverage\CodeCoverage;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*
* @phpstan-import-type TestType from \SebastianBergmann\CodeCoverage\CodeCoverage
* @phpstan-import-type TestType from CodeCoverage
*/
final class Tests
final readonly class Tests
{
private readonly DOMElement $contextNode;
private DOMElement $contextNode;
public function __construct(DOMElement $context)
{
@@ -11,25 +11,22 @@ namespace SebastianBergmann\CodeCoverage\Report\Xml;
use function sprintf;
use DOMElement;
use DOMNode;
use SebastianBergmann\CodeCoverage\Util\Percentage;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*/
final class Totals
final readonly class Totals
{
private readonly DOMNode $container;
private readonly DOMElement $linesNode;
private readonly DOMElement $methodsNode;
private readonly DOMElement $functionsNode;
private readonly DOMElement $classesNode;
private readonly DOMElement $traitsNode;
private DOMElement $linesNode;
private DOMElement $methodsNode;
private DOMElement $functionsNode;
private DOMElement $classesNode;
private DOMElement $traitsNode;
public function __construct(DOMElement $container)
{
$this->container = $container;
$dom = $container->ownerDocument;
$dom = $container->ownerDocument;
$this->linesNode = $dom->createElementNS(
'https://schema.phpunit.de/coverage/1.0',
@@ -63,11 +60,6 @@ final class Totals
$container->appendChild($this->traitsNode);
}
public function container(): DOMNode
{
return $this->container;
}
public function setNumLines(int $loc, int $cloc, int $ncloc, int $executable, int $executed): void
{
$this->linesNode->setAttribute('total', (string) $loc);
@@ -15,9 +15,9 @@ use DOMElement;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*/
final class Unit
final readonly class Unit
{
private readonly DOMElement $contextNode;
private DOMElement $contextNode;
public function __construct(DOMElement $context, string $name)
{
@@ -45,7 +45,7 @@ final class Unit
'namespace',
)->item(0);
if (!$node) {
if ($node === null) {
$node = $this->contextNode->appendChild(
$this->contextNode->ownerDocument->createElementNS(
'https://schema.phpunit.de/coverage/1.0',
@@ -68,6 +68,8 @@ final class Unit
),
);
assert($node instanceof DOMElement);
return new Method($node, $name);
}
@@ -9,24 +9,36 @@
*/
namespace SebastianBergmann\CodeCoverage\StaticAnalysis;
use function file_get_contents;
use SebastianBergmann\CodeCoverage\Filter;
final class CacheWarmer
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*/
final readonly class CacheWarmer
{
public function warmCache(string $cacheDirectory, bool $useAnnotationsForIgnoringCode, bool $ignoreDeprecatedCode, Filter $filter): void
/**
* @return array{cacheHits: non-negative-int, cacheMisses: non-negative-int}
*/
public function warmCache(string $cacheDirectory, bool $useAnnotationsForIgnoringCode, bool $ignoreDeprecatedCode, Filter $filter): array
{
$analyser = new CachingFileAnalyser(
$analyser = new CachingSourceAnalyser(
$cacheDirectory,
new ParsingFileAnalyser(
$useAnnotationsForIgnoringCode,
$ignoreDeprecatedCode,
),
$useAnnotationsForIgnoringCode,
$ignoreDeprecatedCode,
new ParsingSourceAnalyser,
);
foreach ($filter->files() as $file) {
$analyser->process($file);
$analyser->analyse(
$file,
file_get_contents($file),
$useAnnotationsForIgnoringCode,
$ignoreDeprecatedCode,
);
}
return [
'cacheHits' => $analyser->cacheHits(),
'cacheMisses' => $analyser->cacheMisses(),
];
}
}
@@ -1,184 +0,0 @@
<?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\StaticAnalysis;
use const DIRECTORY_SEPARATOR;
use function file_get_contents;
use function file_put_contents;
use function implode;
use function is_file;
use function md5;
use function serialize;
use function unserialize;
use SebastianBergmann\CodeCoverage\Util\Filesystem;
use SebastianBergmann\CodeCoverage\Version;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*
* @phpstan-import-type CodeUnitFunctionType from \SebastianBergmann\CodeCoverage\StaticAnalysis\CodeUnitFindingVisitor
* @phpstan-import-type CodeUnitMethodType from \SebastianBergmann\CodeCoverage\StaticAnalysis\CodeUnitFindingVisitor
* @phpstan-import-type CodeUnitClassType from \SebastianBergmann\CodeCoverage\StaticAnalysis\CodeUnitFindingVisitor
* @phpstan-import-type CodeUnitTraitType from \SebastianBergmann\CodeCoverage\StaticAnalysis\CodeUnitFindingVisitor
* @phpstan-import-type LinesOfCodeType from \SebastianBergmann\CodeCoverage\StaticAnalysis\FileAnalyser
* @phpstan-import-type LinesType from \SebastianBergmann\CodeCoverage\StaticAnalysis\FileAnalyser
*/
final class CachingFileAnalyser implements FileAnalyser
{
private readonly string $directory;
private readonly FileAnalyser $analyser;
private readonly bool $useAnnotationsForIgnoringCode;
private readonly bool $ignoreDeprecatedCode;
private array $cache = [];
public function __construct(string $directory, FileAnalyser $analyser, bool $useAnnotationsForIgnoringCode, bool $ignoreDeprecatedCode)
{
Filesystem::createDirectory($directory);
$this->analyser = $analyser;
$this->directory = $directory;
$this->useAnnotationsForIgnoringCode = $useAnnotationsForIgnoringCode;
$this->ignoreDeprecatedCode = $ignoreDeprecatedCode;
}
/**
* @return array<string, CodeUnitClassType>
*/
public function classesIn(string $filename): array
{
if (!isset($this->cache[$filename])) {
$this->process($filename);
}
return $this->cache[$filename]['classesIn'];
}
/**
* @return array<string, CodeUnitTraitType>
*/
public function traitsIn(string $filename): array
{
if (!isset($this->cache[$filename])) {
$this->process($filename);
}
return $this->cache[$filename]['traitsIn'];
}
/**
* @return array<string, CodeUnitFunctionType>
*/
public function functionsIn(string $filename): array
{
if (!isset($this->cache[$filename])) {
$this->process($filename);
}
return $this->cache[$filename]['functionsIn'];
}
/**
* @return LinesOfCodeType
*/
public function linesOfCodeFor(string $filename): array
{
if (!isset($this->cache[$filename])) {
$this->process($filename);
}
return $this->cache[$filename]['linesOfCodeFor'];
}
/**
* @return LinesType
*/
public function executableLinesIn(string $filename): array
{
if (!isset($this->cache[$filename])) {
$this->process($filename);
}
return $this->cache[$filename]['executableLinesIn'];
}
/**
* @return LinesType
*/
public function ignoredLinesFor(string $filename): array
{
if (!isset($this->cache[$filename])) {
$this->process($filename);
}
return $this->cache[$filename]['ignoredLinesFor'];
}
public function process(string $filename): void
{
$cache = $this->read($filename);
if ($cache !== false) {
$this->cache[$filename] = $cache;
return;
}
$this->cache[$filename] = [
'classesIn' => $this->analyser->classesIn($filename),
'traitsIn' => $this->analyser->traitsIn($filename),
'functionsIn' => $this->analyser->functionsIn($filename),
'linesOfCodeFor' => $this->analyser->linesOfCodeFor($filename),
'ignoredLinesFor' => $this->analyser->ignoredLinesFor($filename),
'executableLinesIn' => $this->analyser->executableLinesIn($filename),
];
$this->write($filename, $this->cache[$filename]);
}
private function read(string $filename): array|false
{
$cacheFile = $this->cacheFile($filename);
if (!is_file($cacheFile)) {
return false;
}
return unserialize(
file_get_contents($cacheFile),
['allowed_classes' => false],
);
}
private function write(string $filename, array $data): void
{
file_put_contents(
$this->cacheFile($filename),
serialize($data),
);
}
private function cacheFile(string $filename): string
{
$cacheKey = md5(
implode(
"\0",
[
$filename,
file_get_contents($filename),
Version::id(),
$this->useAnnotationsForIgnoringCode,
$this->ignoreDeprecatedCode,
],
),
);
return $this->directory . DIRECTORY_SEPARATOR . $cacheKey;
}
}
@@ -1,360 +0,0 @@
<?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\StaticAnalysis;
use function assert;
use function implode;
use function rtrim;
use function trim;
use PhpParser\Node;
use PhpParser\Node\ComplexType;
use PhpParser\Node\Identifier;
use PhpParser\Node\IntersectionType;
use PhpParser\Node\Name;
use PhpParser\Node\NullableType;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\Stmt\Enum_;
use PhpParser\Node\Stmt\Function_;
use PhpParser\Node\Stmt\Interface_;
use PhpParser\Node\Stmt\Trait_;
use PhpParser\Node\UnionType;
use PhpParser\NodeTraverser;
use PhpParser\NodeVisitorAbstract;
use SebastianBergmann\Complexity\CyclomaticComplexityCalculatingVisitor;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*
* @phpstan-type CodeUnitFunctionType = array{
* name: string,
* namespacedName: string,
* namespace: string,
* signature: string,
* startLine: int,
* endLine: int,
* ccn: int
* }
* @phpstan-type CodeUnitMethodType = array{
* methodName: string,
* signature: string,
* visibility: string,
* startLine: int,
* endLine: int,
* ccn: int
* }
* @phpstan-type CodeUnitClassType = array{
* name: string,
* namespacedName: string,
* namespace: string,
* startLine: int,
* endLine: int,
* methods: array<string, CodeUnitMethodType>
* }
* @phpstan-type CodeUnitTraitType = array{
* name: string,
* namespacedName: string,
* namespace: string,
* startLine: int,
* endLine: int,
* methods: array<string, CodeUnitMethodType>
* }
*/
final class CodeUnitFindingVisitor extends NodeVisitorAbstract
{
/**
* @var array<string, CodeUnitClassType>
*/
private array $classes = [];
/**
* @var array<string, CodeUnitTraitType>
*/
private array $traits = [];
/**
* @var array<string, CodeUnitFunctionType>
*/
private array $functions = [];
public function enterNode(Node $node): void
{
if ($node instanceof Class_) {
if ($node->isAnonymous()) {
return;
}
$this->processClass($node);
}
if ($node instanceof Trait_) {
$this->processTrait($node);
}
if (!$node instanceof ClassMethod && !$node instanceof Function_) {
return;
}
if ($node instanceof ClassMethod) {
$parentNode = $node->getAttribute('parent');
if ($parentNode instanceof Class_ && $parentNode->isAnonymous()) {
return;
}
$this->processMethod($node);
return;
}
$this->processFunction($node);
}
/**
* @return array<string, CodeUnitClassType>
*/
public function classes(): array
{
return $this->classes;
}
/**
* @return array<string, CodeUnitTraitType>
*/
public function traits(): array
{
return $this->traits;
}
/**
* @return array<string, CodeUnitFunctionType>
*/
public function functions(): array
{
return $this->functions;
}
private function cyclomaticComplexity(ClassMethod|Function_ $node): int
{
$nodes = $node->getStmts();
if ($nodes === null) {
return 0;
}
$traverser = new NodeTraverser;
$cyclomaticComplexityCalculatingVisitor = new CyclomaticComplexityCalculatingVisitor;
$traverser->addVisitor($cyclomaticComplexityCalculatingVisitor);
/* @noinspection UnusedFunctionResultInspection */
$traverser->traverse($nodes);
return $cyclomaticComplexityCalculatingVisitor->cyclomaticComplexity();
}
private function signature(ClassMethod|Function_ $node): string
{
$signature = ($node->returnsByRef() ? '&' : '') . $node->name->toString() . '(';
$parameters = [];
foreach ($node->getParams() as $parameter) {
assert(isset($parameter->var->name));
$parameterAsString = '';
if ($parameter->type !== null) {
$parameterAsString = $this->type($parameter->type) . ' ';
}
$parameterAsString .= '$' . $parameter->var->name;
/* @todo Handle default values */
$parameters[] = $parameterAsString;
}
$signature .= implode(', ', $parameters) . ')';
$returnType = $node->getReturnType();
if ($returnType !== null) {
$signature .= ': ' . $this->type($returnType);
}
return $signature;
}
private function type(ComplexType|Identifier|Name $type): string
{
if ($type instanceof NullableType) {
return '?' . $type->type;
}
if ($type instanceof UnionType) {
return $this->unionTypeAsString($type);
}
if ($type instanceof IntersectionType) {
return $this->intersectionTypeAsString($type);
}
return $type->toString();
}
private function visibility(ClassMethod $node): string
{
if ($node->isPrivate()) {
return 'private';
}
if ($node->isProtected()) {
return 'protected';
}
return 'public';
}
private function processClass(Class_ $node): void
{
$name = $node->name->toString();
$namespacedName = $node->namespacedName->toString();
$this->classes[$namespacedName] = [
'name' => $name,
'namespacedName' => $namespacedName,
'namespace' => $this->namespace($namespacedName, $name),
'startLine' => $node->getStartLine(),
'endLine' => $node->getEndLine(),
'methods' => [],
];
}
private function processTrait(Trait_ $node): void
{
$name = $node->name->toString();
$namespacedName = $node->namespacedName->toString();
$this->traits[$namespacedName] = [
'name' => $name,
'namespacedName' => $namespacedName,
'namespace' => $this->namespace($namespacedName, $name),
'startLine' => $node->getStartLine(),
'endLine' => $node->getEndLine(),
'methods' => [],
];
}
private function processMethod(ClassMethod $node): void
{
$parentNode = $node->getAttribute('parent');
if ($parentNode instanceof Interface_) {
return;
}
assert($parentNode instanceof Class_ || $parentNode instanceof Trait_ || $parentNode instanceof Enum_);
assert(isset($parentNode->name));
assert(isset($parentNode->namespacedName));
assert($parentNode->namespacedName instanceof Name);
$parentName = $parentNode->name->toString();
$parentNamespacedName = $parentNode->namespacedName->toString();
if ($parentNode instanceof Class_) {
$storage = &$this->classes;
} else {
$storage = &$this->traits;
}
if (!isset($storage[$parentNamespacedName])) {
$storage[$parentNamespacedName] = [
'name' => $parentName,
'namespacedName' => $parentNamespacedName,
'namespace' => $this->namespace($parentNamespacedName, $parentName),
'startLine' => $parentNode->getStartLine(),
'endLine' => $parentNode->getEndLine(),
'methods' => [],
];
}
$storage[$parentNamespacedName]['methods'][$node->name->toString()] = [
'methodName' => $node->name->toString(),
'signature' => $this->signature($node),
'visibility' => $this->visibility($node),
'startLine' => $node->getStartLine(),
'endLine' => $node->getEndLine(),
'ccn' => $this->cyclomaticComplexity($node),
];
}
private function processFunction(Function_ $node): void
{
assert(isset($node->name));
assert(isset($node->namespacedName));
assert($node->namespacedName instanceof Name);
$name = $node->name->toString();
$namespacedName = $node->namespacedName->toString();
$this->functions[$namespacedName] = [
'name' => $name,
'namespacedName' => $namespacedName,
'namespace' => $this->namespace($namespacedName, $name),
'signature' => $this->signature($node),
'startLine' => $node->getStartLine(),
'endLine' => $node->getEndLine(),
'ccn' => $this->cyclomaticComplexity($node),
];
}
private function namespace(string $namespacedName, string $name): string
{
return trim(rtrim($namespacedName, $name), '\\');
}
private function unionTypeAsString(UnionType $node): string
{
$types = [];
foreach ($node->types as $type) {
if ($type instanceof IntersectionType) {
$types[] = '(' . $this->intersectionTypeAsString($type) . ')';
continue;
}
$types[] = $this->typeAsString($type);
}
return implode('|', $types);
}
private function intersectionTypeAsString(IntersectionType $node): string
{
$types = [];
foreach ($node->types as $type) {
$types[] = $this->typeAsString($type);
}
return implode('&', $types);
}
private function typeAsString(Identifier|Name $node): string
{
if ($node instanceof Name) {
return $node->toCodeString();
}
return $node->toString();
}
}
@@ -1,398 +0,0 @@
<?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\StaticAnalysis;
use function array_diff_key;
use function assert;
use function count;
use function current;
use function end;
use function explode;
use function max;
use function preg_match;
use function preg_quote;
use function range;
use function reset;
use function sprintf;
use PhpParser\Node;
use PhpParser\NodeVisitorAbstract;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*
* @phpstan-import-type LinesType from \SebastianBergmann\CodeCoverage\StaticAnalysis\FileAnalyser
*/
final class ExecutableLinesFindingVisitor extends NodeVisitorAbstract
{
private int $nextBranch = 0;
private readonly string $source;
/**
* @var LinesType
*/
private array $executableLinesGroupedByBranch = [];
/**
* @var array<int, bool>
*/
private array $unsets = [];
/**
* @var array<int, string>
*/
private array $commentsToCheckForUnset = [];
public function __construct(string $source)
{
$this->source = $source;
}
public function enterNode(Node $node): void
{
foreach ($node->getComments() as $comment) {
$commentLine = $comment->getStartLine();
if (!isset($this->executableLinesGroupedByBranch[$commentLine])) {
continue;
}
foreach (explode("\n", $comment->getText()) as $text) {
$this->commentsToCheckForUnset[$commentLine] = $text;
$commentLine++;
}
}
if ($node instanceof Node\Scalar\String_ ||
$node instanceof Node\Scalar\EncapsedStringPart) {
$startLine = $node->getStartLine() + 1;
$endLine = $node->getEndLine() - 1;
if ($startLine <= $endLine) {
foreach (range($startLine, $endLine) as $line) {
unset($this->executableLinesGroupedByBranch[$line]);
}
}
return;
}
if ($node instanceof Node\Stmt\Interface_) {
foreach (range($node->getStartLine(), $node->getEndLine()) as $line) {
$this->unsets[$line] = true;
}
return;
}
if ($node instanceof Node\Stmt\Declare_ ||
$node instanceof Node\Stmt\DeclareDeclare ||
$node instanceof Node\Stmt\Else_ ||
$node instanceof Node\Stmt\EnumCase ||
$node instanceof Node\Stmt\Finally_ ||
$node instanceof Node\Stmt\GroupUse ||
$node instanceof Node\Stmt\Label ||
$node instanceof Node\Stmt\Namespace_ ||
$node instanceof Node\Stmt\Nop ||
$node instanceof Node\Stmt\Switch_ ||
$node instanceof Node\Stmt\TryCatch ||
$node instanceof Node\Stmt\Use_ ||
$node instanceof Node\Stmt\UseUse ||
$node instanceof Node\Expr\ConstFetch ||
$node instanceof Node\Expr\Variable ||
$node instanceof Node\Expr\Throw_ ||
$node instanceof Node\ComplexType ||
$node instanceof Node\Const_ ||
$node instanceof Node\Identifier ||
$node instanceof Node\Name ||
$node instanceof Node\Param ||
$node instanceof Node\Scalar) {
return;
}
if ($node instanceof Node\Expr\Match_) {
foreach ($node->arms as $arm) {
$this->setLineBranch(
$arm->body->getStartLine(),
$arm->body->getEndLine(),
++$this->nextBranch,
);
}
return;
}
if ($node instanceof Node\Stmt\Expression && $node->expr instanceof Node\Expr\Throw_) {
$this->setLineBranch($node->expr->expr->getEndLine(), $node->expr->expr->getEndLine(), ++$this->nextBranch);
return;
}
if ($node instanceof Node\Stmt\Enum_ ||
$node instanceof Node\Stmt\Function_ ||
$node instanceof Node\Stmt\Class_ ||
$node instanceof Node\Stmt\ClassMethod ||
$node instanceof Node\Expr\Closure ||
$node instanceof Node\Stmt\Trait_) {
if ($node instanceof Node\Stmt\Function_ || $node instanceof Node\Stmt\ClassMethod) {
$unsets = [];
foreach ($node->getParams() as $param) {
foreach (range($param->getStartLine(), $param->getEndLine()) as $line) {
$unsets[$line] = true;
}
}
unset($unsets[$node->getEndLine()]);
$this->unsets += $unsets;
}
$isConcreteClassLike = $node instanceof Node\Stmt\Enum_ || $node instanceof Node\Stmt\Class_ || $node instanceof Node\Stmt\Trait_;
if (null !== $node->stmts) {
foreach ($node->stmts as $stmt) {
if ($stmt instanceof Node\Stmt\Nop) {
continue;
}
foreach (range($stmt->getStartLine(), $stmt->getEndLine()) as $line) {
unset($this->executableLinesGroupedByBranch[$line]);
if (
$isConcreteClassLike &&
!$stmt instanceof Node\Stmt\ClassMethod
) {
$this->unsets[$line] = true;
}
}
}
}
if ($isConcreteClassLike) {
return;
}
$hasEmptyBody = [] === $node->stmts ||
null === $node->stmts ||
(
1 === count($node->stmts) &&
$node->stmts[0] instanceof Node\Stmt\Nop
);
if ($hasEmptyBody) {
if ($node->getEndLine() === $node->getStartLine() && isset($this->executableLinesGroupedByBranch[$node->getStartLine()])) {
return;
}
$this->setLineBranch($node->getEndLine(), $node->getEndLine(), ++$this->nextBranch);
return;
}
return;
}
if ($node instanceof Node\Expr\ArrowFunction) {
$startLine = max(
$node->getStartLine() + 1,
$node->expr->getStartLine(),
);
$endLine = $node->expr->getEndLine();
if ($endLine < $startLine) {
return;
}
$this->setLineBranch($startLine, $endLine, ++$this->nextBranch);
return;
}
if ($node instanceof Node\Expr\Ternary) {
if (null !== $node->if &&
$node->getStartLine() !== $node->if->getEndLine()) {
$this->setLineBranch($node->if->getStartLine(), $node->if->getEndLine(), ++$this->nextBranch);
}
if ($node->getStartLine() !== $node->else->getEndLine()) {
$this->setLineBranch($node->else->getStartLine(), $node->else->getEndLine(), ++$this->nextBranch);
}
return;
}
if ($node instanceof Node\Expr\BinaryOp\Coalesce) {
if ($node->getStartLine() !== $node->getEndLine()) {
$this->setLineBranch($node->getEndLine(), $node->getEndLine(), ++$this->nextBranch);
}
return;
}
if ($node instanceof Node\Stmt\If_ ||
$node instanceof Node\Stmt\ElseIf_ ||
$node instanceof Node\Stmt\Case_) {
if (null === $node->cond) {
return;
}
$this->setLineBranch(
$node->cond->getStartLine(),
$node->cond->getStartLine(),
++$this->nextBranch,
);
return;
}
if ($node instanceof Node\Stmt\For_) {
$startLine = null;
$endLine = null;
if ([] !== $node->init) {
$startLine = $node->init[0]->getStartLine();
end($node->init);
$endLine = current($node->init)->getEndLine();
reset($node->init);
}
if ([] !== $node->cond) {
if (null === $startLine) {
$startLine = $node->cond[0]->getStartLine();
}
end($node->cond);
$endLine = current($node->cond)->getEndLine();
reset($node->cond);
}
if ([] !== $node->loop) {
if (null === $startLine) {
$startLine = $node->loop[0]->getStartLine();
}
end($node->loop);
$endLine = current($node->loop)->getEndLine();
reset($node->loop);
}
if (null === $startLine || null === $endLine) {
return;
}
$this->setLineBranch(
$startLine,
$endLine,
++$this->nextBranch,
);
return;
}
if ($node instanceof Node\Stmt\Foreach_) {
$this->setLineBranch(
$node->expr->getStartLine(),
$node->valueVar->getEndLine(),
++$this->nextBranch,
);
return;
}
if ($node instanceof Node\Stmt\While_ ||
$node instanceof Node\Stmt\Do_) {
$this->setLineBranch(
$node->cond->getStartLine(),
$node->cond->getEndLine(),
++$this->nextBranch,
);
return;
}
if ($node instanceof Node\Stmt\Catch_) {
assert([] !== $node->types);
$startLine = $node->types[0]->getStartLine();
end($node->types);
$endLine = current($node->types)->getEndLine();
$this->setLineBranch(
$startLine,
$endLine,
++$this->nextBranch,
);
return;
}
if ($node instanceof Node\Expr\CallLike) {
if (isset($this->executableLinesGroupedByBranch[$node->getStartLine()])) {
$branch = $this->executableLinesGroupedByBranch[$node->getStartLine()];
} else {
$branch = ++$this->nextBranch;
}
$this->setLineBranch($node->getStartLine(), $node->getEndLine(), $branch);
return;
}
if (isset($this->executableLinesGroupedByBranch[$node->getStartLine()])) {
return;
}
$this->setLineBranch($node->getStartLine(), $node->getEndLine(), ++$this->nextBranch);
}
public function afterTraverse(array $nodes): void
{
$lines = explode("\n", $this->source);
foreach ($lines as $lineNumber => $line) {
$lineNumber++;
if (1 === preg_match('/^\s*$/', $line) ||
(
isset($this->commentsToCheckForUnset[$lineNumber]) &&
1 === preg_match(sprintf('/^\s*%s\s*$/', preg_quote($this->commentsToCheckForUnset[$lineNumber], '/')), $line)
)) {
unset($this->executableLinesGroupedByBranch[$lineNumber]);
}
}
$this->executableLinesGroupedByBranch = array_diff_key(
$this->executableLinesGroupedByBranch,
$this->unsets,
);
}
/**
* @return LinesType
*/
public function executableLinesGroupedByBranch(): array
{
return $this->executableLinesGroupedByBranch;
}
private function setLineBranch(int $start, int $end, int $branch): void
{
foreach (range($start, $end) as $line) {
$this->executableLinesGroupedByBranch[$line] = $branch;
}
}
}
@@ -9,50 +9,45 @@
*/
namespace SebastianBergmann\CodeCoverage\StaticAnalysis;
use function file_get_contents;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*
* @phpstan-import-type CodeUnitFunctionType from \SebastianBergmann\CodeCoverage\StaticAnalysis\CodeUnitFindingVisitor
* @phpstan-import-type CodeUnitMethodType from \SebastianBergmann\CodeCoverage\StaticAnalysis\CodeUnitFindingVisitor
* @phpstan-import-type CodeUnitClassType from \SebastianBergmann\CodeCoverage\StaticAnalysis\CodeUnitFindingVisitor
* @phpstan-import-type CodeUnitTraitType from \SebastianBergmann\CodeCoverage\StaticAnalysis\CodeUnitFindingVisitor
*
* @phpstan-type LinesOfCodeType = array{
* linesOfCode: int,
* commentLinesOfCode: int,
* nonCommentLinesOfCode: int
* }
* @phpstan-type LinesType = array<int, int>
*/
interface FileAnalyser
final class FileAnalyser
{
/**
* @return array<string, CodeUnitClassType>
*/
public function classesIn(string $filename): array;
private readonly SourceAnalyser $sourceAnalyser;
private readonly bool $useAnnotationsForIgnoringCode;
private readonly bool $ignoreDeprecatedCode;
/**
* @return array<string, CodeUnitTraitType>
* @var array<non-empty-string, AnalysisResult>
*/
public function traitsIn(string $filename): array;
private array $cache = [];
public function __construct(SourceAnalyser $sourceAnalyser, bool $useAnnotationsForIgnoringCode, bool $ignoreDeprecatedCode)
{
$this->sourceAnalyser = $sourceAnalyser;
$this->useAnnotationsForIgnoringCode = $useAnnotationsForIgnoringCode;
$this->ignoreDeprecatedCode = $ignoreDeprecatedCode;
}
/**
* @return array<string, CodeUnitFunctionType>
* @param non-empty-string $sourceCodeFile
*/
public function functionsIn(string $filename): array;
public function analyse(string $sourceCodeFile): AnalysisResult
{
if (isset($this->cache[$sourceCodeFile])) {
return $this->cache[$sourceCodeFile];
}
/**
* @return LinesOfCodeType
*/
public function linesOfCodeFor(string $filename): array;
$this->cache[$sourceCodeFile] = $this->sourceAnalyser->analyse(
$sourceCodeFile,
file_get_contents($sourceCodeFile),
$this->useAnnotationsForIgnoringCode,
$this->ignoreDeprecatedCode,
);
/**
* @return LinesType
*/
public function executableLinesIn(string $filename): array;
/**
* @return LinesType
*/
public function ignoredLinesFor(string $filename): array;
return $this->cache[$sourceCodeFile];
}
}
@@ -1,121 +0,0 @@
<?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\StaticAnalysis;
use function assert;
use function str_contains;
use PhpParser\Node;
use PhpParser\Node\Attribute;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\Stmt\Enum_;
use PhpParser\Node\Stmt\Function_;
use PhpParser\Node\Stmt\Interface_;
use PhpParser\Node\Stmt\Trait_;
use PhpParser\NodeVisitorAbstract;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*/
final class IgnoredLinesFindingVisitor extends NodeVisitorAbstract
{
/**
* @var array<int>
*/
private array $ignoredLines = [];
private readonly bool $useAnnotationsForIgnoringCode;
private readonly bool $ignoreDeprecated;
public function __construct(bool $useAnnotationsForIgnoringCode, bool $ignoreDeprecated)
{
$this->useAnnotationsForIgnoringCode = $useAnnotationsForIgnoringCode;
$this->ignoreDeprecated = $ignoreDeprecated;
}
public function enterNode(Node $node): void
{
if (!$node instanceof Class_ &&
!$node instanceof Trait_ &&
!$node instanceof Interface_ &&
!$node instanceof Enum_ &&
!$node instanceof ClassMethod &&
!$node instanceof Function_ &&
!$node instanceof Attribute) {
return;
}
if ($node instanceof Class_ && $node->isAnonymous()) {
return;
}
if ($node instanceof Class_ ||
$node instanceof Trait_ ||
$node instanceof Interface_ ||
$node instanceof Attribute) {
$this->ignoredLines[] = $node->getStartLine();
assert($node->name !== null);
// Workaround for https://github.com/nikic/PHP-Parser/issues/886
$this->ignoredLines[] = $node->name->getStartLine();
}
if (!$this->useAnnotationsForIgnoringCode) {
return;
}
if ($node instanceof Interface_) {
return;
}
if ($node instanceof Attribute &&
$node->name->toString() === 'PHPUnit\Framework\Attributes\CodeCoverageIgnore') {
$attributeGroup = $node->getAttribute('parent');
$attributedNode = $attributeGroup->getAttribute('parent');
for ($line = $attributedNode->getStartLine(); $line <= $attributedNode->getEndLine(); $line++) {
$this->ignoredLines[] = $line;
}
return;
}
$this->processDocComment($node);
}
/**
* @return array<int>
*/
public function ignoredLines(): array
{
return $this->ignoredLines;
}
private function processDocComment(Node $node): void
{
$docComment = $node->getDocComment();
if ($docComment === null) {
return;
}
if (str_contains($docComment->getText(), '@codeCoverageIgnore')) {
for ($line = $node->getStartLine(); $line <= $node->getEndLine(); $line++) {
$this->ignoredLines[] = $line;
}
}
if ($this->ignoreDeprecated && str_contains($docComment->getText(), '@deprecated')) {
for ($line = $node->getStartLine(); $line <= $node->getEndLine(); $line++) {
$this->ignoredLines[] = $line;
}
}
}
}
@@ -1,267 +0,0 @@
<?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\StaticAnalysis;
use const T_COMMENT;
use const T_DOC_COMMENT;
use function array_merge;
use function array_unique;
use function assert;
use function file_get_contents;
use function is_array;
use function max;
use function range;
use function sort;
use function sprintf;
use function substr_count;
use function token_get_all;
use function trim;
use PhpParser\Error;
use PhpParser\NodeTraverser;
use PhpParser\NodeVisitor\NameResolver;
use PhpParser\NodeVisitor\ParentConnectingVisitor;
use PhpParser\ParserFactory;
use SebastianBergmann\CodeCoverage\ParserException;
use SebastianBergmann\LinesOfCode\LineCountingVisitor;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*
* @phpstan-import-type CodeUnitFunctionType from \SebastianBergmann\CodeCoverage\StaticAnalysis\CodeUnitFindingVisitor
* @phpstan-import-type CodeUnitMethodType from \SebastianBergmann\CodeCoverage\StaticAnalysis\CodeUnitFindingVisitor
* @phpstan-import-type CodeUnitClassType from \SebastianBergmann\CodeCoverage\StaticAnalysis\CodeUnitFindingVisitor
* @phpstan-import-type CodeUnitTraitType from \SebastianBergmann\CodeCoverage\StaticAnalysis\CodeUnitFindingVisitor
* @phpstan-import-type LinesOfCodeType from \SebastianBergmann\CodeCoverage\StaticAnalysis\FileAnalyser
* @phpstan-import-type LinesType from \SebastianBergmann\CodeCoverage\StaticAnalysis\FileAnalyser
*/
final class ParsingFileAnalyser implements FileAnalyser
{
/**
* @var array<string, array<string, CodeUnitClassType>>
*/
private array $classes = [];
/**
* @var array<string, array<string, CodeUnitTraitType>>
*/
private array $traits = [];
/**
* @var array<string, array<string, CodeUnitFunctionType>>
*/
private array $functions = [];
/**
* @var array<string, LinesOfCodeType>
*/
private array $linesOfCode = [];
/**
* @var array<string, LinesType>
*/
private array $ignoredLines = [];
/**
* @var array<string, LinesType>
*/
private array $executableLines = [];
private readonly bool $useAnnotationsForIgnoringCode;
private readonly bool $ignoreDeprecatedCode;
public function __construct(bool $useAnnotationsForIgnoringCode, bool $ignoreDeprecatedCode)
{
$this->useAnnotationsForIgnoringCode = $useAnnotationsForIgnoringCode;
$this->ignoreDeprecatedCode = $ignoreDeprecatedCode;
}
/**
* @return array<string, CodeUnitClassType>
*/
public function classesIn(string $filename): array
{
$this->analyse($filename);
return $this->classes[$filename];
}
/**
* @return array<string, CodeUnitTraitType>
*/
public function traitsIn(string $filename): array
{
$this->analyse($filename);
return $this->traits[$filename];
}
/**
* @return array<string, CodeUnitFunctionType>
*/
public function functionsIn(string $filename): array
{
$this->analyse($filename);
return $this->functions[$filename];
}
/**
* @return LinesOfCodeType
*/
public function linesOfCodeFor(string $filename): array
{
$this->analyse($filename);
return $this->linesOfCode[$filename];
}
/**
* @return LinesType
*/
public function executableLinesIn(string $filename): array
{
$this->analyse($filename);
return $this->executableLines[$filename];
}
/**
* @return LinesType
*/
public function ignoredLinesFor(string $filename): array
{
$this->analyse($filename);
return $this->ignoredLines[$filename];
}
/**
* @throws ParserException
*/
private function analyse(string $filename): void
{
if (isset($this->classes[$filename])) {
return;
}
$source = file_get_contents($filename);
$linesOfCode = max(substr_count($source, "\n") + 1, substr_count($source, "\r") + 1);
if ($linesOfCode === 0 && !empty($source)) {
$linesOfCode = 1;
}
assert($linesOfCode > 0);
$parser = (new ParserFactory)->createForHostVersion();
try {
$nodes = $parser->parse($source);
assert($nodes !== null);
$traverser = new NodeTraverser;
$codeUnitFindingVisitor = new CodeUnitFindingVisitor;
$lineCountingVisitor = new LineCountingVisitor($linesOfCode);
$ignoredLinesFindingVisitor = new IgnoredLinesFindingVisitor($this->useAnnotationsForIgnoringCode, $this->ignoreDeprecatedCode);
$executableLinesFindingVisitor = new ExecutableLinesFindingVisitor($source);
$traverser->addVisitor(new NameResolver);
$traverser->addVisitor(new ParentConnectingVisitor);
$traverser->addVisitor($codeUnitFindingVisitor);
$traverser->addVisitor($lineCountingVisitor);
$traverser->addVisitor($ignoredLinesFindingVisitor);
$traverser->addVisitor($executableLinesFindingVisitor);
/* @noinspection UnusedFunctionResultInspection */
$traverser->traverse($nodes);
// @codeCoverageIgnoreStart
} catch (Error $error) {
throw new ParserException(
sprintf(
'Cannot parse %s: %s',
$filename,
$error->getMessage(),
),
$error->getCode(),
$error,
);
}
// @codeCoverageIgnoreEnd
$this->classes[$filename] = $codeUnitFindingVisitor->classes();
$this->traits[$filename] = $codeUnitFindingVisitor->traits();
$this->functions[$filename] = $codeUnitFindingVisitor->functions();
$this->executableLines[$filename] = $executableLinesFindingVisitor->executableLinesGroupedByBranch();
$this->ignoredLines[$filename] = [];
$this->findLinesIgnoredByLineBasedAnnotations($filename, $source, $this->useAnnotationsForIgnoringCode);
$this->ignoredLines[$filename] = array_unique(
array_merge(
$this->ignoredLines[$filename],
$ignoredLinesFindingVisitor->ignoredLines(),
),
);
sort($this->ignoredLines[$filename]);
$result = $lineCountingVisitor->result();
$this->linesOfCode[$filename] = [
'linesOfCode' => $result->linesOfCode(),
'commentLinesOfCode' => $result->commentLinesOfCode(),
'nonCommentLinesOfCode' => $result->nonCommentLinesOfCode(),
];
}
private function findLinesIgnoredByLineBasedAnnotations(string $filename, string $source, bool $useAnnotationsForIgnoringCode): void
{
if (!$useAnnotationsForIgnoringCode) {
return;
}
$start = false;
foreach (token_get_all($source) as $token) {
if (!is_array($token) ||
!(T_COMMENT === $token[0] || T_DOC_COMMENT === $token[0])) {
continue;
}
$comment = trim($token[1]);
if ($comment === '// @codeCoverageIgnore' ||
$comment === '//@codeCoverageIgnore') {
$this->ignoredLines[$filename][] = $token[2];
continue;
}
if ($comment === '// @codeCoverageIgnoreStart' ||
$comment === '//@codeCoverageIgnoreStart') {
$start = $token[2];
continue;
}
if ($comment === '// @codeCoverageIgnoreEnd' ||
$comment === '//@codeCoverageIgnoreEnd') {
if (false === $start) {
$start = $token[2];
}
$this->ignoredLines[$filename] = array_merge(
$this->ignoredLines[$filename],
range($start, $token[2]),
);
}
}
}
}
@@ -9,9 +9,13 @@
*/
namespace SebastianBergmann\CodeCoverage\Util;
use function dirname;
use function file_put_contents;
use function is_dir;
use function mkdir;
use function sprintf;
use function str_contains;
use SebastianBergmann\CodeCoverage\WriteOperationFailedException;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
@@ -34,4 +38,20 @@ final class Filesystem
);
}
}
/**
* @param non-empty-string $target
*
* @throws WriteOperationFailedException
*/
public static function write(string $target, string $buffer): void
{
if (!str_contains($target, '://')) {
self::createDirectory(dirname($target));
}
if (@file_put_contents($target, $buffer) === false) {
throw new WriteOperationFailedException($target);
}
}
}
@@ -14,10 +14,10 @@ use function sprintf;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*/
final class Percentage
final readonly class Percentage
{
private readonly float $fraction;
private readonly float $total;
private float $fraction;
private float $total;
public static function fromFractionAndTotal(float $fraction, float $total): self
{
+1 -1
View File
@@ -19,7 +19,7 @@ final class Version
public static function id(): string
{
if (self::$version === '') {
self::$version = (new VersionId('11.0.11', dirname(__DIR__)))->asString();
self::$version = (new VersionId('12.4.0', dirname(__DIR__)))->asString();
}
return self::$version;
+7
View File
@@ -2,6 +2,12 @@
All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/).
## [6.0.0] - 2025-02-07
### Removed
* This component is no longer supported on PHP 8.2
## [5.1.0] - 2024-08-27
### Added
@@ -168,6 +174,7 @@ No changes
* [#23](https://github.com/sebastianbergmann/php-file-iterator/pull/23): Added support for wildcards (glob) in exclude
[6.0.0]: https://github.com/sebastianbergmann/php-file-iterator/compare/5.1...6.0.0
[5.1.0]: https://github.com/sebastianbergmann/php-file-iterator/compare/5.0.1...5.1.0
[5.0.1]: https://github.com/sebastianbergmann/php-file-iterator/compare/5.0.0...5.0.1
[5.0.0]: https://github.com/sebastianbergmann/php-file-iterator/compare/4.1...5.0.0
+1 -1
View File
@@ -1,6 +1,6 @@
BSD 3-Clause License
Copyright (c) 2009-2024, Sebastian Bergmann
Copyright (c) 2009-2025, Sebastian Bergmann
All rights reserved.
Redistribution and use in source and binary forms, with or without
+4 -4
View File
@@ -21,17 +21,17 @@
},
"config": {
"platform": {
"php": "8.2.0"
"php": "8.3.0"
},
"optimize-autoloader": true,
"sort-packages": true
},
"prefer-stable": true,
"require": {
"php": ">=8.2"
"php": ">=8.3"
},
"require-dev": {
"phpunit/phpunit": "^11.0"
"phpunit/phpunit": "^12.0"
},
"autoload": {
"classmap": [
@@ -40,7 +40,7 @@
},
"extra": {
"branch-alias": {
"dev-main": "5.0-dev"
"dev-main": "6.0-dev"
}
}
}
+9 -6
View File
@@ -37,6 +37,8 @@ final class Factory
* @param list<non-empty-string>|string $suffixes
* @param list<non-empty-string>|string $prefixes
* @param list<non-empty-string> $exclude
*
* @phpstan-ignore missingType.generics
*/
public function getFileIterator(array|string $paths, array|string $suffixes = '', array|string $prefixes = '', array $exclude = []): AppendIterator
{
@@ -113,7 +115,7 @@ final class Factory
*
* @return list<string>
*/
private function globstar(string $pattern)
private function globstar(string $pattern): array
{
if (stripos($pattern, '**') === false) {
$files = glob($pattern, GLOB_ONLYDIR);
@@ -125,23 +127,24 @@ final class Factory
$patterns = [$rootPattern . $restPattern];
$rootPattern .= '/*';
while ($dirs = glob($rootPattern, GLOB_ONLYDIR)) {
while ($directories = glob($rootPattern, GLOB_ONLYDIR)) {
$rootPattern .= '/*';
foreach ($dirs as $dir) {
$patterns[] = $dir . $restPattern;
foreach ($directories as $directory) {
$patterns[] = $directory . $restPattern;
}
}
$files = [];
foreach ($patterns as $pat) {
$files = array_merge($files, $this->globstar($pat));
foreach ($patterns as $_pattern) {
$files = array_merge($files, $this->globstar($_pattern));
}
}
if ($files !== false) {
$files = array_unique($files);
sort($files);
return $files;
+2 -5
View File
@@ -9,7 +9,6 @@
*/
namespace SebastianBergmann\FileIterator;
use function assert;
use function preg_match;
use function realpath;
use function str_ends_with;
@@ -25,8 +24,8 @@ use SplFileInfo;
*/
final class Iterator extends FilterIterator
{
public const PREFIX = 0;
public const SUFFIX = 1;
public const int PREFIX = 0;
public const int SUFFIX = 1;
private false|string $basePath;
/**
@@ -56,8 +55,6 @@ final class Iterator extends FilterIterator
{
$current = $this->getInnerIterator()->current();
assert($current instanceof SplFileInfo);
$filename = $current->getFilename();
$realPath = $current->getRealPath();
+7
View File
@@ -2,6 +2,12 @@
All notable changes are documented in this file using the [Keep a CHANGELOG](https://keepachangelog.com/) principles.
## [6.0.0] - 2025-02-07
### Removed
* This component is no longer supported on PHP 8.2
## [5.0.1] - 2024-07-03
### Changed
@@ -59,6 +65,7 @@ All notable changes are documented in this file using the [Keep a CHANGELOG](htt
* This component is no longer supported on PHP 7.1 and PHP 7.2
[6.0.0]: https://github.com/sebastianbergmann/php-invoker/compare/5.0...6.0.0
[5.0.1]: https://github.com/sebastianbergmann/php-invoker/compare/5.0.1...5.0.1
[5.0.0]: https://github.com/sebastianbergmann/php-invoker/compare/4.0...5.0.0
[4.0.0]: https://github.com/sebastianbergmann/php-invoker/compare/3.1.1...4.0.0
+1 -1
View File
@@ -1,6 +1,6 @@
BSD 3-Clause License
Copyright (c) 2011-2024, Sebastian Bergmann
Copyright (c) 2011-2025, Sebastian Bergmann
All rights reserved.
Redistribution and use in source and binary forms, with or without
+1 -1
View File
@@ -1,6 +1,6 @@
# phpunit/php-invoker
[![Latest Stable Version](https://poser.pugx.org/phpunit/php-invoker/v/stable.png)](https://packagist.org/packages/phpunit/php-invoker)
[![Latest Stable Version](https://poser.pugx.org/phpunit/php-invoker/v)](https://packagist.org/packages/phpunit/php-invoker)
[![CI Status](https://github.com/sebastianbergmann/php-invoker/workflows/CI/badge.svg)](https://github.com/sebastianbergmann/php-invoker/actions)
[![codecov](https://codecov.io/gh/sebastianbergmann/php-invoker/branch/main/graph/badge.svg)](https://codecov.io/gh/sebastianbergmann/php-invoker)
+4 -4
View File
@@ -21,17 +21,17 @@
"prefer-stable": true,
"config": {
"platform": {
"php": "8.2.0"
"php": "8.3.0"
},
"optimize-autoloader": true,
"sort-packages": true
},
"require": {
"php": ">=8.2"
"php": ">=8.3"
},
"require-dev": {
"ext-pcntl": "*",
"phpunit/phpunit": "^11.0"
"phpunit/phpunit": "^12.0"
},
"autoload": {
"classmap": [
@@ -48,7 +48,7 @@
},
"extra": {
"branch-alias": {
"dev-main": "5.0-dev"
"dev-main": "6.0-dev"
}
}
}
@@ -9,6 +9,7 @@
*/
namespace SebastianBergmann\Invoker;
use const PHP_EOL;
use function extension_loaded;
use function function_exists;
use function implode;
+7
View File
@@ -2,6 +2,12 @@
All notable changes are documented in this file using the [Keep a CHANGELOG](http://keepachangelog.com/) principles.
## [5.0.0] - 2025-02-07
### Removed
* This component is no longer supported on PHP 8.2
## [4.0.1] - 2024-07-03
### Changed
@@ -61,6 +67,7 @@ All notable changes are documented in this file using the [Keep a CHANGELOG](htt
* Removed support for PHP 5.3, PHP 5.4, PHP 5.5, PHP 5.6, PHP 7.0, PHP 7.1, and PHP 7.2
[5.0.0]: https://github.com/sebastianbergmann/php-text-template/compare/4.0...5.0.0
[4.0.1]: https://github.com/sebastianbergmann/php-text-template/compare/4.0.0...4.0.1
[4.0.0]: https://github.com/sebastianbergmann/php-text-template/compare/3.0...4.0.0
[3.0.1]: https://github.com/sebastianbergmann/php-text-template/compare/3.0.0...3.0.1
+1 -1
View File
@@ -1,6 +1,6 @@
BSD 3-Clause License
Copyright (c) 2009-2024, Sebastian Bergmann
Copyright (c) 2009-2025, Sebastian Bergmann
All rights reserved.
Redistribution and use in source and binary forms, with or without
+1 -1
View File
@@ -1,4 +1,4 @@
[![Latest Stable Version](https://poser.pugx.org/phpunit/php-text-template/v/stable.png)](https://packagist.org/packages/phpunit/php-text-template)
[![Latest Stable Version](https://poser.pugx.org/phpunit/php-text-template/v)](https://packagist.org/packages/phpunit/php-text-template)
[![CI Status](https://github.com/sebastianbergmann/php-text-template/workflows/CI/badge.svg)](https://github.com/sebastianbergmann/php-text-template/actions)
[![codecov](https://codecov.io/gh/sebastianbergmann/php-text-template/branch/main/graph/badge.svg)](https://codecov.io/gh/sebastianbergmann/php-text-template)
+4 -4
View File
@@ -20,17 +20,17 @@
},
"config": {
"platform": {
"php": "8.2.0"
"php": "8.3.0"
},
"optimize-autoloader": true,
"sort-packages": true
},
"prefer-stable": true,
"require": {
"php": ">=8.2"
"php": ">=8.3"
},
"require-dev": {
"phpunit/phpunit": "^11.0"
"phpunit/phpunit": "^12.0"
},
"autoload": {
"classmap": [
@@ -39,7 +39,7 @@
},
"extra": {
"branch-alias": {
"dev-main": "4.0-dev"
"dev-main": "5.0-dev"
}
}
}
+7
View File
@@ -2,6 +2,12 @@
All notable changes are documented in this file using the [Keep a CHANGELOG](http://keepachangelog.com/) principles.
## [8.0.0] - 2025-02-07
### Removed
* This component is no longer supported on PHP 8.1
## [7.0.1] - 2024-07-03
### Changed
@@ -139,6 +145,7 @@ All notable changes are documented in this file using the [Keep a CHANGELOG](htt
* This component is no longer supported on PHP 5.3, PHP 5.4, PHP 5.5, PHP 5.6, and PHP 7.0
[8.0.0]: https://github.com/sebastianbergmann/php-timer/compare/7.0...8.0.0
[7.0.1]: https://github.com/sebastianbergmann/php-timer/compare/7.0.0...7.0.1
[7.0.0]: https://github.com/sebastianbergmann/php-timer/compare/6.0...7.0.0
[6.0.0]: https://github.com/sebastianbergmann/php-timer/compare/5.0.3...6.0.0
+1 -1
View File
@@ -1,6 +1,6 @@
BSD 3-Clause License
Copyright (c) 2010-2024, Sebastian Bergmann
Copyright (c) 2010-2025, Sebastian Bergmann
All rights reserved.
Redistribution and use in source and binary forms, with or without
+1 -1
View File
@@ -1,6 +1,6 @@
# phpunit/php-timer
[![Latest Stable Version](https://poser.pugx.org/phpunit/php-timer/v/stable.png)](https://packagist.org/packages/phpunit/php-timer)
[![Latest Stable Version](https://poser.pugx.org/phpunit/php-timer/v)](https://packagist.org/packages/phpunit/php-timer)
[![CI Status](https://github.com/sebastianbergmann/php-timer/workflows/CI/badge.svg)](https://github.com/sebastianbergmann/php-timer/actions)
[![codecov](https://codecov.io/gh/sebastianbergmann/php-timer/branch/main/graph/badge.svg)](https://codecov.io/gh/sebastianbergmann/php-timer)
+4 -4
View File
@@ -20,14 +20,14 @@
},
"prefer-stable": true,
"require": {
"php": ">=8.2"
"php": ">=8.3"
},
"require-dev": {
"phpunit/phpunit": "^11.0"
"phpunit/phpunit": "^12.0"
},
"config": {
"platform": {
"php": "8.2.0"
"php": "8.3.0"
},
"optimize-autoloader": true,
"sort-packages": true
@@ -39,7 +39,7 @@
},
"extra": {
"branch-alias": {
"dev-main": "7.0-dev"
"dev-main": "8.0-dev"
}
}
}
@@ -19,7 +19,7 @@ final class ResourceUsageFormatter
/**
* @var array<string,int>
*/
private const SIZES = [
private const array SIZES = [
'GB' => 1073741824,
'MB' => 1048576,
'KB' => 1024,
-414
View File
@@ -1,414 +0,0 @@
# Changes in PHPUnit 11.5
All notable changes of the PHPUnit 11.5 release series are documented in this file using the [Keep a CHANGELOG](https://keepachangelog.com/) principles.
## [11.5.43] - 2025-10-30
### Changed
* Skipped tests alone no longer lead to a yellow background for the test result summary
### Fixed
* [#6391](https://github.com/sebastianbergmann/phpunit/issues/6391): Errors during backup of global variables and static properties are not reported
## [11.5.42] - 2025-09-28
### Fixed
* [#6368](https://github.com/sebastianbergmann/phpunit/issues/6368): `failOnPhpunitWarning="false"` has no effect
## [11.5.41] - 2025-09-24
* No changes; `phpunit.phar` rebuilt with updated dependencies
## [11.5.40] - 2025-09-23
* No changes; `phpunit.phar` rebuilt with updated dependencies
## [11.5.39] - 2025-09-14
### Changed
* [#6366](https://github.com/sebastianbergmann/phpunit/issues/6366): Exclude `__sleep()` and `__wakeup()` from test double code generation on PHP >= 8.5
## [11.5.38] - 2025-09-11
### Changed
* [#6353](https://github.com/sebastianbergmann/phpunit/pull/6353): Disable performance optimization for tests requiring Xdebug
## [11.5.37] - 2025-09-11
### Changed
* Do not use `__sleep()` method (which will be deprecated in PHP 8.5)
## [11.5.36] - 2025-09-03
### Fixed
* [#6340](https://github.com/sebastianbergmann/phpunit/issues/6340): Implicitly enabled display of deprecation details is not disabled when it should be
## [11.5.35] - 2025-08-28
### Changed
* `#[IgnorePhpunitDeprecations]` is now considered for test runner deprecations (where applicable)
## [11.5.34] - 2025-08-20
### Changed
* Do not configure `report_memleaks` setting (which will be deprecated in PHP 8.5) for PHPT processes
## [11.5.33] - 2025-08-16
### Changed
* [#6321](https://github.com/sebastianbergmann/phpunit/issues/6321): Allow `error_reporting=E_ALL` for `--check-php-configuration`
### Fixed
* [#5863](https://github.com/sebastianbergmann/phpunit/issues/5863): TestDox printer does not show previous exception
* [#6102](https://github.com/sebastianbergmann/phpunit/issues/6102): `expectUserDeprecationMessage*()` fails when test is run in separate process
## [11.5.32] - 2025-08-12
### Changed
* [#6308](https://github.com/sebastianbergmann/phpunit/pull/6308): Improve output of `--check-php-configuration`
* The version number for the test result cache file has been incremented to reflect that its structure for PHPUnit 11.5 is not compatible with its structure for PHPUnit 8.5 and PHPUnit 9.6
### Fixed
* [#6281](https://github.com/sebastianbergmann/phpunit/issues/6281): Exceptions raised in after-test method are not reported for skipped tests
## [11.5.31] - 2025-08-11
### Fixed
* [#6304](https://github.com/sebastianbergmann/phpunit/issues/6304): PHPUnit 11.5.29 hangs when a test runner deprecation is triggered and process isolation is used (this reverts "`#[IgnorePhpunitDeprecations]` is now considered for test runner deprecations" from PHPUnit 11.5.29)
## [11.5.30] - 2025-08-10
### Changed
* [#6300](https://github.com/sebastianbergmann/phpunit/issues/6300): Emit warning when the name of a data provider method begins with `test`
* Do not use `SplObjectStorage` methods that will be deprecated in PHP 8.5
## [11.5.29] - 2025-08-09
### Added
* [#6297](https://github.com/sebastianbergmann/phpunit/issues/6297): `--check-php-configuration` CLI option for checking whether PHP is configured for testing
### Changed
* `#[IgnorePhpunitDeprecations]` is now considered for test runner deprecations (where applicable)
### Fixed
* [#6160](https://github.com/sebastianbergmann/phpunit/issues/6160): Baseline file in a subdirectory contains absolute paths
* Errors due to invalid data provided using `#[TestWith]` or `#[TestWithJson]` attributes are now properly reported
* The `DataProviderMethodFinished` event is now also emitted when the provided data set has an invalid key
## [11.5.28] - 2025-07-31
### Fixed
* [#6097](https://github.com/sebastianbergmann/phpunit/issues/6097): The `file` attribute of `<testClass>` node of XML test list can be wrong
## [11.5.27] - 2025-07-11
### Fixed
* [#6254](https://github.com/sebastianbergmann/phpunit/issues/6254): `defects,random`configuration is supported by implementation, but it is not allowed by the XML configuration file schema
* [#6259](https://github.com/sebastianbergmann/phpunit/issues/6259): Order of tests which use data from data providers is not affected by test sorting
* [#6266](https://github.com/sebastianbergmann/phpunit/issues/6266): Superfluous whitespace in TestDox output when test method name has a number after the `test` prefix
## [11.5.26] - 2025-07-04
### Fixed
* [#6104](https://github.com/sebastianbergmann/phpunit/issues/6104): Test with dependencies and data provider fails
* [#6163](https://github.com/sebastianbergmann/phpunit/issues/6163): `@no-named-arguments` leads to static analysis errors for variadic arguments
## [11.5.25] - 2025-06-27
### Fixed
* [#6249](https://github.com/sebastianbergmann/phpunit/issues/6249): No meaningful error when `<testsuite>` element is missing required `name` attribute
## [11.5.24] - 2025-06-20
### Added
* [#6236](https://github.com/sebastianbergmann/phpunit/issues/6236): `failOnPhpunitWarning` attribute on the `<phpunit>` element of the XML configuration file and `--fail-on-phpunit-warning` CLI option for controlling whether PHPUnit should fail on PHPUnit warnings (default: `true`)
* [#6239](https://github.com/sebastianbergmann/phpunit/issues/6239): `--do-not-fail-on-deprecation`, `--do-not-fail-on-phpunit-warning`, `--do-not-fail-on-phpunit-deprecation`, `--do-not-fail-on-empty-test-suite`, `--do-not-fail-on-incomplete`, `--do-not-fail-on-notice`, `--do-not-fail-on-risky`, `--do-not-fail-on-skipped`, and `--do-not-fail-on-warning` CLI options
* `--do-not-report-useless-tests` CLI option as a replacement for `--dont-report-useless-tests`
### Deprecated
* `--dont-report-useless-tests` CLI option (use `--do-not-report-useless-tests` instead)
### Fixed
* [#6243](https://github.com/sebastianbergmann/phpunit/issues/6243): Constraints cannot be implemented without using internal class `ExpectationFailedException`
## [11.5.23] - 2025-06-13
### Fixed
* [#6222](https://github.com/sebastianbergmann/phpunit/issues/6222): Data Provider seems to mess up Test Dependencies
* `shortenArraysForExportThreshold` XML configuration setting has no effect on all arrays exported for event-related value objects
## [11.5.22] - 2025-06-06
### Changed
* Do not treat warnings differently than other issues in summary section of default output
## [11.5.21] - 2025-05-21
### Changed
* [#6210](https://github.com/sebastianbergmann/phpunit/pull/6210): Set default Clover coverage project name
* [#6217](https://github.com/sebastianbergmann/phpunit/pull/6217): Improve the error message when `createStubForIntersectionOfInterfaces()` is called with a class
## [11.5.20] - 2025-05-11
### Fixed
* [#6192](https://github.com/sebastianbergmann/phpunit/issues/6192): Reverted change made in PHPUnit 11.5.19 due to regression
* [#6199](https://github.com/sebastianbergmann/phpunit/issues/6199): `assertEmpty()` and `assertNotEmpty()` use overly restrictive `phpstan-assert empty` directives
## [11.5.19] - 2025-05-02
### Added
* `displayDetailsOnAllIssues` attribute on the `<phpunit>` element of the XML configuration file and `--display-all-issues` CLI option for controlling whether PHPUnit should display details on all issues that are triggered (default: `false`)
* `failOnAllIssues` attribute on the `<phpunit>` element of the XML configuration file and `--fail-on-all-issues` CLI option for controlling whether PHPUnit should fail on all issues that are triggered (default: `false`)
### Changed
* [#5956](https://github.com/sebastianbergmann/phpunit/issues/5956): Improved handling of deprecated `E_STRICT` constant
### Fixed
* [#6192](https://github.com/sebastianbergmann/phpunit/issues/6192): Positive `%a` and `%A` matches are not ignored from diff when `EXPECTF` fails
## [11.5.18] - 2025-04-22
### Changed
* When gathering the telemetry information that each event has, the real size of memory allocated from the operating system is no longer used as this is grown by PHP's memory manager in chunks that are so large that small(er) increases in peak memory usage cannot be seen
* The peak memory usage returned by `memory_get_peak_usage()` is now reset immediately before the `Test\Prepared` event is emitted using `memory_reset_peak_usage()` so that (memory usage at `Test\Finished` - memory usage at `Test\Prepared`) is a better approximation of the memory usage of the test
* The string representation of `Telemetry\Info` now uses peak memory usage instead of memory usage (this affects `--log-events-verbose-text`)
### Fixed
* A "Before Test Method Errored" event is no longer emitted when a test is skipped in a "before test" method
## [11.5.17] - 2025-04-08
### Fixed
* [#6104](https://github.com/sebastianbergmann/phpunit/issues/6104): Reverted change introduced in PHPUnit 11.5.16
## [11.5.16] - 2025-04-08
### Fixed
* [#6104](https://github.com/sebastianbergmann/phpunit/issues/6104): Test with dependencies and data provider fails
* [#6174](https://github.com/sebastianbergmann/phpunit/issues/6174): `willReturnMap()` fails with nullable parameters when their default is `null` and no argument is passed for them
## [11.5.15] - 2025-03-23
### Changed
* [#6150](https://github.com/sebastianbergmann/phpunit/issues/6150): Reverted change introduced in PHPUnit 11.5.13
## [11.5.14] - 2025-03-19
### Changed
* Updated dependencies for PHAR distribution
## [11.5.13] - 2025-03-18
### Changed
* [#6150](https://github.com/sebastianbergmann/phpunit/issues/6150): Trigger warning when code coverage analysis is performed and no cache directory is configured
## [11.5.12] - 2025-03-07
### Fixed
* [#5976](https://github.com/sebastianbergmann/phpunit/issues/5976): TestDox result printer does not display details about errors triggered in before-first-test and after-last-test methods
## [11.5.11] - 2025-03-05
### Fixed
* [#6142](https://github.com/sebastianbergmann/phpunit/issues/6142): `$expected` and `$actual` are mixed up in failure description when `assertJsonFileEqualsJsonFile()` fails
## [11.5.10] - 2025-02-25
### Fixed
* [#6138](https://github.com/sebastianbergmann/phpunit/issues/6138): Test with failed expectation on value passed to mocked method is incorrectly considered risky
## [11.5.9] - 2025-02-21
### Fixed
* [#6134](https://github.com/sebastianbergmann/phpunit/issues/6134): Missing event when child process ends unexpectedly
## [11.5.8] - 2025-02-18
### Fixed
* A `Test\PreparationFailed` event is now emitted in addition to a `Test\Errored` event when an unexpected exception is triggered in a before-test method
* A `Test\Passed` event is no longer emitted in addition to a `Test\Failed` or `Test\Errored` event when an assertion failure or an unexpected exception is triggered in an after-test method
* A `TestSuite\Finished` event is now emitted when a before-first-test method errors
## [11.5.7] - 2025-02-06
### Changed
* [#5951](https://github.com/sebastianbergmann/phpunit/issues/5951): The `includeUncoveredFiles` configuration option is no longer deprecated
* [#6117](https://github.com/sebastianbergmann/phpunit/issues/6117): Include source location information for issues triggered during test in `--debug` output
* [#6119](https://github.com/sebastianbergmann/phpunit/issues/6119): Improve message for errors that occur while parsing attributes
* [#6120](https://github.com/sebastianbergmann/phpunit/issues/6120): Allow negative priorities for hook methods
## [11.5.6] - 2025-01-31
### Changed
* [#6112](https://github.com/sebastianbergmann/phpunit/pull/6112): Improve performance of `SourceMapper`
### Fixed
* [#6115](https://github.com/sebastianbergmann/phpunit/issues/6115): Backed enumerations with values not of type `string` cannot be used in customized TestDox output
## [11.5.5] - 2025-01-29
### Changed
* Do not skip execution of test that depends on a test that is larger than itself
## [11.5.4] - 2025-01-28
### Changed
* [#5958](https://github.com/sebastianbergmann/phpunit/issues/5958): Support for `#[CoversTrait]` and `#[UsesTrait]` attributes is no longer deprecated
* [#5960](https://github.com/sebastianbergmann/phpunit/issues/5960): Support for targeting trait methods with the `#[CoversMethod]` and `#[UsesMethod]` attributes is no longer deprecated
### Fixed
* [#6103](https://github.com/sebastianbergmann/phpunit/issues/6103): Output from test run in separate process is printed twice
* [#6109](https://github.com/sebastianbergmann/phpunit/issues/6109): Skipping a test in a before-class method crashes JUnit XML logger
* [#6111](https://github.com/sebastianbergmann/phpunit/issues/6111): Deprecations cause `SourceMapper` to scan all `<source/>` files
## [11.5.3] - 2025-01-13
### Added
* `Test\AfterLastTestMethodErrored`, `Test\AfterTestMethodErrored`, `Test\BeforeTestMethodErrored`, `Test\PostConditionErrored`, and `Test\PreConditionErrored` events
### Fixed
* [#6093](https://github.com/sebastianbergmann/phpunit/issues/6093): Test Double Code Generator does not work when PHPUnit is used from PHAR on PHP 8.4
* [#6094](https://github.com/sebastianbergmann/phpunit/issues/6094): Errors in after-last-test methods are not reported
* [#6095](https://github.com/sebastianbergmann/phpunit/issues/6095): Expectation is not counted correctly when a doubled method is called more often than is expected
* [#6096](https://github.com/sebastianbergmann/phpunit/issues/6096): `--list-tests-xml` is broken when a group with a numeric name is defined
* [#6098](https://github.com/sebastianbergmann/phpunit/issues/6098): No `system-out` element in JUnit XML logfile
* [#6100](https://github.com/sebastianbergmann/phpunit/issues/6100): Suppressed deprecations incorrectly stop test execution when execution should be stopped on deprecation
## [11.5.2] - 2024-12-21
### Fixed
* [#6082](https://github.com/sebastianbergmann/phpunit/issues/6082): `assertArrayHasKey()`, `assertArrayNotHasKey()`, `arrayHasKey()`, and `ArrayHasKey::__construct()` do not support all possible key types
* [#6087](https://github.com/sebastianbergmann/phpunit/issues/6087): `--migrate-configuration` does not remove `beStrictAboutTodoAnnotatedTests` attribute from XML configuration file
## [11.5.1] - 2024-12-11
### Added
* [#6081](https://github.com/sebastianbergmann/phpunit/pull/6081): `DefaultResultCache::mergeWith()` for merging result cache instances
### Fixed
* [#6066](https://github.com/sebastianbergmann/phpunit/pull/6066): TeamCity logger does not handle error/skipped events in before-class methods correctly
## [11.5.0] - 2024-12-06
### Added
* [#5948](https://github.com/sebastianbergmann/phpunit/pull/5948): Support for Property Hooks in Test Doubles
* [#5954](https://github.com/sebastianbergmann/phpunit/issues/5954): Provide a way to stop execution at a particular deprecation
* Method `assertContainsNotOnlyInstancesOf()` in the `PHPUnit\Framework\Assert` class as the inverse of the `assertContainsOnlyInstancesOf()` method
* Methods `assertContainsOnlyArray()`, `assertContainsOnlyBool()`, `assertContainsOnlyCallable()`, `assertContainsOnlyFloat()`, `assertContainsOnlyInt()`, `assertContainsOnlyIterable()`, `assertContainsOnlyNull()`, `assertContainsOnlyNumeric()`, `assertContainsOnlyObject()`, `assertContainsOnlyResource()`, `assertContainsOnlyClosedResource()`, `assertContainsOnlyScalar()`, and `assertContainsOnlyString()` in the `PHPUnit\Framework\Assert` class as specialized alternatives for the generic `assertContainsOnly()` method
* Methods `assertContainsNotOnlyArray()`, `assertContainsNotOnlyBool()`, `assertContainsNotOnlyCallable()`, `assertContainsNotOnlyFloat()`, `assertContainsNotOnlyInt()`, `assertContainsNotOnlyIterable()`, `assertContainsNotOnlyNull()`, `assertContainsNotOnlyNumeric()`, `assertContainsNotOnlyObject()`, `assertContainsNotOnlyResource()`, `assertContainsNotOnlyClosedResource()`, `assertContainsNotOnlyScalar()`, and `assertContainsNotOnlyString()` in the `PHPUnit\Framework\Assert` class as specialized alternatives for the generic `assertNotContainsOnly()` method
* Methods `containsOnlyArray()`, `containsOnlyBool()`, `containsOnlyCallable()`, `containsOnlyFloat()`, `containsOnlyInt()`, `containsOnlyIterable()`, `containsOnlyNull()`, `containsOnlyNumeric()`, `containsOnlyObject()`, `containsOnlyResource()`, `containsOnlyClosedResource()`, `containsOnlyScalar()`, and `containsOnlyString()` in the `PHPUnit\Framework\Assert` class as specialized alternatives for the generic `containsOnly()` method
* Methods `isArray()`, `isBool()`, `isCallable()`, `isFloat()`, `isInt()`, `isIterable()`, `isNumeric()`, `isObject()`, `isResource()`, `isClosedResource()`, `isScalar()`, `isString()` in the `PHPUnit\Framework\Assert` class as specialized alternatives for the generic `isType()` method
* `TestRunner\ChildProcessStarted` and `TestRunner\ChildProcessFinished` events
### Changed
* [#5998](https://github.com/sebastianbergmann/phpunit/pull/5998): Do not run `SKIPIF` section of PHPT test in separate process when it is free of side effects
* [#5999](https://github.com/sebastianbergmann/phpunit/pull/5999): Do not run `CLEAN` section of PHPT test in separate process when it is free of side effects that modify the parent process
### Deprecated
* [#6052](https://github.com/sebastianbergmann/phpunit/issues/6052): `isType()` (use `isArray()`, `isBool()`, `isCallable()`, `isFloat()`, `isInt()`, `isIterable()`, `isNull()`, `isNumeric()`, `isObject()`, `isResource()`, `isClosedResource()`, `isScalar()`, or `isString()` instead)
* [#6055](https://github.com/sebastianbergmann/phpunit/issues/6055): `assertContainsOnly()` (use `assertContainsOnlyArray()`, `assertContainsOnlyBool()`, `assertContainsOnlyCallable()`, `assertContainsOnlyFloat()`, `assertContainsOnlyInt()`, `assertContainsOnlyIterable()`, `assertContainsOnlyNumeric()`, `assertContainsOnlyObject()`, `assertContainsOnlyResource()`, `assertContainsOnlyClosedResource()`, `assertContainsOnlyScalar()`, or `assertContainsOnlyString()` instead)
* [#6055](https://github.com/sebastianbergmann/phpunit/issues/6055): `assertNotContainsOnly()` (use `assertContainsNotOnlyArray()`, `assertContainsNotOnlyBool()`, `assertContainsNotOnlyCallable()`, `assertContainsNotOnlyFloat()`, `assertContainsNotOnlyInt()`, `assertContainsNotOnlyIterable()`, `assertContainsNotOnlyNumeric()`, `assertContainsNotOnlyObject()`, `assertContainsNotOnlyResource()`, `assertContainsNotOnlyClosedResource()`, `assertContainsNotOnlyScalar()`, or `assertContainsNotOnlyString()` instead)
* [#6059](https://github.com/sebastianbergmann/phpunit/issues/6059): `containsOnly()` (use `containsOnlyArray()`, `containsOnlyBool()`, `containsOnlyCallable()`, `containsOnlyFloat()`, `containsOnlyInt()`, `containsOnlyIterable()`, `containsOnlyNumeric()`, `containsOnlyObject()`, `containsOnlyResource()`, `containsOnlyClosedResource()`, `containsOnlyScalar()`, or `containsOnlyString()` instead)
[11.5.43]: https://github.com/sebastianbergmann/phpunit/compare/11.5.42...11.5.43
[11.5.42]: https://github.com/sebastianbergmann/phpunit/compare/11.5.41...11.5.42
[11.5.41]: https://github.com/sebastianbergmann/phpunit/compare/11.5.40...11.5.41
[11.5.40]: https://github.com/sebastianbergmann/phpunit/compare/11.5.39...11.5.40
[11.5.39]: https://github.com/sebastianbergmann/phpunit/compare/11.5.38...11.5.39
[11.5.38]: https://github.com/sebastianbergmann/phpunit/compare/11.5.37...11.5.38
[11.5.37]: https://github.com/sebastianbergmann/phpunit/compare/11.5.36...11.5.37
[11.5.36]: https://github.com/sebastianbergmann/phpunit/compare/11.5.35...11.5.36
[11.5.35]: https://github.com/sebastianbergmann/phpunit/compare/11.5.34...11.5.35
[11.5.34]: https://github.com/sebastianbergmann/phpunit/compare/11.5.33...11.5.34
[11.5.33]: https://github.com/sebastianbergmann/phpunit/compare/11.5.32...11.5.33
[11.5.32]: https://github.com/sebastianbergmann/phpunit/compare/11.5.31...11.5.32
[11.5.31]: https://github.com/sebastianbergmann/phpunit/compare/11.5.30...11.5.31
[11.5.30]: https://github.com/sebastianbergmann/phpunit/compare/11.5.29...11.5.30
[11.5.29]: https://github.com/sebastianbergmann/phpunit/compare/11.5.28...11.5.29
[11.5.28]: https://github.com/sebastianbergmann/phpunit/compare/11.5.27...11.5.28
[11.5.27]: https://github.com/sebastianbergmann/phpunit/compare/11.5.26...11.5.27
[11.5.26]: https://github.com/sebastianbergmann/phpunit/compare/11.5.25...11.5.26
[11.5.25]: https://github.com/sebastianbergmann/phpunit/compare/11.5.24...11.5.25
[11.5.24]: https://github.com/sebastianbergmann/phpunit/compare/11.5.23...11.5.24
[11.5.23]: https://github.com/sebastianbergmann/phpunit/compare/11.5.22...11.5.23
[11.5.22]: https://github.com/sebastianbergmann/phpunit/compare/11.5.21...11.5.22
[11.5.21]: https://github.com/sebastianbergmann/phpunit/compare/11.5.20...11.5.21
[11.5.20]: https://github.com/sebastianbergmann/phpunit/compare/11.5.19...11.5.20
[11.5.19]: https://github.com/sebastianbergmann/phpunit/compare/11.5.18...11.5.19
[11.5.18]: https://github.com/sebastianbergmann/phpunit/compare/11.5.17...11.5.18
[11.5.17]: https://github.com/sebastianbergmann/phpunit/compare/11.5.16...11.5.17
[11.5.16]: https://github.com/sebastianbergmann/phpunit/compare/11.5.15...11.5.16
[11.5.15]: https://github.com/sebastianbergmann/phpunit/compare/11.5.14...11.5.15
[11.5.14]: https://github.com/sebastianbergmann/phpunit/compare/11.5.13...11.5.14
[11.5.13]: https://github.com/sebastianbergmann/phpunit/compare/11.5.12...11.5.13
[11.5.12]: https://github.com/sebastianbergmann/phpunit/compare/11.5.11...11.5.12
[11.5.11]: https://github.com/sebastianbergmann/phpunit/compare/11.5.10...11.5.11
[11.5.10]: https://github.com/sebastianbergmann/phpunit/compare/11.5.9...11.5.10
[11.5.9]: https://github.com/sebastianbergmann/phpunit/compare/11.5.8...11.5.9
[11.5.8]: https://github.com/sebastianbergmann/phpunit/compare/11.5.7...11.5.8
[11.5.7]: https://github.com/sebastianbergmann/phpunit/compare/11.5.6...11.5.7
[11.5.6]: https://github.com/sebastianbergmann/phpunit/compare/11.5.5...11.5.6
[11.5.5]: https://github.com/sebastianbergmann/phpunit/compare/11.5.4...11.5.5
[11.5.4]: https://github.com/sebastianbergmann/phpunit/compare/11.5.3...11.5.4
[11.5.3]: https://github.com/sebastianbergmann/phpunit/compare/11.5.2...11.5.3
[11.5.2]: https://github.com/sebastianbergmann/phpunit/compare/11.5.1...11.5.2
[11.5.1]: https://github.com/sebastianbergmann/phpunit/compare/11.5.0...11.5.1
[11.5.0]: https://github.com/sebastianbergmann/phpunit/compare/11.4.4...11.5.0
+38 -60
View File
@@ -1,5 +1,28 @@
# Deprecations
## Soft Deprecations
This functionality is currently [soft-deprecated](https://phpunit.de/backward-compatibility.html#soft-deprecation):
### Extending PHPUnit
| Issue | Description | Since | Replacement |
|-------------------------------------------------------------------|---------------------------------------------|--------|-------------------------------------------------|
| [#6140](https://github.com/sebastianbergmann/phpunit/issues/6140) | `AfterTestMethodCalled::testClassName()` | 12.1.0 | `AfterTestMethodCalled::test()->className()` |
| [#6140](https://github.com/sebastianbergmann/phpunit/issues/6140) | `AfterTestMethodErrored::testClassName()` | 12.1.0 | `AfterTestMethodErrored::test()->className()` |
| [#6140](https://github.com/sebastianbergmann/phpunit/issues/6140) | `AfterTestMethodFinished::testClassName()` | 12.1.0 | `AfterTestMethodFinished::test()->className()` |
| [#6140](https://github.com/sebastianbergmann/phpunit/issues/6140) | `BeforeTestMethodCalled::testClassName()` | 12.1.0 | `BeforeTestMethodCalled::test()->className()` |
| [#6140](https://github.com/sebastianbergmann/phpunit/issues/6140) | `BeforeTestMethodErrored::testClassName()` | 12.1.0 | `BeforeTestMethodErrored::test()->className()` |
| [#6140](https://github.com/sebastianbergmann/phpunit/issues/6140) | `BeforeTestMethodFinished::testClassName()` | 12.1.0 | `BeforeTestMethodFinished::test()->className()` |
| [#6140](https://github.com/sebastianbergmann/phpunit/issues/6140) | `PreConditionCalled::testClassName()` | 12.1.0 | `PreConditionCalled::test()->className()` |
| [#6140](https://github.com/sebastianbergmann/phpunit/issues/6140) | `PreConditionErrored::testClassName()` | 12.1.0 | `PreConditionErrored::test()->className()` |
| [#6140](https://github.com/sebastianbergmann/phpunit/issues/6140) | `PreConditionFinished::testClassName()` | 12.1.0 | `PreConditionFinished::test()->className()` |
| [#6140](https://github.com/sebastianbergmann/phpunit/issues/6140) | `PostConditionCalled::testClassName()` | 12.1.0 | `PostConditionCalled::test()->className()` |
| [#6140](https://github.com/sebastianbergmann/phpunit/issues/6140) | `PostConditionErrored::testClassName()` | 12.1.0 | `PostConditionErrored::test()->className()` |
| [#6140](https://github.com/sebastianbergmann/phpunit/issues/6140) | `PostConditionFinished::testClassName()` | 12.1.0 | `PostConditionFinished::test()->className()` |
| [#6229](https://github.com/sebastianbergmann/phpunit/issues/6229) | `Configuration::includeTestSuite()` | 12.3.0 | `Configuration::includeTestSuites()` |
| [#6229](https://github.com/sebastianbergmann/phpunit/issues/6229) | `Configuration::excludeTestSuite()` | 12.3.0 | `Configuration::excludeTestSuites()` |
## Hard Deprecations
This functionality is currently [hard-deprecated](https://phpunit.de/backward-compatibility.html#hard-deprecation):
@@ -8,68 +31,23 @@ This functionality is currently [hard-deprecated](https://phpunit.de/backward-co
#### Assertions, Constraints, and Expectations
| Issue | Description | Since | Replacement |
|-------------------------------------------------------------------|----------------------------------------------|--------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [#5472](https://github.com/sebastianbergmann/phpunit/issues/5472) | `Assert::assertStringNotMatchesFormat()` | 10.4.0 | |
| [#5472](https://github.com/sebastianbergmann/phpunit/issues/5472) | `Assert::assertStringNotMatchesFormatFile()` | 10.4.0 | |
| Issue | Description | Since | Replacement |
|-------------------------------------------------------------------|------------------------------------------------------|--------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [#6052](https://github.com/sebastianbergmann/phpunit/issues/6052) | `Assert::isType()` | 11.5.0 | Use `isArray()`, `isBool()`, `isCallable()`, `isFloat()`, `isInt()`, `isIterable()`, `isNull()`, `isNumeric()`, `isObject()`, `isResource()`, `isClosedResource()`, `isScalar()`, or `isString()` instead |
| [#6055](https://github.com/sebastianbergmann/phpunit/issues/6055) | `Assert::assertContainsOnly()` | 11.5.0 | Use `assertContainsOnlyArray()`, `assertContainsOnlyBool()`, `assertContainsOnlyCallable()`, `assertContainsOnlyFloat()`, `assertContainsOnlyInt()`, `assertContainsOnlyIterable()`, `assertContainsOnlyNumeric()`, `assertContainsOnlyObject()`, `assertContainsOnlyResource()`, `assertContainsOnlyClosedResource()`, `assertContainsOnlyScalar()`, or `assertContainsOnlyString()` instead |
| [#6055](https://github.com/sebastianbergmann/phpunit/issues/6055) | `Assert::assertNotContainsOnly()` | 11.5.0 | Use `assertContainsNotOnlyArray()`, `assertContainsNotOnlyBool()`, `assertContainsNotOnlyCallable()`, `assertContainsNotOnlyFloat()`, `assertContainsNotOnlyInt()`, `assertContainsNotOnlyIterable()`, `assertContainsNotOnlyNumeric()`, `assertContainsNotOnlyObject()`, `assertContainsNotOnlyResource()`, `assertContainsNotOnlyClosedResource()`, `assertContainsNotOnlyScalar()`, or `assertContainsNotOnlyString()` instead |
| [#6059](https://github.com/sebastianbergmann/phpunit/issues/6059) | `Assert::containsOnly()` | 11.5.0 | Use `containsOnlyArray()`, `containsOnlyBool()`, `containsOnlyCallable()`, `containsOnlyFloat()`, `containsOnlyInt()`, `containsOnlyIterable()`, `containsOnlyNumeric()`, `containsOnlyObject()`, `containsOnlyResource()`, `containsOnlyClosedResource()`, `containsOnlyScalar()`, or `containsOnlyString()` instead |
#### Test Double API
### Attributes
| Issue | Description | Since | Replacement |
|-------------------------------------------------------------------|--------------------------------------------------------------------------------|--------|-----------------------------------------------------------------------------------------|
| [#5240](https://github.com/sebastianbergmann/phpunit/issues/5240) | `TestCase::createTestProxy()` | 10.1.0 | |
| [#5241](https://github.com/sebastianbergmann/phpunit/issues/5241) | `TestCase::getMockForAbstractClass()` | 10.1.0 | |
| [#5242](https://github.com/sebastianbergmann/phpunit/issues/5242) | `TestCase::getMockFromWsdl()` | 10.1.0 | |
| [#5243](https://github.com/sebastianbergmann/phpunit/issues/5243) | `TestCase::getMockForTrait()` | 10.1.0 | |
| [#5244](https://github.com/sebastianbergmann/phpunit/issues/5244) | `TestCase::getObjectForTrait()` | 10.1.0 | |
| [#5305](https://github.com/sebastianbergmann/phpunit/issues/5305) | `MockBuilder::getMockForAbstractClass()` | 10.1.0 | |
| [#5306](https://github.com/sebastianbergmann/phpunit/issues/5306) | `MockBuilder::getMockForTrait()` | 10.1.0 | |
| [#5307](https://github.com/sebastianbergmann/phpunit/issues/5307) | `MockBuilder::disableProxyingToOriginalMethods()` | 10.1.0 | |
| [#5307](https://github.com/sebastianbergmann/phpunit/issues/5307) | `MockBuilder::enableProxyingToOriginalMethods()` | 10.1.0 | |
| [#5307](https://github.com/sebastianbergmann/phpunit/issues/5307) | `MockBuilder::setProxyTarget()` | 10.1.0 | |
| [#5308](https://github.com/sebastianbergmann/phpunit/issues/5308) | `MockBuilder::allowMockingUnknownTypes()` | 10.1.0 | |
| [#5308](https://github.com/sebastianbergmann/phpunit/issues/5308) | `MockBuilder::disallowMockingUnknownTypes()` | 10.1.0 | |
| [#5309](https://github.com/sebastianbergmann/phpunit/issues/5309) | `MockBuilder::disableAutoload()` | 10.1.0 | |
| [#5309](https://github.com/sebastianbergmann/phpunit/issues/5309) | `MockBuilder::enableAutoload()` | 10.1.0 | |
| [#5315](https://github.com/sebastianbergmann/phpunit/issues/5315) | `MockBuilder::disableArgumentCloning()` | 10.1.0 | |
| [#5315](https://github.com/sebastianbergmann/phpunit/issues/5315) | `MockBuilder::enableArgumentCloning()` | 10.1.0 | |
| [#5320](https://github.com/sebastianbergmann/phpunit/issues/5320) | `MockBuilder::addMethods()` | 10.1.0 | |
| [#5415](https://github.com/sebastianbergmann/phpunit/issues/5415) | Support for doubling interfaces (or classes) that have a method named `method` | 11.0.0 | |
| [#5423](https://github.com/sebastianbergmann/phpunit/issues/5423) | `TestCase::onConsecutiveCalls()` | 10.3.0 | Use `$double->willReturn()` instead of `$double->will($this->onConsecutiveCalls())` |
| [#5423](https://github.com/sebastianbergmann/phpunit/issues/5423) | `TestCase::returnArgument()` | 10.3.0 | Use `$double->willReturnArgument()` instead of `$double->will($this->returnArgument())` |
| [#5423](https://github.com/sebastianbergmann/phpunit/issues/5423) | `TestCase::returnCallback()` | 10.3.0 | Use `$double->willReturnCallback()` instead of `$double->will($this->returnCallback())` |
| [#5423](https://github.com/sebastianbergmann/phpunit/issues/5423) | `TestCase::returnSelf()` | 10.3.0 | Use `$double->willReturnSelf()` instead of `$double->will($this->returnSelf())` |
| [#5423](https://github.com/sebastianbergmann/phpunit/issues/5423) | `TestCase::returnValue()` | 10.3.0 | Use `$double->willReturn()` instead of `$double->will($this->returnValue())` |
| [#5423](https://github.com/sebastianbergmann/phpunit/issues/5423) | `TestCase::returnValueMap()` | 10.3.0 | Use `$double->willReturnMap()` instead of `$double->will($this->returnValueMap())` |
| [#5535](https://github.com/sebastianbergmann/phpunit/issues/5525) | Configuring expectations using `expects()` on test stubs | 11.0.0 | Create a mock object when you need to configure expectations on a test double |
| Issue | Description | Since | Replacement |
|-------------------------------------------------------------------|---------------------------------------------------------------------------------------------|--------|----------------------------------------------|
| [#6246](https://github.com/sebastianbergmann/phpunit/issues/6246) | Using `#[CoversNothing]` on a test method | 12.3.0 | |
| [#6284](https://github.com/sebastianbergmann/phpunit/issues/6284) | Using `#[RunClassInSeparateProcess]` on a test class | 12.4.0 | Use `#[RunTestsInSeparateProcesses]` instead |
| [#6355](https://github.com/sebastianbergmann/phpunit/issues/6355) | Support for version constraint string argument without explicit version comparison operator | 12.4.0 | |
### Running Tests
| Issue | Description | Since | Replacement |
|-------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------|--------|----------------------------------------------------------------------------------------------------|
| [#5689](https://github.com/sebastianbergmann/phpunit/issues/5689) | `restrictDeprecations` attribute on the `<source>` element of the XML configuration file | 11.1.0 | Use `ignoreSelfDeprecations`, `ignoreDirectDeprecations`, and `ignoreIndirectDeprecations` instead |
| [#5709](https://github.com/sebastianbergmann/phpunit/issues/5709) | Support for using comma-separated values with the `--group`, `--exclude-group`, `--covers`, `--uses`, and `--test-suffix` CLI options | 11.1.0 | Use `--group foo --group bar` instead of `--group foo,bar`, for example |
#### Miscellaneous
| Issue | Description | Since | Replacement |
|-------------------------------------------------------------------|-------------------------------------------------------------|--------|-----------------------------------------------------------------------------------------|
| [#4505](https://github.com/sebastianbergmann/phpunit/issues/4505) | Metadata in doc-comments | 10.3.0 | Metadata in attributes |
| [#5214](https://github.com/sebastianbergmann/phpunit/issues/5214) | `TestCase::iniSet()` | 10.3.0 | |
| [#5216](https://github.com/sebastianbergmann/phpunit/issues/5216) | `TestCase::setLocale()` | 10.3.0 | |
| [#5800](https://github.com/sebastianbergmann/phpunit/issues/5800) | Targeting traits with `#[CoversClass]` and `#[UsesClass]` | 11.2.0 | `#[CoversClass]` and `#[UsesClass]` also target the traits used by the targeted classes |
## Soft Deprecations
This functionality is currently [soft-deprecated](https://phpunit.de/backward-compatibility.html#soft-deprecation):
### Writing Tests
#### Assertions, Constraints, and Expectations
| Issue | Description | Since | Replacement |
|-------------------------------------------------------------------|-----------------------------------|--------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [#6052](https://github.com/sebastianbergmann/phpunit/issues/6052) | `Assert::isType()` | 11.5.0 | Use `isArray()`, `isBool()`, `isCallable()`, `isFloat()`, `isInt()`, `isIterable()`, `isNull()`, `isNumeric()`, `isObject()`, `isResource()`, `isClosedResource()`, `isScalar()`, or `isString()` instead |
| [#6055](https://github.com/sebastianbergmann/phpunit/issues/6055) | `Assert::assertContainsOnly()` | 11.5.0 | Use `assertContainsOnlyArray()`, `assertContainsOnlyBool()`, `assertContainsOnlyCallable()`, `assertContainsOnlyFloat()`, `assertContainsOnlyInt()`, `assertContainsOnlyIterable()`, `assertContainsOnlyNumeric()`, `assertContainsOnlyObject()`, `assertContainsOnlyResource()`, `assertContainsOnlyClosedResource()`, `assertContainsOnlyScalar()`, or `assertContainsOnlyString()` instead |
| [#6055](https://github.com/sebastianbergmann/phpunit/issues/6055) | `Assert::assertNotContainsOnly()` | 11.5.0 | Use `assertContainsNotOnlyArray()`, `assertContainsNotOnlyBool()`, `assertContainsNotOnlyCallable()`, `assertContainsNotOnlyFloat()`, `assertContainsNotOnlyInt()`, `assertContainsNotOnlyIterable()`, `assertContainsNotOnlyNumeric()`, `assertContainsNotOnlyObject()`, `assertContainsNotOnlyResource()`, `assertContainsNotOnlyClosedResource()`, `assertContainsNotOnlyScalar()`, or `assertContainsNotOnlyString()` instead |
| [#6059](https://github.com/sebastianbergmann/phpunit/issues/6059) | `Assert::containsOnly()` | 11.5.0 | Use `containsOnlyArray()`, `containsOnlyBool()`, `containsOnlyCallable()`, `containsOnlyFloat()`, `containsOnlyInt()`, `containsOnlyIterable()`, `containsOnlyNumeric()`, `containsOnlyObject()`, `containsOnlyResource()`, `containsOnlyClosedResource()`, `containsOnlyScalar()`, or `containsOnlyString()` instead |
| Issue | Description | Since | Replacement |
|-------------------------------------------------------------------|------------------------------------------|--------|--------------------------------------------------------|
| [#6240](https://github.com/sebastianbergmann/phpunit/issues/6240) | `--dont-report-useless-tests` CLI option | 12.2.3 | Use `--do-not-report-useless-tests` CLI option instead |
+87 -12
View File
@@ -1,14 +1,20 @@
[![PHPUnit](.github/img/phpunit.svg)](https://phpunit.de/?ref=github)
[![CI Status](https://github.com/sebastianbergmann/phpunit/workflows/CI/badge.svg)](https://github.com/sebastianbergmann/phpunit/actions)
[![codecov](https://codecov.io/gh/sebastianbergmann/phpunit/branch/main/graph/badge.svg?token=0yzBUK8Wri)](https://codecov.io/gh/sebastianbergmann/phpunit)
[![Latest Stable Version](https://poser.pugx.org/phpunit/phpunit/v)](https://packagist.org/packages/phpunit/phpunit)
[![Total Downloads](https://poser.pugx.org/phpunit/phpunit/downloads)](https://packagist.org/packages/phpunit/phpunit/stats)
[![Monthly Downloads](https://poser.pugx.org/phpunit/phpunit/d/monthly)](https://packagist.org/packages/phpunit/phpunit/stats)
[![Daily Downloads](https://poser.pugx.org/phpunit/phpunit/d/daily)](https://packagist.org/packages/phpunit/phpunit/stats)
# PHPUnit
[![Latest Stable Version](https://poser.pugx.org/phpunit/phpunit/v)](https://packagist.org/packages/phpunit/phpunit)
[![CI Status](https://github.com/sebastianbergmann/phpunit/workflows/CI/badge.svg)](https://github.com/sebastianbergmann/phpunit/actions)
[![codecov](https://codecov.io/gh/sebastianbergmann/phpunit/branch/main/graph/badge.svg)](https://codecov.io/gh/sebastianbergmann/phpunit)
PHPUnit is a programmer-oriented testing framework for PHP. It is an instance of the xUnit architecture for unit testing frameworks.
PHPUnit is a programmer-oriented testing framework for PHP.
It is an instance of the xUnit architecture for unit testing frameworks.
## Installation
We distribute a [PHP Archive (PHAR)](https://php.net/phar) that has all required (as well as some optional) dependencies of PHPUnit bundled in a single file:
We distribute a [PHP Archive (PHAR)](https://php.net/phar) that has all required dependencies of PHPUnit bundled in a single file:
```bash
$ wget https://phar.phpunit.de/phpunit-X.Y.phar
@@ -18,17 +24,86 @@ $ php phpunit-X.Y.phar --version
Please replace `X.Y` with the version of PHPUnit you are interested in.
Alternatively, you may use [Composer](https://getcomposer.org/) to download and install PHPUnit as well as its dependencies. Please refer to the [documentation](https://phpunit.de/documentation.html) for details on how to install PHPUnit.
Alternatively, you may use [Composer](https://getcomposer.org/) to download and install PHPUnit as well as its dependencies.
Please refer to the [documentation](https://phpunit.de/documentation.html?ref=github) for details on how to install PHPUnit.
## Contribute
Please refer to [CONTRIBUTING.md](https://github.com/sebastianbergmann/phpunit/blob/main/.github/CONTRIBUTING.md) for information on how to contribute to PHPUnit and its related projects.
## List of Contributors
A big "Thank you!" to everyone who has contributed to PHPUnit!
You can find a detailed list of contributors on every PHPUnit related package on GitHub.
Thanks to everyone who has contributed to PHPUnit! You can find a detailed list of contributors on every PHPUnit related package on GitHub. This list shows only the major components:
Here is a list of all components that are primarily developed and maintained by [Sebastian Bergmann](https://sebastian-bergmann.de/open-source.html?ref=github):
* [PHPUnit](https://github.com/sebastianbergmann/phpunit/graphs/contributors)
* [php-code-coverage](https://github.com/sebastianbergmann/php-code-coverage/graphs/contributors)
* [phpunit/phpunit](https://github.com/sebastianbergmann/phpunit)
* [phpunit/php-code-coverage](https://github.com/sebastianbergmann/php-code-coverage)
* [phpunit/php-file-iterator](https://github.com/sebastianbergmann/php-file-iterator)
* [phpunit/php-invoker](https://github.com/sebastianbergmann/php-invoker)
* [phpunit/php-text-template](https://github.com/sebastianbergmann/php-text-template)
* [phpunit/php-timer](https://github.com/sebastianbergmann/php-timer)
* [sebastian/cli-parser](https://github.com/sebastianbergmann/cli-parser)
* [sebastian/comparator](https://github.com/sebastianbergmann/comparator)
* [sebastian/complexity](https://github.com/sebastianbergmann/complexity)
* [sebastian/diff](https://github.com/sebastianbergmann/diff)
* [sebastian/environment](https://github.com/sebastianbergmann/environment)
* [sebastian/exporter](https://github.com/sebastianbergmann/exporter)
* [sebastian/global-state](https://github.com/sebastianbergmann/global-state)
* [sebastian/lines-of-code](https://github.com/sebastianbergmann/lines-of-code)
* [sebastian/object-enumerator](https://github.com/sebastianbergmann/object-enumerator)
* [sebastian/object-reflector](https://github.com/sebastianbergmann/object-reflector)
* [sebastian/recursion-context](https://github.com/sebastianbergmann/recursion-context)
* [sebastian/type](https://github.com/sebastianbergmann/type)
* [sebastian/version](https://github.com/sebastianbergmann/version)
A very special thanks to everyone who has contributed to the [documentation](https://github.com/sebastianbergmann/phpunit-documentation-english/graphs/contributors).
A very special thanks to everyone who has contributed to the [PHPUnit Manual](https://github.com/sebastianbergmann/phpunit-documentation-english).
In addition to the components listed above, PHPUnit depends on the components listed below:
* [myclabs/deep-copy](https://github.com/myclabs/DeepCopy)
* [nikic/php-parser](https://github.com/nikic/php-parser)
* [phar-io/manifest](https://github.com/phar-io/manifest)
* [phar-io/version](https://github.com/phar-io/version)
* [staabm/side-effects-detector](https://github.com/staabm/side-effects-detector)
* [theseer/tokenizer](https://github.com/theseer/tokenizer)
These tools are used to develop PHPUnit:
* [Composer](https://getcomposer.org/)
* [Phive](https://phar.io/)
* [PHP Autoload Builder](https://github.com/theseer/Autoload/)
* [PHP-CS-Fixer](https://cs.symfony.com/)
* [PHP-Scoper](https://github.com/humbug/php-scoper)
* [PHPStan](https://phpstan.org/)
## Sponsors
It has taken [Sebastian Bergmann](https://sebastian-bergmann.de/open-source.html?ref=github) thousands of hours to develop, test, and support PHPUnit.
[**You can sponsor his Open Source work through GitHub Sponsors**](https://github.com/sponsors/sebastianbergmann), for example.
These businesses support Sebastian Bergmann's work on PHPUnit:
<table>
<tbody>
<tr>
<td style="width: 30%; vertical-align: middle;"><a href="https://www.bubbleshooter.net/"><img alt="Bubble Shooter" src=".github/img/bubble-shooter.png" style="width: 200px;"/></a></td>
<td style="width: 30%; vertical-align: middle;"><a href="https://www.in2it.be/phpunit-supporter/"><img alt="in2it vof" src=".github/img/in2it.svg" style="width: 200px;"/></a></td>
<td style="width: 30%; vertical-align: middle;"><a href="https://www.lambdatest.com/"><img alt="LambdaTest" src=".github/img/lambdatest.svg" style="width: 200px;"/></a></td>
</tr>
<tr>
<td style="width: 30%; vertical-align: middle;"><a href="https://roave.com/"><img alt="Roave" src=".github/img/roave.svg" style="width: 200px;"/></a></td>
<td style="width: 30%; vertical-align: middle;"><a href="https://testmo.com/"><img alt="Testmo GmbH" src=".github/img/testmo.svg" style="width: 200px;"/></a></td>
<td style="width: 30%; vertical-align: middle;"><a href="https://tideways.com/"><img alt="Tideways GmbH" src=".github/img/tideways.svg" style="width: 200px;"/></a></td>
</tr>
<tr>
<td style="width: 30%; vertical-align: middle;"><a href="https://typo3.com/"><img alt="TYPO3 GmbH" src=".github/img/typo3.svg" style="width: 200px;"/></a></td>
<td style="width: 30%; vertical-align: middle;"><a href="https://vema-eg.de/"><img alt="VEMA Versicherungsmakler Genossenschaft eG" src=".github/img/vema.svg" style="width: 200px;"/></a></td>
</tr>
</tbody>
</table>
Would you like to see your logo here as well as on the [PHPUnit website](https://phpunit.de/sponsors.html?ref=github)?
Contact Sebastian Bergmann at [[email protected]](mailto:[email protected]) to learn more about how you can support his work on PHPUnit.
Whether you are a CEO, CFO, CTO, or a developer: your company surely depends on Open Source software.
[It is time to pay your share](https://opensourcepledge.com/) and support maintainers like [Sebastian Bergmann](https://sebastian-bergmann.de/open-source.html?ref=github).
+18 -23
View File
@@ -22,7 +22,7 @@
},
"prefer-stable": true,
"require": {
"php": ">=8.2",
"php": ">=8.3",
"ext-dom": "*",
"ext-json": "*",
"ext-libxml": "*",
@@ -32,34 +32,30 @@
"myclabs/deep-copy": "^1.13.4",
"phar-io/manifest": "^2.0.4",
"phar-io/version": "^3.2.1",
"phpunit/php-code-coverage": "^11.0.11",
"phpunit/php-file-iterator": "^5.1.0",
"phpunit/php-invoker": "^5.0.1",
"phpunit/php-text-template": "^4.0.1",
"phpunit/php-timer": "^7.0.1",
"sebastian/cli-parser": "^3.0.2",
"sebastian/code-unit": "^3.0.3",
"sebastian/comparator": "^6.3.2",
"sebastian/diff": "^6.0.2",
"sebastian/environment": "^7.2.1",
"sebastian/exporter": "^6.3.2",
"sebastian/global-state": "^7.0.2",
"sebastian/object-enumerator": "^6.0.1",
"sebastian/type": "^5.1.3",
"sebastian/version": "^5.0.2",
"phpunit/php-code-coverage": "^12.4.0",
"phpunit/php-file-iterator": "^6.0.0",
"phpunit/php-invoker": "^6.0.0",
"phpunit/php-text-template": "^5.0.0",
"phpunit/php-timer": "^8.0.0",
"sebastian/cli-parser": "^4.2.0",
"sebastian/comparator": "^7.1.3",
"sebastian/diff": "^7.0.0",
"sebastian/environment": "^8.0.3",
"sebastian/exporter": "^7.0.2",
"sebastian/global-state": "^8.0.2",
"sebastian/object-enumerator": "^7.0.0",
"sebastian/type": "^6.0.3",
"sebastian/version": "^6.0.0",
"staabm/side-effects-detector": "^1.0.5"
},
"config": {
"platform": {
"php": "8.2.0"
"php": "8.3.0"
},
"classmap-authoritative": true,
"optimize-autoloader": true,
"sort-packages": true
},
"suggest": {
"ext-soap": "To be able to generate mocks based on WSDL files"
},
"bin": [
"phpunit"
],
@@ -78,8 +74,8 @@
"files": [
"tests/_files/deprecation-trigger/trigger_deprecation.php",
"tests/unit/Event/AbstractEventTestCase.php",
"tests/unit/TextUI/AbstractSouceFilterTestCase.php",
"tests/unit/Framework/MockObject/TestDoubleTestCase.php",
"tests/unit/Metadata/Parser/AnnotationParserTestCase.php",
"tests/unit/Metadata/Parser/AttributeParserTestCase.php",
"tests/unit/Framework/Assert/assertContainsOnlyArrayTest.php",
"tests/unit/Framework/Assert/assertContainsOnlyBoolTest.php",
@@ -108,7 +104,6 @@
"tests/unit/Framework/Assert/assertNullTest.php",
"tests/unit/Framework/Assert/assertSameSizeTest.php",
"tests/unit/Framework/Assert/assertSameTest.php",
"tests/_files/CoverageNamespacedFunctionTest.php",
"tests/_files/CoveredFunction.php",
"tests/_files/Generator.php",
"tests/_files/NamespaceCoveredFunction.php",
@@ -117,7 +112,7 @@
},
"extra": {
"branch-alias": {
"dev-main": "11.5-dev"
"dev-main": "12.4-dev"
}
}
}
+206 -296
View File
File diff suppressed because it is too large Load Diff
Vendored Regular → Executable
+2 -2
View File
@@ -24,11 +24,11 @@ if (!version_compare(PHP_VERSION, PHP_VERSION, '=')) {
die(1);
}
if (version_compare('8.2.0', PHP_VERSION, '>')) {
if (version_compare('8.3.0', PHP_VERSION, '>')) {
fwrite(
STDERR,
sprintf(
'This version of PHPUnit requires PHP >= 8.2.' . PHP_EOL .
'This version of PHPUnit requires PHP >= 8.3.' . PHP_EOL .
'You are using PHP %s (%s).' . PHP_EOL,
PHP_VERSION,
PHP_BINARY
+11 -2
View File
@@ -2,7 +2,7 @@
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:annotation>
<xs:documentation source="https://phpunit.de/documentation.html">
This Schema file defines the rules by which the XML configuration file of PHPUnit 11.5 may be structured.
This Schema file defines the rules by which the XML configuration file of PHPUnit 12.4 may be structured.
</xs:documentation>
<xs:appinfo source="https://phpunit.de/documentation.html"/>
</xs:annotation>
@@ -182,6 +182,7 @@
<xs:attribute name="failOnAllIssues" type="xs:boolean" default="false"/>
<xs:attribute name="failOnDeprecation" type="xs:boolean" default="false"/>
<xs:attribute name="failOnPhpunitDeprecation" type="xs:boolean" default="false"/>
<xs:attribute name="failOnPhpunitNotice" type="xs:boolean" default="false"/>
<xs:attribute name="failOnPhpunitWarning" type="xs:boolean" default="true"/>
<xs:attribute name="failOnEmptyTestSuite" type="xs:boolean" default="false"/>
<xs:attribute name="failOnIncomplete" type="xs:boolean" default="false"/>
@@ -220,10 +221,11 @@
<xs:attribute name="displayDetailsOnSkippedTests" type="xs:boolean" default="false"/>
<xs:attribute name="displayDetailsOnTestsThatTriggerDeprecations" type="xs:boolean" default="false"/>
<xs:attribute name="displayDetailsOnPhpunitDeprecations" type="xs:boolean" default="false"/>
<xs:attribute name="displayDetailsOnPhpunitNotices" type="xs:boolean" default="false"/>
<xs:attribute name="displayDetailsOnTestsThatTriggerErrors" type="xs:boolean" default="false"/>
<xs:attribute name="displayDetailsOnTestsThatTriggerNotices" type="xs:boolean" default="false"/>
<xs:attribute name="displayDetailsOnTestsThatTriggerWarnings" type="xs:boolean" default="false"/>
<xs:attribute name="shortenArraysForExportThreshold" type="xs:integer" default="0"/>
<xs:attribute name="shortenArraysForExportThreshold" type="xs:integer" default="10"/>
</xs:attributeGroup>
<xs:group name="configGroup">
<xs:all>
@@ -252,6 +254,7 @@
</xs:choice>
</xs:sequence>
<xs:attribute name="name" type="xs:string" use="required"/>
<xs:attribute name="bootstrap" type="xs:string" use="optional"/>
</xs:complexType>
<xs:group name="testSuitePathGroup">
<xs:sequence>
@@ -289,6 +292,7 @@
<xs:element name="cobertura" type="logToFileType" minOccurs="0"/>
<xs:element name="crap4j" type="coverageReportCrap4JType" minOccurs="0" />
<xs:element name="html" type="coverageReportHtmlType" minOccurs="0" />
<xs:element name="openclover" type="logToFileType" minOccurs="0"/>
<xs:element name="php" type="logToFileType" minOccurs="0" />
<xs:element name="text" type="coverageReportTextType" minOccurs="0" />
<xs:element name="xml" type="logToDirectoryType" minOccurs="0" />
@@ -297,6 +301,7 @@
<xs:group name="loggingGroup">
<xs:all>
<xs:element name="junit" type="logToFileType" minOccurs="0" />
<xs:element name="otr" type="otrType" minOccurs="0" />
<xs:element name="teamcity" type="logToFileType" minOccurs="0" />
<xs:element name="testdoxHtml" type="logToFileType" minOccurs="0" />
<xs:element name="testdoxText" type="logToFileType" minOccurs="0" />
@@ -308,6 +313,10 @@
<xs:complexType name="logToDirectoryType">
<xs:attribute name="outputDirectory" type="xs:anyURI" use="required"/>
</xs:complexType>
<xs:complexType name="otrType">
<xs:attribute name="outputFile" type="xs:anyURI" use="required"/>
<xs:attribute name="includeGitInformation" type="xs:boolean" default="false"/>
</xs:complexType>
<xs:complexType name="coverageReportCrap4JType">
<xs:attribute name="outputFile" type="xs:anyURI" use="required"/>
<xs:attribute name="threshold" type="xs:integer"/>
@@ -11,6 +11,7 @@ namespace PHPUnit\Event;
use function assert;
use function memory_reset_peak_usage;
use function preg_match;
use PHPUnit\Event\Code\ClassMethod;
use PHPUnit\Event\Code\ComparisonFailure;
use PHPUnit\Event\Code\IssueTrigger\IssueTrigger;
@@ -27,8 +28,11 @@ use PHPUnit\Event\TestSuite\Skipped as TestSuiteSkipped;
use PHPUnit\Event\TestSuite\Sorted as TestSuiteSorted;
use PHPUnit\Event\TestSuite\Started as TestSuiteStarted;
use PHPUnit\Event\TestSuite\TestSuite;
use PHPUnit\Framework\TestCase;
use PHPUnit\Metadata\IgnorePhpunitWarnings;
use PHPUnit\Metadata\Parser\Registry;
use PHPUnit\TextUI\Configuration\Configuration;
use PHPUnit\Util\Exporter;
use SebastianBergmann\Comparator\Comparator;
/**
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
@@ -93,6 +97,8 @@ final class DispatchingEmitter implements Emitter
}
/**
* @param non-empty-string $filename
*
* @throws InvalidArgumentException
* @throws UnknownEventTypeException
*/
@@ -107,6 +113,10 @@ final class DispatchingEmitter implements Emitter
}
/**
* @param non-empty-string $filename
* @param non-empty-string $name
* @param non-empty-string $version
*
* @throws InvalidArgumentException
* @throws UnknownEventTypeException
*/
@@ -263,14 +273,21 @@ final class DispatchingEmitter implements Emitter
);
}
public function testRunnerStartedChildProcess(): void
public function childProcessStarted(): void
{
$this->dispatcher->dispatch(
new TestRunner\ChildProcessStarted($this->telemetryInfo()),
);
}
public function testRunnerFinishedChildProcess(string $stdout, string $stderr): void
public function childProcessErrored(): void
{
$this->dispatcher->dispatch(
new TestRunner\ChildProcessErrored($this->telemetryInfo()),
);
}
public function childProcessFinished(string $stdout, string $stderr): void
{
$this->dispatcher->dispatch(
new TestRunner\ChildProcessFinished(
@@ -328,18 +345,34 @@ final class DispatchingEmitter implements Emitter
* @throws InvalidArgumentException
* @throws UnknownEventTypeException
*/
public function testPreparationFailed(Code\Test $test): void
public function testPreparationErrored(Code\Test $test, Throwable $throwable): void
{
$this->dispatcher->dispatch(
new Test\PreparationFailed(
new Test\PreparationErrored(
$this->telemetryInfo(),
$test,
$throwable,
),
);
}
/**
* @param class-string $testClassName
* @throws InvalidArgumentException
* @throws UnknownEventTypeException
*/
public function testPreparationFailed(Code\Test $test, Throwable $throwable): void
{
$this->dispatcher->dispatch(
new Test\PreparationFailed(
$this->telemetryInfo(),
$test,
$throwable,
),
);
}
/**
* @param class-string<TestCase> $testClassName
*
* @throws InvalidArgumentException
* @throws UnknownEventTypeException
@@ -356,7 +389,7 @@ final class DispatchingEmitter implements Emitter
}
/**
* @param class-string $testClassName
* @param class-string<TestCase> $testClassName
*
* @throws InvalidArgumentException
* @throws UnknownEventTypeException
@@ -374,7 +407,25 @@ final class DispatchingEmitter implements Emitter
}
/**
* @param class-string $testClassName
* @param class-string<TestCase> $testClassName
*
* @throws InvalidArgumentException
* @throws UnknownEventTypeException
*/
public function beforeFirstTestMethodFailed(string $testClassName, ClassMethod $calledMethod, Throwable $throwable): void
{
$this->dispatcher->dispatch(
new Test\BeforeFirstTestMethodFailed(
$this->telemetryInfo(),
$testClassName,
$calledMethod,
$throwable,
),
);
}
/**
* @param class-string<TestCase> $testClassName
*
* @throws InvalidArgumentException
* @throws UnknownEventTypeException
@@ -391,34 +442,30 @@ final class DispatchingEmitter implements Emitter
}
/**
* @param class-string $testClassName
*
* @throws InvalidArgumentException
* @throws UnknownEventTypeException
*/
public function beforeTestMethodCalled(string $testClassName, ClassMethod $calledMethod): void
public function beforeTestMethodCalled(TestMethod $test, ClassMethod $calledMethod): void
{
$this->dispatcher->dispatch(
new Test\BeforeTestMethodCalled(
$this->telemetryInfo(),
$testClassName,
$test,
$calledMethod,
),
);
}
/**
* @param class-string $testClassName
*
* @throws InvalidArgumentException
* @throws UnknownEventTypeException
*/
public function beforeTestMethodErrored(string $testClassName, ClassMethod $calledMethod, Throwable $throwable): void
public function beforeTestMethodErrored(TestMethod $test, ClassMethod $calledMethod, Throwable $throwable): void
{
$this->dispatcher->dispatch(
new Test\BeforeTestMethodErrored(
$this->telemetryInfo(),
$testClassName,
$test,
$calledMethod,
$throwable,
),
@@ -426,51 +473,61 @@ final class DispatchingEmitter implements Emitter
}
/**
* @param class-string $testClassName
*
* @throws InvalidArgumentException
* @throws UnknownEventTypeException
*/
public function beforeTestMethodFinished(string $testClassName, ClassMethod ...$calledMethods): void
public function beforeTestMethodFailed(TestMethod $test, ClassMethod $calledMethod, Throwable $throwable): void
{
$this->dispatcher->dispatch(
new Test\BeforeTestMethodFailed(
$this->telemetryInfo(),
$test,
$calledMethod,
$throwable,
),
);
}
/**
* @throws InvalidArgumentException
* @throws UnknownEventTypeException
*/
public function beforeTestMethodFinished(TestMethod $test, ClassMethod ...$calledMethods): void
{
$this->dispatcher->dispatch(
new Test\BeforeTestMethodFinished(
$this->telemetryInfo(),
$testClassName,
$test,
...$calledMethods,
),
);
}
/**
* @param class-string $testClassName
*
* @throws InvalidArgumentException
* @throws UnknownEventTypeException
*/
public function preConditionCalled(string $testClassName, ClassMethod $calledMethod): void
public function preConditionCalled(TestMethod $test, ClassMethod $calledMethod): void
{
$this->dispatcher->dispatch(
new Test\PreConditionCalled(
$this->telemetryInfo(),
$testClassName,
$test,
$calledMethod,
),
);
}
/**
* @param class-string $testClassName
*
* @throws InvalidArgumentException
* @throws UnknownEventTypeException
*/
public function preConditionErrored(string $testClassName, ClassMethod $calledMethod, Throwable $throwable): void
public function preConditionErrored(TestMethod $test, ClassMethod $calledMethod, Throwable $throwable): void
{
$this->dispatcher->dispatch(
new Test\PreConditionErrored(
$this->telemetryInfo(),
$testClassName,
$test,
$calledMethod,
$throwable,
),
@@ -478,17 +535,31 @@ final class DispatchingEmitter implements Emitter
}
/**
* @param class-string $testClassName
*
* @throws InvalidArgumentException
* @throws UnknownEventTypeException
*/
public function preConditionFinished(string $testClassName, ClassMethod ...$calledMethods): void
public function preConditionFailed(TestMethod $test, ClassMethod $calledMethod, Throwable $throwable): void
{
$this->dispatcher->dispatch(
new Test\PreConditionFailed(
$this->telemetryInfo(),
$test,
$calledMethod,
$throwable,
),
);
}
/**
* @throws InvalidArgumentException
* @throws UnknownEventTypeException
*/
public function preConditionFinished(TestMethod $test, ClassMethod ...$calledMethods): void
{
$this->dispatcher->dispatch(
new Test\PreConditionFinished(
$this->telemetryInfo(),
$testClassName,
$test,
...$calledMethods,
),
);
@@ -511,7 +582,7 @@ final class DispatchingEmitter implements Emitter
}
/**
* @param class-string $className
* @param class-string<Comparator> $className
*
* @throws InvalidArgumentException
* @throws UnknownEventTypeException
@@ -558,62 +629,6 @@ final class DispatchingEmitter implements Emitter
);
}
/**
* @param trait-string $traitName
*
* @throws InvalidArgumentException
* @throws UnknownEventTypeException
*/
public function testCreatedMockObjectForTrait(string $traitName): void
{
$this->dispatcher->dispatch(
new Test\MockObjectForTraitCreated(
$this->telemetryInfo(),
$traitName,
),
);
}
/**
* @param class-string $className
*
* @throws InvalidArgumentException
* @throws UnknownEventTypeException
*/
public function testCreatedMockObjectForAbstractClass(string $className): void
{
$this->dispatcher->dispatch(
new Test\MockObjectForAbstractClassCreated(
$this->telemetryInfo(),
$className,
),
);
}
/**
* @param class-string $originalClassName
* @param class-string $mockClassName
* @param list<string> $methods
* @param list<mixed> $options
*
* @throws InvalidArgumentException
* @throws UnknownEventTypeException
*/
public function testCreatedMockObjectFromWsdl(string $wsdlFile, string $originalClassName, string $mockClassName, array $methods, bool $callOriginalConstructor, array $options): void
{
$this->dispatcher->dispatch(
new Test\MockObjectFromWsdlCreated(
$this->telemetryInfo(),
$wsdlFile,
$originalClassName,
$mockClassName,
$methods,
$callOriginalConstructor,
$options,
),
);
}
/**
* @param class-string $className
*
@@ -631,24 +646,6 @@ final class DispatchingEmitter implements Emitter
);
}
/**
* @param class-string $className
* @param list<mixed> $constructorArguments
*
* @throws InvalidArgumentException
* @throws UnknownEventTypeException
*/
public function testCreatedTestProxy(string $className, array $constructorArguments): void
{
$this->dispatcher->dispatch(
new Test\TestProxyCreated(
$this->telemetryInfo(),
$className,
Exporter::shortenedRecursiveExport($constructorArguments),
),
);
}
/**
* @param class-string $className
*
@@ -801,6 +798,28 @@ final class DispatchingEmitter implements Emitter
);
}
/**
* @param non-empty-string $message
*
* @throws InvalidArgumentException
* @throws NoTestCaseObjectOnCallStackException
* @throws UnknownEventTypeException
*/
public function testTriggeredPhpunitNotice(?Code\Test $test, string $message): void
{
if ($test === null) {
$test = TestMethodBuilder::fromCallStack();
}
$this->dispatcher->dispatch(
new Test\PhpunitNoticeTriggered(
$this->telemetryInfo(),
$test,
$message,
),
);
}
/**
* @param non-empty-string $message
* @param non-empty-string $file
@@ -992,11 +1011,30 @@ final class DispatchingEmitter implements Emitter
*/
public function testTriggeredPhpunitWarning(Code\Test $test, string $message): void
{
$ignoredByTest = false;
if ($test->isTestMethod()) {
assert($test instanceof TestMethod);
$metadata = Registry::parser()->forMethod($test->className(), $test->methodName())->isIgnorePhpunitWarnings()->asArray();
if (isset($metadata[0])) {
assert($metadata[0] instanceof IgnorePhpunitWarnings);
$messagePattern = $metadata[0]->messagePattern();
if ($messagePattern === null || (bool) preg_match('{' . $messagePattern . '}', $message)) {
$ignoredByTest = true;
}
}
}
$this->dispatcher->dispatch(
new Test\PhpunitWarningTriggered(
$this->telemetryInfo(),
$test,
$message,
$ignoredByTest,
),
);
}
@@ -1018,6 +1056,25 @@ final class DispatchingEmitter implements Emitter
}
/**
* @param non-empty-string $additionalInformation
*
* @throws InvalidArgumentException
* @throws UnknownEventTypeException
*/
public function testProvidedAdditionalInformation(TestMethod $test, string $additionalInformation): void
{
$this->dispatcher->dispatch(
new Test\AdditionalInformationProvided(
$this->telemetryInfo(),
$test,
$additionalInformation,
),
);
}
/**
* @param non-negative-int $numberOfAssertionsPerformed
*
* @throws InvalidArgumentException
* @throws UnknownEventTypeException
*/
@@ -1033,34 +1090,30 @@ final class DispatchingEmitter implements Emitter
}
/**
* @param class-string $testClassName
*
* @throws InvalidArgumentException
* @throws UnknownEventTypeException
*/
public function postConditionCalled(string $testClassName, ClassMethod $calledMethod): void
public function postConditionCalled(TestMethod $test, ClassMethod $calledMethod): void
{
$this->dispatcher->dispatch(
new Test\PostConditionCalled(
$this->telemetryInfo(),
$testClassName,
$test,
$calledMethod,
),
);
}
/**
* @param class-string $testClassName
*
* @throws InvalidArgumentException
* @throws UnknownEventTypeException
*/
public function postConditionErrored(string $testClassName, ClassMethod $calledMethod, Throwable $throwable): void
public function postConditionErrored(TestMethod $test, ClassMethod $calledMethod, Throwable $throwable): void
{
$this->dispatcher->dispatch(
new Test\PostConditionErrored(
$this->telemetryInfo(),
$testClassName,
$test,
$calledMethod,
$throwable,
),
@@ -1068,51 +1121,61 @@ final class DispatchingEmitter implements Emitter
}
/**
* @param class-string $testClassName
*
* @throws InvalidArgumentException
* @throws UnknownEventTypeException
*/
public function postConditionFinished(string $testClassName, ClassMethod ...$calledMethods): void
public function postConditionFailed(TestMethod $test, ClassMethod $calledMethod, Throwable $throwable): void
{
$this->dispatcher->dispatch(
new Test\PostConditionFailed(
$this->telemetryInfo(),
$test,
$calledMethod,
$throwable,
),
);
}
/**
* @throws InvalidArgumentException
* @throws UnknownEventTypeException
*/
public function postConditionFinished(TestMethod $test, ClassMethod ...$calledMethods): void
{
$this->dispatcher->dispatch(
new Test\PostConditionFinished(
$this->telemetryInfo(),
$testClassName,
$test,
...$calledMethods,
),
);
}
/**
* @param class-string $testClassName
*
* @throws InvalidArgumentException
* @throws UnknownEventTypeException
*/
public function afterTestMethodCalled(string $testClassName, ClassMethod $calledMethod): void
public function afterTestMethodCalled(TestMethod $test, ClassMethod $calledMethod): void
{
$this->dispatcher->dispatch(
new Test\AfterTestMethodCalled(
$this->telemetryInfo(),
$testClassName,
$test,
$calledMethod,
),
);
}
/**
* @param class-string $testClassName
*
* @throws InvalidArgumentException
* @throws UnknownEventTypeException
*/
public function afterTestMethodErrored(string $testClassName, ClassMethod $calledMethod, Throwable $throwable): void
public function afterTestMethodErrored(TestMethod $test, ClassMethod $calledMethod, Throwable $throwable): void
{
$this->dispatcher->dispatch(
new Test\AfterTestMethodErrored(
$this->telemetryInfo(),
$testClassName,
$test,
$calledMethod,
$throwable,
),
@@ -1120,24 +1183,38 @@ final class DispatchingEmitter implements Emitter
}
/**
* @param class-string $testClassName
*
* @throws InvalidArgumentException
* @throws UnknownEventTypeException
*/
public function afterTestMethodFinished(string $testClassName, ClassMethod ...$calledMethods): void
public function afterTestMethodFailed(TestMethod $test, ClassMethod $calledMethod, Throwable $throwable): void
{
$this->dispatcher->dispatch(
new Test\AfterTestMethodFailed(
$this->telemetryInfo(),
$test,
$calledMethod,
$throwable,
),
);
}
/**
* @throws InvalidArgumentException
* @throws UnknownEventTypeException
*/
public function afterTestMethodFinished(TestMethod $test, ClassMethod ...$calledMethods): void
{
$this->dispatcher->dispatch(
new Test\AfterTestMethodFinished(
$this->telemetryInfo(),
$testClassName,
$test,
...$calledMethods,
),
);
}
/**
* @param class-string $testClassName
* @param class-string<TestCase> $testClassName
*
* @throws InvalidArgumentException
* @throws UnknownEventTypeException
@@ -1154,7 +1231,7 @@ final class DispatchingEmitter implements Emitter
}
/**
* @param class-string $testClassName
* @param class-string<TestCase> $testClassName
*
* @throws InvalidArgumentException
* @throws UnknownEventTypeException
@@ -1172,7 +1249,25 @@ final class DispatchingEmitter implements Emitter
}
/**
* @param class-string $testClassName
* @param class-string<TestCase> $testClassName
*
* @throws InvalidArgumentException
* @throws UnknownEventTypeException
*/
public function afterLastTestMethodFailed(string $testClassName, ClassMethod $calledMethod, Throwable $throwable): void
{
$this->dispatcher->dispatch(
new Test\AfterLastTestMethodFailed(
$this->telemetryInfo(),
$testClassName,
$calledMethod,
$throwable,
),
);
}
/**
* @param class-string<TestCase> $testClassName
*
* @throws InvalidArgumentException
* @throws UnknownEventTypeException
@@ -1206,6 +1301,39 @@ final class DispatchingEmitter implements Emitter
* @throws InvalidArgumentException
* @throws UnknownEventTypeException
*/
public function testRunnerStartedStaticAnalysisForCodeCoverage(): void
{
$this->dispatcher->dispatch(
new TestRunner\StaticAnalysisForCodeCoverageStarted(
$this->telemetryInfo(),
),
);
}
/**
* @param non-negative-int $cacheHits
* @param non-negative-int $cacheMisses
*
* @throws InvalidArgumentException
* @throws UnknownEventTypeException
*/
public function testRunnerFinishedStaticAnalysisForCodeCoverage(int $cacheHits, int $cacheMisses): void
{
$this->dispatcher->dispatch(
new TestRunner\StaticAnalysisForCodeCoverageFinished(
$this->telemetryInfo(),
$cacheHits,
$cacheMisses,
),
);
}
/**
* @param non-empty-string $message
*
* @throws InvalidArgumentException
* @throws UnknownEventTypeException
*/
public function testRunnerTriggeredPhpunitDeprecation(string $message): void
{
try {
@@ -1224,6 +1352,24 @@ final class DispatchingEmitter implements Emitter
}
/**
* @param non-empty-string $message
*
* @throws InvalidArgumentException
* @throws UnknownEventTypeException
*/
public function testRunnerTriggeredPhpunitNotice(string $message): void
{
$this->dispatcher->dispatch(
new TestRunner\NoticeTriggered(
$this->telemetryInfo(),
$message,
),
);
}
/**
* @param non-empty-string $message
*
* @throws InvalidArgumentException
* @throws UnknownEventTypeException
*/
+95 -87
View File
@@ -12,9 +12,12 @@ namespace PHPUnit\Event;
use PHPUnit\Event\Code\ClassMethod;
use PHPUnit\Event\Code\ComparisonFailure;
use PHPUnit\Event\Code\IssueTrigger\IssueTrigger;
use PHPUnit\Event\Code\TestMethod;
use PHPUnit\Event\Code\Throwable;
use PHPUnit\Event\TestSuite\TestSuite;
use PHPUnit\Framework\TestCase;
use PHPUnit\TextUI\Configuration\Configuration;
use SebastianBergmann\Comparator\Comparator;
/**
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
@@ -29,8 +32,16 @@ interface Emitter
public function testRunnerConfigured(Configuration $configuration): void;
/**
* @param non-empty-string $filename
*/
public function testRunnerBootstrapFinished(string $filename): void;
/**
* @param non-empty-string $filename
* @param non-empty-string $name
* @param non-empty-string $version
*/
public function testRunnerLoadedExtensionFromPhar(string $filename, string $name, string $version): void;
/**
@@ -57,63 +68,59 @@ interface Emitter
public function testRunnerTriggeredGarbageCollection(): void;
/**
* @param non-empty-string $message
*/
public function testSuiteSkipped(TestSuite $testSuite, string $message): void;
public function testSuiteStarted(TestSuite $testSuite): void;
public function testPreparationStarted(Code\Test $test): void;
public function testPreparationFailed(Code\Test $test): void;
public function testPreparationErrored(Code\Test $test, Throwable $throwable): void;
public function testPreparationFailed(Code\Test $test, Throwable $throwable): void;
/**
* @param class-string $testClassName
* @param class-string<TestCase> $testClassName
*/
public function beforeFirstTestMethodCalled(string $testClassName, ClassMethod $calledMethod): void;
/**
* @param class-string $testClassName
* @param class-string<TestCase> $testClassName
*/
public function beforeFirstTestMethodErrored(string $testClassName, ClassMethod $calledMethod, Throwable $throwable): void;
/**
* @param class-string $testClassName
* @param class-string<TestCase> $testClassName
*/
public function beforeFirstTestMethodFailed(string $testClassName, ClassMethod $calledMethod, Throwable $throwable): void;
/**
* @param class-string<TestCase> $testClassName
*/
public function beforeFirstTestMethodFinished(string $testClassName, ClassMethod ...$calledMethods): void;
/**
* @param class-string $testClassName
*/
public function beforeTestMethodCalled(string $testClassName, ClassMethod $calledMethod): void;
public function beforeTestMethodCalled(TestMethod $test, ClassMethod $calledMethod): void;
/**
* @param class-string $testClassName
*/
public function beforeTestMethodErrored(string $testClassName, ClassMethod $calledMethod, Throwable $throwable): void;
public function beforeTestMethodErrored(TestMethod $test, ClassMethod $calledMethod, Throwable $throwable): void;
/**
* @param class-string $testClassName
*/
public function beforeTestMethodFinished(string $testClassName, ClassMethod ...$calledMethods): void;
public function beforeTestMethodFailed(TestMethod $test, ClassMethod $calledMethod, Throwable $throwable): void;
/**
* @param class-string $testClassName
*/
public function preConditionCalled(string $testClassName, ClassMethod $calledMethod): void;
public function beforeTestMethodFinished(TestMethod $test, ClassMethod ...$calledMethods): void;
/**
* @param class-string $testClassName
*/
public function preConditionErrored(string $testClassName, ClassMethod $calledMethod, Throwable $throwable): void;
public function preConditionCalled(TestMethod $test, ClassMethod $calledMethod): void;
/**
* @param class-string $testClassName
*/
public function preConditionFinished(string $testClassName, ClassMethod ...$calledMethods): void;
public function preConditionErrored(TestMethod $test, ClassMethod $calledMethod, Throwable $throwable): void;
public function preConditionFailed(TestMethod $test, ClassMethod $calledMethod, Throwable $throwable): void;
public function preConditionFinished(TestMethod $test, ClassMethod ...$calledMethods): void;
public function testPrepared(Code\Test $test): void;
/**
* @param class-string $className
* @param class-string<Comparator> $className
*/
public function testRegisteredComparator(string $className): void;
@@ -127,35 +134,11 @@ interface Emitter
*/
public function testCreatedMockObjectForIntersectionOfInterfaces(array $interfaces): void;
/**
* @param trait-string $traitName
*/
public function testCreatedMockObjectForTrait(string $traitName): void;
/**
* @param class-string $className
*/
public function testCreatedMockObjectForAbstractClass(string $className): void;
/**
* @param class-string $originalClassName
* @param class-string $mockClassName
* @param list<string> $methods
* @param list<mixed> $options
*/
public function testCreatedMockObjectFromWsdl(string $wsdlFile, string $originalClassName, string $mockClassName, array $methods, bool $callOriginalConstructor, array $options): void;
/**
* @param class-string $className
*/
public function testCreatedPartialMockObject(string $className, string ...$methodNames): void;
/**
* @param class-string $className
* @param list<mixed> $constructorArguments
*/
public function testCreatedTestProxy(string $className, array $constructorArguments): void;
/**
* @param class-string $className
*/
@@ -189,6 +172,11 @@ interface Emitter
*/
public function testTriggeredPhpunitDeprecation(?Code\Test $test, string $message): void;
/**
* @param non-empty-string $message
*/
public function testTriggeredPhpunitNotice(?Code\Test $test, string $message): void;
/**
* @param non-empty-string $message
* @param non-empty-string $file
@@ -254,61 +242,81 @@ interface Emitter
*/
public function testPrintedUnexpectedOutput(string $output): void;
/**
* @param non-empty-string $additionalInformation
*/
public function testProvidedAdditionalInformation(TestMethod $test, string $additionalInformation): void;
/**
* @param non-negative-int $numberOfAssertionsPerformed
*/
public function testFinished(Code\Test $test, int $numberOfAssertionsPerformed): void;
/**
* @param class-string $testClassName
*/
public function postConditionCalled(string $testClassName, ClassMethod $calledMethod): void;
public function postConditionCalled(TestMethod $test, ClassMethod $calledMethod): void;
public function postConditionErrored(TestMethod $test, ClassMethod $calledMethod, Throwable $throwable): void;
public function postConditionFailed(TestMethod $test, ClassMethod $calledMethod, Throwable $throwable): void;
public function postConditionFinished(TestMethod $test, ClassMethod ...$calledMethods): void;
public function afterTestMethodCalled(TestMethod $test, ClassMethod $calledMethod): void;
public function afterTestMethodErrored(TestMethod $test, ClassMethod $calledMethod, Throwable $throwable): void;
public function afterTestMethodFailed(TestMethod $test, ClassMethod $calledMethod, Throwable $throwable): void;
public function afterTestMethodFinished(TestMethod $test, ClassMethod ...$calledMethods): void;
/**
* @param class-string $testClassName
*/
public function postConditionErrored(string $testClassName, ClassMethod $calledMethod, Throwable $throwable): void;
/**
* @param class-string $testClassName
*/
public function postConditionFinished(string $testClassName, ClassMethod ...$calledMethods): void;
/**
* @param class-string $testClassName
*/
public function afterTestMethodCalled(string $testClassName, ClassMethod $calledMethod): void;
/**
* @param class-string $testClassName
*/
public function afterTestMethodErrored(string $testClassName, ClassMethod $calledMethod, Throwable $throwable): void;
/**
* @param class-string $testClassName
*/
public function afterTestMethodFinished(string $testClassName, ClassMethod ...$calledMethods): void;
/**
* @param class-string $testClassName
* @param class-string<TestCase> $testClassName
*/
public function afterLastTestMethodCalled(string $testClassName, ClassMethod $calledMethod): void;
/**
* @param class-string $testClassName
* @param class-string<TestCase> $testClassName
*/
public function afterLastTestMethodErrored(string $testClassName, ClassMethod $calledMethod, Throwable $throwable): void;
/**
* @param class-string $testClassName
* @param class-string<TestCase> $testClassName
*/
public function afterLastTestMethodFailed(string $testClassName, ClassMethod $calledMethod, Throwable $throwable): void;
/**
* @param class-string<TestCase> $testClassName
*/
public function afterLastTestMethodFinished(string $testClassName, ClassMethod ...$calledMethods): void;
public function testSuiteFinished(TestSuite $testSuite): void;
public function testRunnerStartedChildProcess(): void;
public function childProcessStarted(): void;
public function testRunnerFinishedChildProcess(string $stdout, string $stderr): void;
public function childProcessErrored(): void;
public function childProcessFinished(string $stdout, string $stderr): void;
public function testRunnerStartedStaticAnalysisForCodeCoverage(): void;
/**
* @param non-negative-int $cacheHits
* @param non-negative-int $cacheMisses
*/
public function testRunnerFinishedStaticAnalysisForCodeCoverage(int $cacheHits, int $cacheMisses): void;
/**
* @param non-empty-string $message
*/
public function testRunnerTriggeredPhpunitDeprecation(string $message): void;
/**
* @param non-empty-string $message
*/
public function testRunnerTriggeredPhpunitNotice(string $message): void;
/**
* @param non-empty-string $message
*/
public function testRunnerTriggeredPhpunitWarning(string $message): void;
public function testRunnerEnabledGarbageCollection(): void;
@@ -39,6 +39,9 @@ final readonly class Finished implements Event
return $this->shellExitCode;
}
/**
* @return non-empty-string
*/
public function asString(): string
{
return sprintf(
@@ -40,6 +40,9 @@ final readonly class Started implements Event
return $this->runtime;
}
/**
* @return non-empty-string
*/
public function asString(): string
{
return sprintf(
+3
View File
@@ -16,5 +16,8 @@ interface Event
{
public function telemetryInfo(): Telemetry\Info;
/**
* @return non-empty-string
*/
public function asString(): string;
}
@@ -12,6 +12,7 @@ namespace PHPUnit\Event\Test;
use function sprintf;
use PHPUnit\Event\Event;
use PHPUnit\Event\Telemetry;
use SebastianBergmann\Comparator\Comparator;
/**
* @immutable
@@ -23,12 +24,12 @@ final readonly class ComparatorRegistered implements Event
private Telemetry\Info $telemetryInfo;
/**
* @var class-string
* @var class-string<Comparator>
*/
private string $className;
/**
* @param class-string $className
* @param class-string<Comparator> $className
*/
public function __construct(Telemetry\Info $telemetryInfo, string $className)
{
@@ -42,13 +43,16 @@ final readonly class ComparatorRegistered implements Event
}
/**
* @return class-string
* @return class-string<Comparator>
*/
public function className(): string
{
return $this->className;
}
/**
* @return non-empty-string
*/
public function asString(): string
{
return sprintf(
@@ -13,6 +13,7 @@ use function sprintf;
use PHPUnit\Event\Code;
use PHPUnit\Event\Event;
use PHPUnit\Event\Telemetry;
use PHPUnit\Framework\TestCase;
/**
* @immutable
@@ -24,13 +25,13 @@ final readonly class AfterLastTestMethodCalled implements Event
private Telemetry\Info $telemetryInfo;
/**
* @var class-string
* @var class-string<TestCase>
*/
private string $testClassName;
private Code\ClassMethod $calledMethod;
/**
* @param class-string $testClassName
* @param class-string<TestCase> $testClassName
*/
public function __construct(Telemetry\Info $telemetryInfo, string $testClassName, Code\ClassMethod $calledMethod)
{
@@ -45,7 +46,7 @@ final readonly class AfterLastTestMethodCalled implements Event
}
/**
* @return class-string
* @return class-string<TestCase>
*/
public function testClassName(): string
{
@@ -57,6 +58,9 @@ final readonly class AfterLastTestMethodCalled implements Event
return $this->calledMethod;
}
/**
* @return non-empty-string
*/
public function asString(): string
{
return sprintf(
@@ -15,6 +15,7 @@ use PHPUnit\Event\Code;
use PHPUnit\Event\Code\Throwable;
use PHPUnit\Event\Event;
use PHPUnit\Event\Telemetry;
use PHPUnit\Framework\TestCase;
/**
* @immutable
@@ -26,14 +27,14 @@ final readonly class AfterLastTestMethodErrored implements Event
private Telemetry\Info $telemetryInfo;
/**
* @var class-string
* @var class-string<TestCase>
*/
private string $testClassName;
private Code\ClassMethod $calledMethod;
private Throwable $throwable;
/**
* @param class-string $testClassName
* @param class-string<TestCase> $testClassName
*/
public function __construct(Telemetry\Info $telemetryInfo, string $testClassName, Code\ClassMethod $calledMethod, Throwable $throwable)
{
@@ -49,7 +50,7 @@ final readonly class AfterLastTestMethodErrored implements Event
}
/**
* @return class-string
* @return class-string<TestCase>
*/
public function testClassName(): string
{
@@ -66,11 +67,14 @@ final readonly class AfterLastTestMethodErrored implements Event
return $this->throwable;
}
/**
* @return non-empty-string
*/
public function asString(): string
{
$message = $this->throwable->message();
if (!empty($message)) {
if ($message !== '') {
$message = PHP_EOL . $message;
}
@@ -14,6 +14,7 @@ use function sprintf;
use PHPUnit\Event\Code;
use PHPUnit\Event\Event;
use PHPUnit\Event\Telemetry;
use PHPUnit\Framework\TestCase;
/**
* @immutable
@@ -25,7 +26,7 @@ final readonly class AfterLastTestMethodFinished implements Event
private Telemetry\Info $telemetryInfo;
/**
* @var class-string
* @var class-string<TestCase>
*/
private string $testClassName;
@@ -35,7 +36,7 @@ final readonly class AfterLastTestMethodFinished implements Event
private array $calledMethods;
/**
* @param class-string $testClassName
* @param class-string<TestCase> $testClassName
*/
public function __construct(Telemetry\Info $telemetryInfo, string $testClassName, Code\ClassMethod ...$calledMethods)
{
@@ -50,7 +51,7 @@ final readonly class AfterLastTestMethodFinished implements Event
}
/**
* @return class-string
* @return class-string<TestCase>
*/
public function testClassName(): string
{
@@ -65,6 +66,9 @@ final readonly class AfterLastTestMethodFinished implements Event
return $this->calledMethods;
}
/**
* @return non-empty-string
*/
public function asString(): string
{
$buffer = 'After Last Test Method Finished:';
@@ -22,20 +22,13 @@ use PHPUnit\Event\Telemetry;
final readonly class AfterTestMethodCalled implements Event
{
private Telemetry\Info $telemetryInfo;
/**
* @var class-string
*/
private string $testClassName;
private Code\TestMethod $test;
private Code\ClassMethod $calledMethod;
/**
* @param class-string $testClassName
*/
public function __construct(Telemetry\Info $telemetryInfo, string $testClassName, Code\ClassMethod $calledMethod)
public function __construct(Telemetry\Info $telemetryInfo, Code\TestMethod $test, Code\ClassMethod $calledMethod)
{
$this->telemetryInfo = $telemetryInfo;
$this->testClassName = $testClassName;
$this->test = $test;
$this->calledMethod = $calledMethod;
}
@@ -44,12 +37,19 @@ final readonly class AfterTestMethodCalled implements Event
return $this->telemetryInfo;
}
public function test(): Code\TestMethod
{
return $this->test;
}
/**
* @return class-string
*
* @deprecated https://github.com/sebastianbergmann/phpunit/issues/6140
*/
public function testClassName(): string
{
return $this->testClassName;
return $this->test->className();
}
public function calledMethod(): Code\ClassMethod
@@ -57,6 +57,9 @@ final readonly class AfterTestMethodCalled implements Event
return $this->calledMethod;
}
/**
* @return non-empty-string
*/
public function asString(): string
{
return sprintf(
@@ -24,21 +24,14 @@ use PHPUnit\Event\Telemetry;
final readonly class AfterTestMethodErrored implements Event
{
private Telemetry\Info $telemetryInfo;
/**
* @var class-string
*/
private string $testClassName;
private Code\TestMethod $test;
private Code\ClassMethod $calledMethod;
private Throwable $throwable;
/**
* @param class-string $testClassName
*/
public function __construct(Telemetry\Info $telemetryInfo, string $testClassName, Code\ClassMethod $calledMethod, Throwable $throwable)
public function __construct(Telemetry\Info $telemetryInfo, Code\TestMethod $test, Code\ClassMethod $calledMethod, Throwable $throwable)
{
$this->telemetryInfo = $telemetryInfo;
$this->testClassName = $testClassName;
$this->test = $test;
$this->calledMethod = $calledMethod;
$this->throwable = $throwable;
}
@@ -48,12 +41,19 @@ final readonly class AfterTestMethodErrored implements Event
return $this->telemetryInfo;
}
public function test(): Code\TestMethod
{
return $this->test;
}
/**
* @return class-string
*
* @deprecated https://github.com/sebastianbergmann/phpunit/issues/6140
*/
public function testClassName(): string
{
return $this->testClassName;
return $this->test->className();
}
public function calledMethod(): Code\ClassMethod
@@ -66,11 +66,14 @@ final readonly class AfterTestMethodErrored implements Event
return $this->throwable;
}
/**
* @return non-empty-string
*/
public function asString(): string
{
$message = $this->throwable->message();
if (!empty($message)) {
if ($message !== '') {
$message = PHP_EOL . $message;
}
@@ -23,24 +23,17 @@ use PHPUnit\Event\Telemetry;
final readonly class AfterTestMethodFinished implements Event
{
private Telemetry\Info $telemetryInfo;
/**
* @var class-string
*/
private string $testClassName;
private Code\TestMethod $test;
/**
* @var list<Code\ClassMethod>
*/
private array $calledMethods;
/**
* @param class-string $testClassName
*/
public function __construct(Telemetry\Info $telemetryInfo, string $testClassName, Code\ClassMethod ...$calledMethods)
public function __construct(Telemetry\Info $telemetryInfo, Code\TestMethod $test, Code\ClassMethod ...$calledMethods)
{
$this->telemetryInfo = $telemetryInfo;
$this->testClassName = $testClassName;
$this->test = $test;
$this->calledMethods = $calledMethods;
}
@@ -49,12 +42,19 @@ final readonly class AfterTestMethodFinished implements Event
return $this->telemetryInfo;
}
public function test(): Code\TestMethod
{
return $this->test;
}
/**
* @return class-string
*
* @deprecated https://github.com/sebastianbergmann/phpunit/issues/6140
*/
public function testClassName(): string
{
return $this->testClassName;
return $this->test->className();
}
/**
@@ -65,6 +65,9 @@ final readonly class AfterTestMethodFinished implements Event
return $this->calledMethods;
}
/**
* @return non-empty-string
*/
public function asString(): string
{
$buffer = 'After Test Method Finished:';
@@ -13,6 +13,7 @@ use function sprintf;
use PHPUnit\Event\Code;
use PHPUnit\Event\Event;
use PHPUnit\Event\Telemetry;
use PHPUnit\Framework\TestCase;
/**
* @immutable
@@ -24,13 +25,13 @@ final readonly class BeforeFirstTestMethodCalled implements Event
private Telemetry\Info $telemetryInfo;
/**
* @var class-string
* @var class-string<TestCase>
*/
private string $testClassName;
private Code\ClassMethod $calledMethod;
/**
* @param class-string $testClassName
* @param class-string<TestCase> $testClassName
*/
public function __construct(Telemetry\Info $telemetryInfo, string $testClassName, Code\ClassMethod $calledMethod)
{
@@ -45,7 +46,7 @@ final readonly class BeforeFirstTestMethodCalled implements Event
}
/**
* @return class-string
* @return class-string<TestCase>
*/
public function testClassName(): string
{
@@ -57,6 +58,9 @@ final readonly class BeforeFirstTestMethodCalled implements Event
return $this->calledMethod;
}
/**
* @return non-empty-string
*/
public function asString(): string
{
return sprintf(
@@ -15,6 +15,7 @@ use PHPUnit\Event\Code;
use PHPUnit\Event\Code\Throwable;
use PHPUnit\Event\Event;
use PHPUnit\Event\Telemetry;
use PHPUnit\Framework\TestCase;
/**
* @immutable
@@ -26,14 +27,14 @@ final readonly class BeforeFirstTestMethodErrored implements Event
private Telemetry\Info $telemetryInfo;
/**
* @var class-string
* @var class-string<TestCase>
*/
private string $testClassName;
private Code\ClassMethod $calledMethod;
private Throwable $throwable;
/**
* @param class-string $testClassName
* @param class-string<TestCase> $testClassName
*/
public function __construct(Telemetry\Info $telemetryInfo, string $testClassName, Code\ClassMethod $calledMethod, Throwable $throwable)
{
@@ -49,7 +50,7 @@ final readonly class BeforeFirstTestMethodErrored implements Event
}
/**
* @return class-string
* @return class-string<TestCase>
*/
public function testClassName(): string
{
@@ -66,11 +67,14 @@ final readonly class BeforeFirstTestMethodErrored implements Event
return $this->throwable;
}
/**
* @return non-empty-string
*/
public function asString(): string
{
$message = $this->throwable->message();
if (!empty($message)) {
if ($message !== '') {
$message = PHP_EOL . $message;
}
@@ -14,6 +14,7 @@ use function sprintf;
use PHPUnit\Event\Code;
use PHPUnit\Event\Event;
use PHPUnit\Event\Telemetry;
use PHPUnit\Framework\TestCase;
/**
* @immutable
@@ -25,7 +26,7 @@ final readonly class BeforeFirstTestMethodFinished implements Event
private Telemetry\Info$telemetryInfo;
/**
* @var class-string
* @var class-string<TestCase>
*/
private string $testClassName;
@@ -35,7 +36,7 @@ final readonly class BeforeFirstTestMethodFinished implements Event
private array $calledMethods;
/**
* @param class-string $testClassName
* @param class-string<TestCase> $testClassName
*/
public function __construct(Telemetry\Info $telemetryInfo, string $testClassName, Code\ClassMethod ...$calledMethods)
{
@@ -50,7 +51,7 @@ final readonly class BeforeFirstTestMethodFinished implements Event
}
/**
* @return class-string
* @return class-string<TestCase>
*/
public function testClassName(): string
{
@@ -65,6 +66,9 @@ final readonly class BeforeFirstTestMethodFinished implements Event
return $this->calledMethods;
}
/**
* @return non-empty-string
*/
public function asString(): string
{
$buffer = 'Before First Test Method Finished:';

Some files were not shown because too many files have changed in this diff Show More