mirror of
https://gitlab.com/signalytic/client-external/streamline/streamline-emr.git
synced 2026-09-13 03:31:31 +00:00
resolved conflicts
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
# ChangeLog
|
||||
|
||||
All notable changes are documented in this file using the [Keep a CHANGELOG](http://keepachangelog.com/) principles.
|
||||
|
||||
## [10.1.16] - 2024-08-22
|
||||
|
||||
### Changed
|
||||
|
||||
* Updated dependencies (so that users that install using Composer's `--prefer-lowest` CLI option also get recent versions)
|
||||
|
||||
## [10.1.15] - 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
|
||||
|
||||
## [10.1.14] - 2024-03-12
|
||||
|
||||
### Fixed
|
||||
|
||||
* [#1033](https://github.com/sebastianbergmann/php-code-coverage/issues/1033): `@codeCoverageIgnore` annotation does not work on `enum`
|
||||
|
||||
## [10.1.13] - 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
|
||||
|
||||
## [10.1.12] - 2024-03-02
|
||||
|
||||
### Changed
|
||||
|
||||
* Do not use implicitly nullable parameters
|
||||
|
||||
## [10.1.11] - 2023-12-21
|
||||
|
||||
### Changed
|
||||
|
||||
* This component is now compatible with `nikic/php-parser` 5.0
|
||||
|
||||
## [10.1.10] - 2023-12-11
|
||||
|
||||
### Fixed
|
||||
|
||||
* [#1023](https://github.com/sebastianbergmann/php-code-coverage/issues/1023): Branch Coverage and Path Coverage are not correctly reported for traits
|
||||
|
||||
## [10.1.9] - 2023-11-23
|
||||
|
||||
### Fixed
|
||||
|
||||
* [#1020](https://github.com/sebastianbergmann/php-code-coverage/issues/1020): Single line method is ignored
|
||||
|
||||
## [10.1.8] - 2023-11-15
|
||||
|
||||
### Fixed
|
||||
|
||||
* [#1018](https://github.com/sebastianbergmann/php-code-coverage/issues/1018): Interface methods are not ignored when their signature is split over multiple lines
|
||||
|
||||
## [10.1.7] - 2023-10-04
|
||||
|
||||
### Fixed
|
||||
|
||||
* [#1014](https://github.com/sebastianbergmann/php-code-coverage/issues/1014): Incorrect statement count in coverage report for constructor property promotion
|
||||
|
||||
## [10.1.6] - 2023-09-19
|
||||
|
||||
### Fixed
|
||||
|
||||
* [#1012](https://github.com/sebastianbergmann/php-code-coverage/issues/1012): Cobertura report pulls functions from report scope, not the individual element
|
||||
|
||||
## [10.1.5] - 2023-09-12
|
||||
|
||||
### Changed
|
||||
|
||||
* [#1011](https://github.com/sebastianbergmann/php-code-coverage/pull/1011): Avoid serialization of cache data in PHP report
|
||||
|
||||
## [10.1.4] - 2023-08-31
|
||||
|
||||
### Fixed
|
||||
|
||||
* Exceptions of type `SebastianBergmann\Template\Exception` are now properly handled
|
||||
|
||||
## [10.1.3] - 2023-07-26
|
||||
|
||||
### Changed
|
||||
|
||||
* The result of `CodeCoverage::getReport()` is now cached
|
||||
|
||||
### Fixed
|
||||
|
||||
* Static analysis cache keys do not include configuration settings that affect source code parsing
|
||||
* The Clover, Cobertura, Crap4j, and PHP report writers no longer create a `php:` directory when they should write to `php://stdout`, for instance
|
||||
|
||||
## [10.1.2] - 2023-05-22
|
||||
|
||||
### Fixed
|
||||
|
||||
* [#998](https://github.com/sebastianbergmann/php-code-coverage/pull/998): Group Use Declarations are not handled properly
|
||||
|
||||
## [10.1.1] - 2023-04-17
|
||||
|
||||
### Fixed
|
||||
|
||||
* [#994](https://github.com/sebastianbergmann/php-code-coverage/issues/994): Argument `$linesToBeIgnored` of `CodeCoverage::stop()` has no effect for files that are not executed at all
|
||||
|
||||
## [10.1.0] - 2023-04-13
|
||||
|
||||
### Added
|
||||
|
||||
* [#982](https://github.com/sebastianbergmann/php-code-coverage/issues/982): Add option to ignore lines from code coverage
|
||||
|
||||
### Deprecated
|
||||
|
||||
* The `SebastianBergmann\CodeCoverage\Filter::includeDirectory()`, `SebastianBergmann\CodeCoverage\Filter::excludeDirectory()`, and `SebastianBergmann\CodeCoverage\Filter::excludeFile()` methods are now deprecated
|
||||
|
||||
[10.1.16]: https://github.com/sebastianbergmann/php-code-coverage/compare/10.1.15...10.1.16
|
||||
[10.1.15]: https://github.com/sebastianbergmann/php-code-coverage/compare/10.1.14...10.1.15
|
||||
[10.1.14]: https://github.com/sebastianbergmann/php-code-coverage/compare/10.1.13...10.1.14
|
||||
[10.1.13]: https://github.com/sebastianbergmann/php-code-coverage/compare/10.1.12...10.1.13
|
||||
[10.1.12]: https://github.com/sebastianbergmann/php-code-coverage/compare/10.1.11...10.1.12
|
||||
[10.1.11]: https://github.com/sebastianbergmann/php-code-coverage/compare/10.1.10...10.1.11
|
||||
[10.1.10]: https://github.com/sebastianbergmann/php-code-coverage/compare/10.1.9...10.1.10
|
||||
[10.1.9]: https://github.com/sebastianbergmann/php-code-coverage/compare/10.1.8...10.1.9
|
||||
[10.1.8]: https://github.com/sebastianbergmann/php-code-coverage/compare/10.1.7...10.1.8
|
||||
[10.1.7]: https://github.com/sebastianbergmann/php-code-coverage/compare/10.1.6...10.1.7
|
||||
[10.1.6]: https://github.com/sebastianbergmann/php-code-coverage/compare/10.1.5...10.1.6
|
||||
[10.1.5]: https://github.com/sebastianbergmann/php-code-coverage/compare/10.1.4...10.1.5
|
||||
[10.1.4]: https://github.com/sebastianbergmann/php-code-coverage/compare/10.1.3...10.1.4
|
||||
[10.1.3]: https://github.com/sebastianbergmann/php-code-coverage/compare/10.1.2...10.1.3
|
||||
[10.1.2]: https://github.com/sebastianbergmann/php-code-coverage/compare/10.1.1...10.1.2
|
||||
[10.1.1]: https://github.com/sebastianbergmann/php-code-coverage/compare/10.1.0...10.1.1
|
||||
[10.1.0]: https://github.com/sebastianbergmann/php-code-coverage/compare/10.0.2...10.1.0
|
||||
@@ -0,0 +1,29 @@
|
||||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2009-2024, Sebastian Bergmann
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"name": "phpunit/php-code-coverage",
|
||||
"description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.",
|
||||
"type": "library",
|
||||
"keywords": [
|
||||
"coverage",
|
||||
"testing",
|
||||
"xunit"
|
||||
],
|
||||
"homepage": "https://github.com/sebastianbergmann/php-code-coverage",
|
||||
"license": "BSD-3-Clause",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Sebastian Bergmann",
|
||||
"email": "sebastian@phpunit.de",
|
||||
"role": "lead"
|
||||
}
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
|
||||
"security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy"
|
||||
},
|
||||
"config": {
|
||||
"platform": {
|
||||
"php": "8.1.0"
|
||||
},
|
||||
"optimize-autoloader": true,
|
||||
"sort-packages": true
|
||||
},
|
||||
"prefer-stable": true,
|
||||
"require": {
|
||||
"php": ">=8.1",
|
||||
"ext-dom": "*",
|
||||
"ext-libxml": "*",
|
||||
"ext-xmlwriter": "*",
|
||||
"nikic/php-parser": "^4.19.1 || ^5.1.0",
|
||||
"phpunit/php-file-iterator": "^4.1.0",
|
||||
"phpunit/php-text-template": "^3.0.1",
|
||||
"sebastian/code-unit-reverse-lookup": "^3.0.0",
|
||||
"sebastian/complexity": "^3.2.0",
|
||||
"sebastian/environment": "^6.1.0",
|
||||
"sebastian/lines-of-code": "^2.0.2",
|
||||
"sebastian/version": "^4.0.1",
|
||||
"theseer/tokenizer": "^1.2.3"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^10.1"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-pcov": "PHP extension that provides line coverage",
|
||||
"ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage"
|
||||
},
|
||||
"autoload": {
|
||||
"classmap": [
|
||||
"src/"
|
||||
]
|
||||
},
|
||||
"autoload-dev": {
|
||||
"files": [
|
||||
"tests/TestCase.php",
|
||||
"tests/_files/BankAccountTest.php"
|
||||
]
|
||||
},
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "10.1.x-dev"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,631 @@
|
||||
<?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 function array_diff;
|
||||
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;
|
||||
use SebastianBergmann\CodeCoverage\Data\ProcessedCodeCoverageData;
|
||||
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\FileAnalyser;
|
||||
use SebastianBergmann\CodeCoverage\StaticAnalysis\ParsingFileAnalyser;
|
||||
use SebastianBergmann\CodeCoverage\Test\TestSize\TestSize;
|
||||
use SebastianBergmann\CodeCoverage\Test\TestStatus\TestStatus;
|
||||
use SebastianBergmann\CodeUnitReverseLookup\Wizard;
|
||||
|
||||
/**
|
||||
* Provides collection functionality for PHP code coverage information.
|
||||
*
|
||||
* @psalm-type TestType = array{
|
||||
* size: string,
|
||||
* status: string,
|
||||
* }
|
||||
*/
|
||||
final class CodeCoverage
|
||||
{
|
||||
private const UNCOVERED_FILES = 'UNCOVERED_FILES';
|
||||
private readonly Driver $driver;
|
||||
private readonly Filter $filter;
|
||||
private readonly Wizard $wizard;
|
||||
private bool $checkForUnintentionallyCoveredCode = false;
|
||||
private bool $includeUncoveredFiles = true;
|
||||
private bool $ignoreDeprecatedCode = false;
|
||||
private ?string $currentId = null;
|
||||
private ?TestSize $currentSize = null;
|
||||
private ProcessedCodeCoverageData $data;
|
||||
private bool $useAnnotationsForIgnoringCode = true;
|
||||
|
||||
/**
|
||||
* @psalm-var array<string,list<int>>
|
||||
*/
|
||||
private array $linesToBeIgnored = [];
|
||||
|
||||
/**
|
||||
* @psalm-var array<string, TestType>
|
||||
*/
|
||||
private array $tests = [];
|
||||
|
||||
/**
|
||||
* @psalm-var list<class-string>
|
||||
*/
|
||||
private array $parentClassesExcludedFromUnintentionallyCoveredCodeCheck = [];
|
||||
private ?FileAnalyser $analyser = null;
|
||||
private ?string $cacheDirectory = null;
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the code coverage information as a graph of node objects.
|
||||
*/
|
||||
public function getReport(): Directory
|
||||
{
|
||||
if ($this->cachedReport === null) {
|
||||
$this->cachedReport = (new Builder($this->analyser()))->build($this);
|
||||
}
|
||||
|
||||
return $this->cachedReport;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears collected code coverage data.
|
||||
*/
|
||||
public function clear(): void
|
||||
{
|
||||
$this->currentId = null;
|
||||
$this->currentSize = null;
|
||||
$this->data = new ProcessedCodeCoverageData;
|
||||
$this->tests = [];
|
||||
$this->cachedReport = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function clearCache(): void
|
||||
{
|
||||
$this->cachedReport = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the filter object used.
|
||||
*/
|
||||
public function filter(): Filter
|
||||
{
|
||||
return $this->filter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the collected code coverage data.
|
||||
*/
|
||||
public function getData(bool $raw = false): ProcessedCodeCoverageData
|
||||
{
|
||||
if (!$raw) {
|
||||
if ($this->includeUncoveredFiles) {
|
||||
$this->addUncoveredFilesFromFilter();
|
||||
}
|
||||
}
|
||||
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the coverage data.
|
||||
*/
|
||||
public function setData(ProcessedCodeCoverageData $data): void
|
||||
{
|
||||
$this->data = $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-return array<string, TestType>
|
||||
*/
|
||||
public function getTests(): array
|
||||
{
|
||||
return $this->tests;
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-param array<string, TestType> $tests
|
||||
*/
|
||||
public function setTests(array $tests): void
|
||||
{
|
||||
$this->tests = $tests;
|
||||
}
|
||||
|
||||
public function start(string $id, ?TestSize $size = null, bool $clear = false): void
|
||||
{
|
||||
if ($clear) {
|
||||
$this->clear();
|
||||
}
|
||||
|
||||
$this->currentId = $id;
|
||||
$this->currentSize = $size;
|
||||
|
||||
$this->driver->start();
|
||||
|
||||
$this->cachedReport = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-param array<string,list<int>> $linesToBeIgnored
|
||||
*/
|
||||
public function stop(bool $append = true, ?TestStatus $status = null, array|false $linesToBeCovered = [], array $linesToBeUsed = [], array $linesToBeIgnored = []): RawCodeCoverageData
|
||||
{
|
||||
$data = $this->driver->stop();
|
||||
|
||||
$this->linesToBeIgnored = array_merge_recursive(
|
||||
$this->linesToBeIgnored,
|
||||
$linesToBeIgnored,
|
||||
);
|
||||
|
||||
$this->append($data, null, $append, $status, $linesToBeCovered, $linesToBeUsed, $linesToBeIgnored);
|
||||
|
||||
$this->currentId = null;
|
||||
$this->currentSize = null;
|
||||
$this->cachedReport = null;
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-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
|
||||
{
|
||||
if ($id === null) {
|
||||
$id = $this->currentId;
|
||||
}
|
||||
|
||||
if ($id === null) {
|
||||
throw new TestIdMissingException;
|
||||
}
|
||||
|
||||
$this->cachedReport = null;
|
||||
|
||||
if ($status === null) {
|
||||
$status = TestStatus::unknown();
|
||||
}
|
||||
|
||||
$size = $this->currentSize;
|
||||
|
||||
if ($size === null) {
|
||||
$size = TestSize::unknown();
|
||||
}
|
||||
|
||||
$this->applyFilter($rawData);
|
||||
|
||||
$this->applyExecutableLinesFilter($rawData);
|
||||
|
||||
if ($this->useAnnotationsForIgnoringCode) {
|
||||
$this->applyIgnoredLinesFilter($rawData, $linesToBeIgnored);
|
||||
}
|
||||
|
||||
$this->data->initializeUnseenData($rawData);
|
||||
|
||||
if (!$append) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($id === self::UNCOVERED_FILES) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->applyCoversAndUsesFilter(
|
||||
$rawData,
|
||||
$linesToBeCovered,
|
||||
$linesToBeUsed,
|
||||
$size,
|
||||
);
|
||||
|
||||
if (empty($rawData->lineCoverage())) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->tests[$id] = [
|
||||
'size' => $size->asString(),
|
||||
'status' => $status->asString(),
|
||||
];
|
||||
|
||||
$this->data->markCodeAsExecutedByTestCase($id, $rawData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges the data from another instance.
|
||||
*/
|
||||
public function merge(self $that): void
|
||||
{
|
||||
$this->filter->includeFiles(
|
||||
$that->filter()->files(),
|
||||
);
|
||||
|
||||
$this->data->merge($that->data);
|
||||
|
||||
$this->tests = array_merge($this->tests, $that->getTests());
|
||||
|
||||
$this->cachedReport = null;
|
||||
}
|
||||
|
||||
public function enableCheckForUnintentionallyCoveredCode(): void
|
||||
{
|
||||
$this->checkForUnintentionallyCoveredCode = true;
|
||||
}
|
||||
|
||||
public function disableCheckForUnintentionallyCoveredCode(): void
|
||||
{
|
||||
$this->checkForUnintentionallyCoveredCode = false;
|
||||
}
|
||||
|
||||
public function includeUncoveredFiles(): void
|
||||
{
|
||||
$this->includeUncoveredFiles = true;
|
||||
}
|
||||
|
||||
public function excludeUncoveredFiles(): void
|
||||
{
|
||||
$this->includeUncoveredFiles = false;
|
||||
}
|
||||
|
||||
public function enableAnnotationsForIgnoringCode(): void
|
||||
{
|
||||
$this->useAnnotationsForIgnoringCode = true;
|
||||
}
|
||||
|
||||
public function disableAnnotationsForIgnoringCode(): void
|
||||
{
|
||||
$this->useAnnotationsForIgnoringCode = false;
|
||||
}
|
||||
|
||||
public function ignoreDeprecatedCode(): void
|
||||
{
|
||||
$this->ignoreDeprecatedCode = true;
|
||||
}
|
||||
|
||||
public function doNotIgnoreDeprecatedCode(): void
|
||||
{
|
||||
$this->ignoreDeprecatedCode = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-assert-if-true !null $this->cacheDirectory
|
||||
*/
|
||||
public function cachesStaticAnalysis(): bool
|
||||
{
|
||||
return $this->cacheDirectory !== null;
|
||||
}
|
||||
|
||||
public function cacheStaticAnalysis(string $directory): void
|
||||
{
|
||||
$this->cacheDirectory = $directory;
|
||||
}
|
||||
|
||||
public function doNotCacheStaticAnalysis(): void
|
||||
{
|
||||
$this->cacheDirectory = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws StaticAnalysisCacheNotConfiguredException
|
||||
*/
|
||||
public function cacheDirectory(): string
|
||||
{
|
||||
if (!$this->cachesStaticAnalysis()) {
|
||||
throw new StaticAnalysisCacheNotConfiguredException(
|
||||
'The static analysis cache is not configured',
|
||||
);
|
||||
}
|
||||
|
||||
return $this->cacheDirectory;
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-param class-string $className
|
||||
*/
|
||||
public function excludeSubclassesOfThisClassFromUnintentionallyCoveredCodeCheck(string $className): void
|
||||
{
|
||||
$this->parentClassesExcludedFromUnintentionallyCoveredCodeCheck[] = $className;
|
||||
}
|
||||
|
||||
public function enableBranchAndPathCoverage(): void
|
||||
{
|
||||
$this->driver->enableBranchAndPathCoverage();
|
||||
}
|
||||
|
||||
public function disableBranchAndPathCoverage(): void
|
||||
{
|
||||
$this->driver->disableBranchAndPathCoverage();
|
||||
}
|
||||
|
||||
public function collectsBranchAndPathCoverage(): bool
|
||||
{
|
||||
return $this->driver->collectsBranchAndPathCoverage();
|
||||
}
|
||||
|
||||
public function detectsDeadCode(): bool
|
||||
{
|
||||
return $this->driver->detectsDeadCode();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ReflectionException
|
||||
* @throws UnintentionallyCoveredCodeException
|
||||
*/
|
||||
private function applyCoversAndUsesFilter(RawCodeCoverageData $rawData, array|false $linesToBeCovered, array $linesToBeUsed, TestSize $size): void
|
||||
{
|
||||
if ($linesToBeCovered === false) {
|
||||
$rawData->clear();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (empty($linesToBeCovered)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->checkForUnintentionallyCoveredCode && !$size->isMedium() && !$size->isLarge()) {
|
||||
$this->performUnintentionallyCoveredCodeCheck($rawData, $linesToBeCovered, $linesToBeUsed);
|
||||
}
|
||||
|
||||
$rawLineData = $rawData->lineCoverage();
|
||||
$filesWithNoCoverage = array_diff_key($rawLineData, $linesToBeCovered);
|
||||
|
||||
foreach (array_keys($filesWithNoCoverage) as $fileWithNoCoverage) {
|
||||
$rawData->removeCoverageDataForFile($fileWithNoCoverage);
|
||||
}
|
||||
|
||||
if (is_array($linesToBeCovered)) {
|
||||
foreach ($linesToBeCovered as $fileToBeCovered => $includedLines) {
|
||||
$rawData->keepLineCoverageDataOnlyForLines($fileToBeCovered, $includedLines);
|
||||
$rawData->keepFunctionCoverageDataOnlyForLines($fileToBeCovered, $includedLines);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function applyFilter(RawCodeCoverageData $data): void
|
||||
{
|
||||
if ($this->filter->isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (array_keys($data->lineCoverage()) as $filename) {
|
||||
if ($this->filter->isExcluded($filename)) {
|
||||
$data->removeCoverageDataForFile($filename);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function applyExecutableLinesFilter(RawCodeCoverageData $data): void
|
||||
{
|
||||
foreach (array_keys($data->lineCoverage()) as $filename) {
|
||||
if (!$this->filter->isFile($filename)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$linesToBranchMap = $this->analyser()->executableLinesIn($filename);
|
||||
|
||||
$data->keepLineCoverageDataOnlyForLines(
|
||||
$filename,
|
||||
array_keys($linesToBranchMap),
|
||||
);
|
||||
|
||||
$data->markExecutableLineByBranch(
|
||||
$filename,
|
||||
$linesToBranchMap,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-param array<string,list<int>> $linesToBeIgnored
|
||||
*/
|
||||
private function applyIgnoredLinesFilter(RawCodeCoverageData $data, array $linesToBeIgnored): 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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws UnintentionallyCoveredCodeException
|
||||
*/
|
||||
private function addUncoveredFilesFromFilter(): void
|
||||
{
|
||||
$uncoveredFiles = array_diff(
|
||||
$this->filter->files(),
|
||||
$this->data->coveredFiles(),
|
||||
);
|
||||
|
||||
foreach ($uncoveredFiles as $uncoveredFile) {
|
||||
if (is_file($uncoveredFile)) {
|
||||
$this->append(
|
||||
RawCodeCoverageData::fromUncoveredFile(
|
||||
$uncoveredFile,
|
||||
$this->analyser(),
|
||||
),
|
||||
self::UNCOVERED_FILES,
|
||||
linesToBeIgnored: $this->linesToBeIgnored,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ReflectionException
|
||||
* @throws UnintentionallyCoveredCodeException
|
||||
*/
|
||||
private function performUnintentionallyCoveredCodeCheck(RawCodeCoverageData $data, array $linesToBeCovered, array $linesToBeUsed): void
|
||||
{
|
||||
$allowedLines = $this->getAllowedLines(
|
||||
$linesToBeCovered,
|
||||
$linesToBeUsed,
|
||||
);
|
||||
|
||||
$unintentionallyCoveredUnits = [];
|
||||
|
||||
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->processUnintentionallyCoveredUnits($unintentionallyCoveredUnits);
|
||||
|
||||
if (!empty($unintentionallyCoveredUnits)) {
|
||||
throw new UnintentionallyCoveredCodeException(
|
||||
$unintentionallyCoveredUnits,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private function getAllowedLines(array $linesToBeCovered, array $linesToBeUsed): array
|
||||
{
|
||||
$allowedLines = [];
|
||||
|
||||
foreach (array_keys($linesToBeCovered) as $file) {
|
||||
if (!isset($allowedLines[$file])) {
|
||||
$allowedLines[$file] = [];
|
||||
}
|
||||
|
||||
$allowedLines[$file] = array_merge(
|
||||
$allowedLines[$file],
|
||||
$linesToBeCovered[$file],
|
||||
);
|
||||
}
|
||||
|
||||
foreach (array_keys($linesToBeUsed) as $file) {
|
||||
if (!isset($allowedLines[$file])) {
|
||||
$allowedLines[$file] = [];
|
||||
}
|
||||
|
||||
$allowedLines[$file] = array_merge(
|
||||
$allowedLines[$file],
|
||||
$linesToBeUsed[$file],
|
||||
);
|
||||
}
|
||||
|
||||
foreach (array_keys($allowedLines) as $file) {
|
||||
$allowedLines[$file] = array_flip(
|
||||
array_unique($allowedLines[$file]),
|
||||
);
|
||||
}
|
||||
|
||||
return $allowedLines;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $unintentionallyCoveredUnits
|
||||
*
|
||||
* @throws ReflectionException
|
||||
*
|
||||
* @return list<string>
|
||||
*/
|
||||
private function processUnintentionallyCoveredUnits(array $unintentionallyCoveredUnits): array
|
||||
{
|
||||
$unintentionallyCoveredUnits = array_unique($unintentionallyCoveredUnits);
|
||||
$processed = [];
|
||||
|
||||
foreach ($unintentionallyCoveredUnits as $unintentionallyCoveredUnit) {
|
||||
$tmp = explode('::', $unintentionallyCoveredUnit);
|
||||
|
||||
if (count($tmp) !== 2) {
|
||||
$processed[] = $unintentionallyCoveredUnit;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$class = new ReflectionClass($tmp[0]);
|
||||
|
||||
foreach ($this->parentClassesExcludedFromUnintentionallyCoveredCodeCheck as $parentClass) {
|
||||
if ($class->isSubclassOf($parentClass)) {
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
} catch (\ReflectionException $e) {
|
||||
throw new ReflectionException(
|
||||
$e->getMessage(),
|
||||
$e->getCode(),
|
||||
$e,
|
||||
);
|
||||
}
|
||||
|
||||
$processed[] = $tmp[0];
|
||||
}
|
||||
|
||||
$processed = array_unique($processed);
|
||||
|
||||
sort($processed);
|
||||
|
||||
return $processed;
|
||||
}
|
||||
|
||||
private function analyser(): FileAnalyser
|
||||
{
|
||||
if ($this->analyser !== null) {
|
||||
return $this->analyser;
|
||||
}
|
||||
|
||||
$this->analyser = new ParsingFileAnalyser(
|
||||
$this->useAnnotationsForIgnoringCode,
|
||||
$this->ignoreDeprecatedCode,
|
||||
);
|
||||
|
||||
if ($this->cachesStaticAnalysis()) {
|
||||
$this->analyser = new CachingFileAnalyser(
|
||||
$this->cacheDirectory,
|
||||
$this->analyser,
|
||||
$this->useAnnotationsForIgnoringCode,
|
||||
$this->ignoreDeprecatedCode,
|
||||
);
|
||||
}
|
||||
|
||||
return $this->analyser;
|
||||
}
|
||||
}
|
||||
Vendored
+278
@@ -0,0 +1,278 @@
|
||||
<?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\Data;
|
||||
|
||||
use function array_key_exists;
|
||||
use function array_keys;
|
||||
use function array_merge;
|
||||
use function array_unique;
|
||||
use function count;
|
||||
use function is_array;
|
||||
use function ksort;
|
||||
use SebastianBergmann\CodeCoverage\Driver\Driver;
|
||||
|
||||
/**
|
||||
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
|
||||
*
|
||||
* @psalm-import-type XdebugFunctionCoverageType from \SebastianBergmann\CodeCoverage\Driver\XdebugDriver
|
||||
*
|
||||
* @psalm-type TestIdType = string
|
||||
*/
|
||||
final class ProcessedCodeCoverageData
|
||||
{
|
||||
/**
|
||||
* Line coverage data.
|
||||
* An array of filenames, each having an array of linenumbers, each executable line having an array of testcase ids.
|
||||
*
|
||||
* @psalm-var array<string, array<int, null|list<TestIdType>>>
|
||||
*/
|
||||
private array $lineCoverage = [];
|
||||
|
||||
/**
|
||||
* Function coverage data.
|
||||
* Maintains base format of raw data (@see https://xdebug.org/docs/code_coverage), but each 'hit' entry is an array
|
||||
* of testcase ids.
|
||||
*
|
||||
* @psalm-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>
|
||||
* }>>
|
||||
*/
|
||||
private array $functionCoverage = [];
|
||||
|
||||
public function initializeUnseenData(RawCodeCoverageData $rawData): void
|
||||
{
|
||||
foreach ($rawData->lineCoverage() as $file => $lines) {
|
||||
if (!isset($this->lineCoverage[$file])) {
|
||||
$this->lineCoverage[$file] = [];
|
||||
|
||||
foreach ($lines as $k => $v) {
|
||||
$this->lineCoverage[$file][$k] = $v === Driver::LINE_NOT_EXECUTABLE ? null : [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($rawData->functionCoverage() as $file => $functions) {
|
||||
foreach ($functions as $functionName => $functionData) {
|
||||
if (isset($this->functionCoverage[$file][$functionName])) {
|
||||
$this->initPreviouslySeenFunction($file, $functionName, $functionData);
|
||||
} else {
|
||||
$this->initPreviouslyUnseenFunction($file, $functionName, $functionData);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function markCodeAsExecutedByTestCase(string $testCaseId, RawCodeCoverageData $executedCode): void
|
||||
{
|
||||
foreach ($executedCode->lineCoverage() as $file => $lines) {
|
||||
foreach ($lines as $k => $v) {
|
||||
if ($v === Driver::LINE_EXECUTED) {
|
||||
$this->lineCoverage[$file][$k][] = $testCaseId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($executedCode->functionCoverage() as $file => $functions) {
|
||||
foreach ($functions as $functionName => $functionData) {
|
||||
foreach ($functionData['branches'] as $branchId => $branchData) {
|
||||
if ($branchData['hit'] === Driver::BRANCH_HIT) {
|
||||
$this->functionCoverage[$file][$functionName]['branches'][$branchId]['hit'][] = $testCaseId;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($functionData['paths'] as $pathId => $pathData) {
|
||||
if ($pathData['hit'] === Driver::BRANCH_HIT) {
|
||||
$this->functionCoverage[$file][$functionName]['paths'][$pathId]['hit'][] = $testCaseId;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function setLineCoverage(array $lineCoverage): void
|
||||
{
|
||||
$this->lineCoverage = $lineCoverage;
|
||||
}
|
||||
|
||||
public function lineCoverage(): array
|
||||
{
|
||||
ksort($this->lineCoverage);
|
||||
|
||||
return $this->lineCoverage;
|
||||
}
|
||||
|
||||
public function setFunctionCoverage(array $functionCoverage): void
|
||||
{
|
||||
$this->functionCoverage = $functionCoverage;
|
||||
}
|
||||
|
||||
public function functionCoverage(): array
|
||||
{
|
||||
ksort($this->functionCoverage);
|
||||
|
||||
return $this->functionCoverage;
|
||||
}
|
||||
|
||||
public function coveredFiles(): array
|
||||
{
|
||||
ksort($this->lineCoverage);
|
||||
|
||||
return array_keys($this->lineCoverage);
|
||||
}
|
||||
|
||||
public function renameFile(string $oldFile, string $newFile): void
|
||||
{
|
||||
$this->lineCoverage[$newFile] = $this->lineCoverage[$oldFile];
|
||||
|
||||
if (isset($this->functionCoverage[$oldFile])) {
|
||||
$this->functionCoverage[$newFile] = $this->functionCoverage[$oldFile];
|
||||
}
|
||||
|
||||
unset($this->lineCoverage[$oldFile], $this->functionCoverage[$oldFile]);
|
||||
}
|
||||
|
||||
public function merge(self $newData): void
|
||||
{
|
||||
foreach ($newData->lineCoverage as $file => $lines) {
|
||||
if (!isset($this->lineCoverage[$file])) {
|
||||
$this->lineCoverage[$file] = $lines;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// we should compare the lines if any of two contains data
|
||||
$compareLineNumbers = array_unique(
|
||||
array_merge(
|
||||
array_keys($this->lineCoverage[$file]),
|
||||
array_keys($newData->lineCoverage[$file]),
|
||||
),
|
||||
);
|
||||
|
||||
foreach ($compareLineNumbers as $line) {
|
||||
$thatPriority = $this->priorityForLine($newData->lineCoverage[$file], $line);
|
||||
$thisPriority = $this->priorityForLine($this->lineCoverage[$file], $line);
|
||||
|
||||
if ($thatPriority > $thisPriority) {
|
||||
$this->lineCoverage[$file][$line] = $newData->lineCoverage[$file][$line];
|
||||
} elseif ($thatPriority === $thisPriority && is_array($this->lineCoverage[$file][$line])) {
|
||||
$this->lineCoverage[$file][$line] = array_unique(
|
||||
array_merge($this->lineCoverage[$file][$line], $newData->lineCoverage[$file][$line]),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($newData->functionCoverage as $file => $functions) {
|
||||
if (!isset($this->functionCoverage[$file])) {
|
||||
$this->functionCoverage[$file] = $functions;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($functions as $functionName => $functionData) {
|
||||
if (isset($this->functionCoverage[$file][$functionName])) {
|
||||
$this->initPreviouslySeenFunction($file, $functionName, $functionData);
|
||||
} else {
|
||||
$this->initPreviouslyUnseenFunction($file, $functionName, $functionData);
|
||||
}
|
||||
|
||||
foreach ($functionData['branches'] as $branchId => $branchData) {
|
||||
$this->functionCoverage[$file][$functionName]['branches'][$branchId]['hit'] = array_unique(array_merge($this->functionCoverage[$file][$functionName]['branches'][$branchId]['hit'], $branchData['hit']));
|
||||
}
|
||||
|
||||
foreach ($functionData['paths'] as $pathId => $pathData) {
|
||||
$this->functionCoverage[$file][$functionName]['paths'][$pathId]['hit'] = array_unique(array_merge($this->functionCoverage[$file][$functionName]['paths'][$pathId]['hit'], $pathData['hit']));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the priority for a line.
|
||||
*
|
||||
* 1 = the line is not set
|
||||
* 2 = the line has not been tested
|
||||
* 3 = the line is dead code
|
||||
* 4 = the line has been tested
|
||||
*
|
||||
* During a merge, a higher number is better.
|
||||
*/
|
||||
private function priorityForLine(array $data, int $line): int
|
||||
{
|
||||
if (!array_key_exists($line, $data)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (is_array($data[$line]) && count($data[$line]) === 0) {
|
||||
return 2;
|
||||
}
|
||||
|
||||
if ($data[$line] === null) {
|
||||
return 3;
|
||||
}
|
||||
|
||||
return 4;
|
||||
}
|
||||
|
||||
/**
|
||||
* For a function we have never seen before, copy all data over and simply init the 'hit' array.
|
||||
*
|
||||
* @psalm-param XdebugFunctionCoverageType $functionData
|
||||
*/
|
||||
private function initPreviouslyUnseenFunction(string $file, string $functionName, array $functionData): void
|
||||
{
|
||||
$this->functionCoverage[$file][$functionName] = $functionData;
|
||||
|
||||
foreach (array_keys($functionData['branches']) as $branchId) {
|
||||
$this->functionCoverage[$file][$functionName]['branches'][$branchId]['hit'] = [];
|
||||
}
|
||||
|
||||
foreach (array_keys($functionData['paths']) as $pathId) {
|
||||
$this->functionCoverage[$file][$functionName]['paths'][$pathId]['hit'] = [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* For a function we have seen before, only copy over and init the 'hit' array for any unseen branches and paths.
|
||||
* 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.
|
||||
*
|
||||
* @psalm-param XdebugFunctionCoverageType $functionData
|
||||
*/
|
||||
private function initPreviouslySeenFunction(string $file, string $functionName, array $functionData): void
|
||||
{
|
||||
foreach ($functionData['branches'] as $branchId => $branchData) {
|
||||
if (!isset($this->functionCoverage[$file][$functionName]['branches'][$branchId])) {
|
||||
$this->functionCoverage[$file][$functionName]['branches'][$branchId] = $branchData;
|
||||
$this->functionCoverage[$file][$functionName]['branches'][$branchId]['hit'] = [];
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($functionData['paths'] as $pathId => $pathData) {
|
||||
if (!isset($this->functionCoverage[$file][$functionName]['paths'][$pathId])) {
|
||||
$this->functionCoverage[$file][$functionName]['paths'][$pathId] = $pathData;
|
||||
$this->functionCoverage[$file][$functionName]['paths'][$pathId]['hit'] = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+281
@@ -0,0 +1,281 @@
|
||||
<?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\Data;
|
||||
|
||||
use function array_diff;
|
||||
use function array_diff_key;
|
||||
use function array_flip;
|
||||
use function array_intersect;
|
||||
use function array_intersect_key;
|
||||
use function count;
|
||||
use function explode;
|
||||
use function file_get_contents;
|
||||
use function in_array;
|
||||
use function is_file;
|
||||
use function preg_replace;
|
||||
use function range;
|
||||
use function str_ends_with;
|
||||
use function str_starts_with;
|
||||
use function trim;
|
||||
use SebastianBergmann\CodeCoverage\Driver\Driver;
|
||||
use SebastianBergmann\CodeCoverage\StaticAnalysis\FileAnalyser;
|
||||
|
||||
/**
|
||||
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
|
||||
*
|
||||
* @psalm-import-type XdebugFunctionsCoverageType from \SebastianBergmann\CodeCoverage\Driver\XdebugDriver
|
||||
* @psalm-import-type XdebugCodeCoverageWithoutPathCoverageType from \SebastianBergmann\CodeCoverage\Driver\XdebugDriver
|
||||
* @psalm-import-type XdebugCodeCoverageWithPathCoverageType from \SebastianBergmann\CodeCoverage\Driver\XdebugDriver
|
||||
*/
|
||||
final class RawCodeCoverageData
|
||||
{
|
||||
/**
|
||||
* @var array<string, array<int>>
|
||||
*/
|
||||
private static array $emptyLineCache = [];
|
||||
|
||||
/**
|
||||
* @psalm-var XdebugCodeCoverageWithoutPathCoverageType
|
||||
*/
|
||||
private array $lineCoverage;
|
||||
|
||||
/**
|
||||
* @psalm-var array<string, XdebugFunctionsCoverageType>
|
||||
*/
|
||||
private array $functionCoverage;
|
||||
|
||||
/**
|
||||
* @psalm-param XdebugCodeCoverageWithoutPathCoverageType $rawCoverage
|
||||
*/
|
||||
public static function fromXdebugWithoutPathCoverage(array $rawCoverage): self
|
||||
{
|
||||
return new self($rawCoverage, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-param XdebugCodeCoverageWithPathCoverageType $rawCoverage
|
||||
*/
|
||||
public static function fromXdebugWithPathCoverage(array $rawCoverage): self
|
||||
{
|
||||
$lineCoverage = [];
|
||||
$functionCoverage = [];
|
||||
|
||||
foreach ($rawCoverage as $file => $fileCoverageData) {
|
||||
// Xdebug annotates the function name of traits, strip that off
|
||||
foreach ($fileCoverageData['functions'] as $existingKey => $data) {
|
||||
if (str_ends_with($existingKey, '}') && !str_starts_with($existingKey, '{')) { // don't want to catch {main}
|
||||
$newKey = preg_replace('/\{.*}$/', '', $existingKey);
|
||||
$fileCoverageData['functions'][$newKey] = $data;
|
||||
unset($fileCoverageData['functions'][$existingKey]);
|
||||
}
|
||||
}
|
||||
|
||||
$lineCoverage[$file] = $fileCoverageData['lines'];
|
||||
$functionCoverage[$file] = $fileCoverageData['functions'];
|
||||
}
|
||||
|
||||
return new self($lineCoverage, $functionCoverage);
|
||||
}
|
||||
|
||||
public static function fromUncoveredFile(string $filename, FileAnalyser $analyser): self
|
||||
{
|
||||
$lineCoverage = [];
|
||||
|
||||
foreach ($analyser->executableLinesIn($filename) as $line => $branch) {
|
||||
$lineCoverage[$line] = Driver::LINE_NOT_EXECUTED;
|
||||
}
|
||||
|
||||
return new self([$filename => $lineCoverage], []);
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-param XdebugCodeCoverageWithoutPathCoverageType $lineCoverage
|
||||
* @psalm-param array<string, XdebugFunctionsCoverageType> $functionCoverage
|
||||
*/
|
||||
private function __construct(array $lineCoverage, array $functionCoverage)
|
||||
{
|
||||
$this->lineCoverage = $lineCoverage;
|
||||
$this->functionCoverage = $functionCoverage;
|
||||
|
||||
$this->skipEmptyLines();
|
||||
}
|
||||
|
||||
public function clear(): void
|
||||
{
|
||||
$this->lineCoverage = $this->functionCoverage = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-return XdebugCodeCoverageWithoutPathCoverageType
|
||||
*/
|
||||
public function lineCoverage(): array
|
||||
{
|
||||
return $this->lineCoverage;
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-return array<string, XdebugFunctionsCoverageType>
|
||||
*/
|
||||
public function functionCoverage(): array
|
||||
{
|
||||
return $this->functionCoverage;
|
||||
}
|
||||
|
||||
public function removeCoverageDataForFile(string $filename): void
|
||||
{
|
||||
unset($this->lineCoverage[$filename], $this->functionCoverage[$filename]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int[] $lines
|
||||
*/
|
||||
public function keepLineCoverageDataOnlyForLines(string $filename, array $lines): void
|
||||
{
|
||||
if (!isset($this->lineCoverage[$filename])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->lineCoverage[$filename] = array_intersect_key(
|
||||
$this->lineCoverage[$filename],
|
||||
array_flip($lines),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int[] $linesToBranchMap
|
||||
*/
|
||||
public function markExecutableLineByBranch(string $filename, array $linesToBranchMap): void
|
||||
{
|
||||
if (!isset($this->lineCoverage[$filename])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$linesByBranch = [];
|
||||
|
||||
foreach ($linesToBranchMap as $line => $branch) {
|
||||
$linesByBranch[$branch][] = $line;
|
||||
}
|
||||
|
||||
foreach ($this->lineCoverage[$filename] as $line => $lineStatus) {
|
||||
if (!isset($linesToBranchMap[$line])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$branch = $linesToBranchMap[$line];
|
||||
|
||||
if (!isset($linesByBranch[$branch])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($linesByBranch[$branch] as $lineInBranch) {
|
||||
$this->lineCoverage[$filename][$lineInBranch] = $lineStatus;
|
||||
}
|
||||
|
||||
if (Driver::LINE_EXECUTED === $lineStatus) {
|
||||
unset($linesByBranch[$branch]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int[] $lines
|
||||
*/
|
||||
public function keepFunctionCoverageDataOnlyForLines(string $filename, array $lines): void
|
||||
{
|
||||
if (!isset($this->functionCoverage[$filename])) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($this->functionCoverage[$filename] as $functionName => $functionData) {
|
||||
foreach ($functionData['branches'] as $branchId => $branch) {
|
||||
if (count(array_diff(range($branch['line_start'], $branch['line_end']), $lines)) > 0) {
|
||||
unset($this->functionCoverage[$filename][$functionName]['branches'][$branchId]);
|
||||
|
||||
foreach ($functionData['paths'] as $pathId => $path) {
|
||||
if (in_array($branchId, $path['path'], true)) {
|
||||
unset($this->functionCoverage[$filename][$functionName]['paths'][$pathId]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int[] $lines
|
||||
*/
|
||||
public function removeCoverageDataForLines(string $filename, array $lines): void
|
||||
{
|
||||
if (empty($lines)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isset($this->lineCoverage[$filename])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->lineCoverage[$filename] = array_diff_key(
|
||||
$this->lineCoverage[$filename],
|
||||
array_flip($lines),
|
||||
);
|
||||
|
||||
if (isset($this->functionCoverage[$filename])) {
|
||||
foreach ($this->functionCoverage[$filename] as $functionName => $functionData) {
|
||||
foreach ($functionData['branches'] as $branchId => $branch) {
|
||||
if (count(array_intersect($lines, range($branch['line_start'], $branch['line_end']))) > 0) {
|
||||
unset($this->functionCoverage[$filename][$functionName]['branches'][$branchId]);
|
||||
|
||||
foreach ($functionData['paths'] as $pathId => $path) {
|
||||
if (in_array($branchId, $path['path'], true)) {
|
||||
unset($this->functionCoverage[$filename][$functionName]['paths'][$pathId]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* At the end of a file, the PHP interpreter always sees an implicit return. Where this occurs in a file that has
|
||||
* e.g. a class definition, that line cannot be invoked from a test and results in confusing coverage. This engine
|
||||
* implementation detail therefore needs to be masked which is done here by simply ensuring that all empty lines
|
||||
* are skipped over for coverage purposes.
|
||||
*
|
||||
* @see https://github.com/sebastianbergmann/php-code-coverage/issues/799
|
||||
*/
|
||||
private function skipEmptyLines(): void
|
||||
{
|
||||
foreach ($this->lineCoverage as $filename => $coverage) {
|
||||
foreach ($this->getEmptyLinesForFile($filename) as $emptyLine) {
|
||||
unset($this->lineCoverage[$filename][$emptyLine]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function getEmptyLinesForFile(string $filename): array
|
||||
{
|
||||
if (!isset(self::$emptyLineCache[$filename])) {
|
||||
self::$emptyLineCache[$filename] = [];
|
||||
|
||||
if (is_file($filename)) {
|
||||
$sourceLines = explode("\n", file_get_contents($filename));
|
||||
|
||||
foreach ($sourceLines as $line => $source) {
|
||||
if (trim($source) === '') {
|
||||
self::$emptyLineCache[$filename][] = ($line + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return self::$emptyLineCache[$filename];
|
||||
}
|
||||
}
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
<?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\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
|
||||
*/
|
||||
abstract class Driver
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
*
|
||||
* @see http://xdebug.org/docs/code_coverage
|
||||
*/
|
||||
public const LINE_NOT_EXECUTABLE = -2;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*
|
||||
* @see http://xdebug.org/docs/code_coverage
|
||||
*/
|
||||
public const LINE_NOT_EXECUTED = -1;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*
|
||||
* @see http://xdebug.org/docs/code_coverage
|
||||
*/
|
||||
public const LINE_EXECUTED = 1;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*
|
||||
* @see http://xdebug.org/docs/code_coverage
|
||||
*/
|
||||
public const BRANCH_NOT_HIT = 0;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*
|
||||
* @see http://xdebug.org/docs/code_coverage
|
||||
*/
|
||||
public const BRANCH_HIT = 1;
|
||||
private bool $collectBranchAndPathCoverage = false;
|
||||
private bool $detectDeadCode = false;
|
||||
|
||||
public function canCollectBranchAndPathCoverage(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function collectsBranchAndPathCoverage(): bool
|
||||
{
|
||||
return $this->collectBranchAndPathCoverage;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BranchAndPathCoverageNotSupportedException
|
||||
*/
|
||||
public function enableBranchAndPathCoverage(): void
|
||||
{
|
||||
if (!$this->canCollectBranchAndPathCoverage()) {
|
||||
throw new BranchAndPathCoverageNotSupportedException(
|
||||
sprintf(
|
||||
'%s does not support branch and path coverage',
|
||||
$this->nameAndVersion(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
$this->collectBranchAndPathCoverage = true;
|
||||
}
|
||||
|
||||
public function disableBranchAndPathCoverage(): void
|
||||
{
|
||||
$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;
|
||||
|
||||
abstract public function stop(): RawCodeCoverageData;
|
||||
}
|
||||
+162
@@ -0,0 +1,162 @@
|
||||
<?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\Driver;
|
||||
|
||||
use const XDEBUG_CC_BRANCH_CHECK;
|
||||
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;
|
||||
use function xdebug_info;
|
||||
use function xdebug_set_filter;
|
||||
use function xdebug_start_code_coverage;
|
||||
use function xdebug_stop_code_coverage;
|
||||
use SebastianBergmann\CodeCoverage\Data\RawCodeCoverageData;
|
||||
use SebastianBergmann\CodeCoverage\Filter;
|
||||
|
||||
/**
|
||||
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
|
||||
*
|
||||
* @see https://xdebug.org/docs/code_coverage#xdebug_get_code_coverage
|
||||
*
|
||||
* @psalm-type XdebugLinesCoverageType = array<int, int>
|
||||
* @psalm-type XdebugBranchCoverageType = array{
|
||||
* op_start: int,
|
||||
* op_end: int,
|
||||
* line_start: int,
|
||||
* line_end: int,
|
||||
* hit: int,
|
||||
* out: array<int, int>,
|
||||
* out_hit: array<int, int>,
|
||||
* }
|
||||
* @psalm-type XdebugPathCoverageType = array{
|
||||
* path: array<int, int>,
|
||||
* hit: int,
|
||||
* }
|
||||
* @psalm-type XdebugFunctionCoverageType = array{
|
||||
* branches: array<int, XdebugBranchCoverageType>,
|
||||
* paths: array<int, XdebugPathCoverageType>,
|
||||
* }
|
||||
* @psalm-type XdebugFunctionsCoverageType = array<string, XdebugFunctionCoverageType>
|
||||
* @psalm-type XdebugPathAndBranchesCoverageType = array{
|
||||
* lines: XdebugLinesCoverageType,
|
||||
* functions: XdebugFunctionsCoverageType,
|
||||
* }
|
||||
* @psalm-type XdebugCodeCoverageWithoutPathCoverageType = array<string, XdebugLinesCoverageType>
|
||||
* @psalm-type XdebugCodeCoverageWithPathCoverageType = array<string, XdebugPathAndBranchesCoverageType>
|
||||
*/
|
||||
final class XdebugDriver extends Driver
|
||||
{
|
||||
/**
|
||||
* @throws XdebugNotAvailableException
|
||||
* @throws XdebugNotEnabledException
|
||||
*/
|
||||
public function __construct(Filter $filter)
|
||||
{
|
||||
$this->ensureXdebugIsAvailable();
|
||||
$this->ensureXdebugCodeCoverageFeatureIsEnabled();
|
||||
|
||||
if (!$filter->isEmpty()) {
|
||||
xdebug_set_filter(
|
||||
XDEBUG_FILTER_CODE_COVERAGE,
|
||||
XDEBUG_PATH_INCLUDE,
|
||||
$filter->files(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function canCollectBranchAndPathCoverage(): bool
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
if ($this->collectsBranchAndPathCoverage()) {
|
||||
$flags |= XDEBUG_CC_BRANCH_CHECK;
|
||||
}
|
||||
|
||||
xdebug_start_code_coverage($flags);
|
||||
}
|
||||
|
||||
public function stop(): RawCodeCoverageData
|
||||
{
|
||||
$data = xdebug_get_code_coverage();
|
||||
|
||||
xdebug_stop_code_coverage();
|
||||
|
||||
if ($this->collectsBranchAndPathCoverage()) {
|
||||
/* @var XdebugCodeCoverageWithPathCoverageType $data */
|
||||
return RawCodeCoverageData::fromXdebugWithPathCoverage($data);
|
||||
}
|
||||
|
||||
/* @var XdebugCodeCoverageWithoutPathCoverageType $data */
|
||||
return RawCodeCoverageData::fromXdebugWithoutPathCoverage($data);
|
||||
}
|
||||
|
||||
public function nameAndVersion(): string
|
||||
{
|
||||
return 'Xdebug ' . phpversion('xdebug');
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws XdebugNotAvailableException
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
$mode = getenv('XDEBUG_MODE');
|
||||
|
||||
if ($mode === false || $mode === '') {
|
||||
$mode = ini_get('xdebug.mode');
|
||||
}
|
||||
|
||||
if ($mode === false ||
|
||||
!in_array('coverage', explode(',', $mode), true)) {
|
||||
throw new XdebugNotEnabledException;
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
<?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\Driver;
|
||||
|
||||
use RuntimeException;
|
||||
use SebastianBergmann\CodeCoverage\Exception;
|
||||
|
||||
final class XdebugNotEnabledException extends RuntimeException implements Exception
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('XDEBUG_MODE=coverage (environment variable) or xdebug.mode=coverage (PHP configuration setting) has to be set');
|
||||
}
|
||||
}
|
||||
+250
@@ -0,0 +1,250 @@
|
||||
<?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\Node;
|
||||
|
||||
use const DIRECTORY_SEPARATOR;
|
||||
use function array_merge;
|
||||
use function str_ends_with;
|
||||
use function str_replace;
|
||||
use function substr;
|
||||
use Countable;
|
||||
use SebastianBergmann\CodeCoverage\Util\Percentage;
|
||||
|
||||
/**
|
||||
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
|
||||
*
|
||||
* @psalm-import-type LinesOfCodeType from \SebastianBergmann\CodeCoverage\StaticAnalysis\FileAnalyser
|
||||
* @psalm-import-type ProcessedFunctionType from \SebastianBergmann\CodeCoverage\Node\File
|
||||
* @psalm-import-type ProcessedClassType from \SebastianBergmann\CodeCoverage\Node\File
|
||||
* @psalm-import-type ProcessedTraitType from \SebastianBergmann\CodeCoverage\Node\File
|
||||
*/
|
||||
abstract class AbstractNode implements Countable
|
||||
{
|
||||
private readonly string $name;
|
||||
private string $pathAsString;
|
||||
private array $pathAsArray;
|
||||
private readonly ?AbstractNode $parent;
|
||||
private string $id;
|
||||
|
||||
public function __construct(string $name, ?self $parent = null)
|
||||
{
|
||||
if (str_ends_with($name, DIRECTORY_SEPARATOR)) {
|
||||
$name = substr($name, 0, -1);
|
||||
}
|
||||
|
||||
$this->name = $name;
|
||||
$this->parent = $parent;
|
||||
|
||||
$this->processId();
|
||||
$this->processPath();
|
||||
}
|
||||
|
||||
public function name(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function id(): string
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function pathAsString(): string
|
||||
{
|
||||
return $this->pathAsString;
|
||||
}
|
||||
|
||||
public function pathAsArray(): array
|
||||
{
|
||||
return $this->pathAsArray;
|
||||
}
|
||||
|
||||
public function parent(): ?self
|
||||
{
|
||||
return $this->parent;
|
||||
}
|
||||
|
||||
public function percentageOfTestedClasses(): Percentage
|
||||
{
|
||||
return Percentage::fromFractionAndTotal(
|
||||
$this->numberOfTestedClasses(),
|
||||
$this->numberOfClasses(),
|
||||
);
|
||||
}
|
||||
|
||||
public function percentageOfTestedTraits(): Percentage
|
||||
{
|
||||
return Percentage::fromFractionAndTotal(
|
||||
$this->numberOfTestedTraits(),
|
||||
$this->numberOfTraits(),
|
||||
);
|
||||
}
|
||||
|
||||
public function percentageOfTestedClassesAndTraits(): Percentage
|
||||
{
|
||||
return Percentage::fromFractionAndTotal(
|
||||
$this->numberOfTestedClassesAndTraits(),
|
||||
$this->numberOfClassesAndTraits(),
|
||||
);
|
||||
}
|
||||
|
||||
public function percentageOfTestedFunctions(): Percentage
|
||||
{
|
||||
return Percentage::fromFractionAndTotal(
|
||||
$this->numberOfTestedFunctions(),
|
||||
$this->numberOfFunctions(),
|
||||
);
|
||||
}
|
||||
|
||||
public function percentageOfTestedMethods(): Percentage
|
||||
{
|
||||
return Percentage::fromFractionAndTotal(
|
||||
$this->numberOfTestedMethods(),
|
||||
$this->numberOfMethods(),
|
||||
);
|
||||
}
|
||||
|
||||
public function percentageOfTestedFunctionsAndMethods(): Percentage
|
||||
{
|
||||
return Percentage::fromFractionAndTotal(
|
||||
$this->numberOfTestedFunctionsAndMethods(),
|
||||
$this->numberOfFunctionsAndMethods(),
|
||||
);
|
||||
}
|
||||
|
||||
public function percentageOfExecutedLines(): Percentage
|
||||
{
|
||||
return Percentage::fromFractionAndTotal(
|
||||
$this->numberOfExecutedLines(),
|
||||
$this->numberOfExecutableLines(),
|
||||
);
|
||||
}
|
||||
|
||||
public function percentageOfExecutedBranches(): Percentage
|
||||
{
|
||||
return Percentage::fromFractionAndTotal(
|
||||
$this->numberOfExecutedBranches(),
|
||||
$this->numberOfExecutableBranches(),
|
||||
);
|
||||
}
|
||||
|
||||
public function percentageOfExecutedPaths(): Percentage
|
||||
{
|
||||
return Percentage::fromFractionAndTotal(
|
||||
$this->numberOfExecutedPaths(),
|
||||
$this->numberOfExecutablePaths(),
|
||||
);
|
||||
}
|
||||
|
||||
public function numberOfClassesAndTraits(): int
|
||||
{
|
||||
return $this->numberOfClasses() + $this->numberOfTraits();
|
||||
}
|
||||
|
||||
public function numberOfTestedClassesAndTraits(): int
|
||||
{
|
||||
return $this->numberOfTestedClasses() + $this->numberOfTestedTraits();
|
||||
}
|
||||
|
||||
public function classesAndTraits(): array
|
||||
{
|
||||
return array_merge($this->classes(), $this->traits());
|
||||
}
|
||||
|
||||
public function numberOfFunctionsAndMethods(): int
|
||||
{
|
||||
return $this->numberOfFunctions() + $this->numberOfMethods();
|
||||
}
|
||||
|
||||
public function numberOfTestedFunctionsAndMethods(): int
|
||||
{
|
||||
return $this->numberOfTestedFunctions() + $this->numberOfTestedMethods();
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-return array<string, ProcessedClassType>
|
||||
*/
|
||||
abstract public function classes(): array;
|
||||
|
||||
/**
|
||||
* @psalm-return array<string, ProcessedTraitType>
|
||||
*/
|
||||
abstract public function traits(): array;
|
||||
|
||||
/**
|
||||
* @psalm-return array<string, ProcessedFunctionType>
|
||||
*/
|
||||
abstract public function functions(): array;
|
||||
|
||||
/**
|
||||
* @psalm-return LinesOfCodeType
|
||||
*/
|
||||
abstract public function linesOfCode(): array;
|
||||
|
||||
abstract public function numberOfExecutableLines(): int;
|
||||
|
||||
abstract public function numberOfExecutedLines(): int;
|
||||
|
||||
abstract public function numberOfExecutableBranches(): int;
|
||||
|
||||
abstract public function numberOfExecutedBranches(): int;
|
||||
|
||||
abstract public function numberOfExecutablePaths(): int;
|
||||
|
||||
abstract public function numberOfExecutedPaths(): int;
|
||||
|
||||
abstract public function numberOfClasses(): int;
|
||||
|
||||
abstract public function numberOfTestedClasses(): int;
|
||||
|
||||
abstract public function numberOfTraits(): int;
|
||||
|
||||
abstract public function numberOfTestedTraits(): int;
|
||||
|
||||
abstract public function numberOfMethods(): int;
|
||||
|
||||
abstract public function numberOfTestedMethods(): int;
|
||||
|
||||
abstract public function numberOfFunctions(): int;
|
||||
|
||||
abstract public function numberOfTestedFunctions(): int;
|
||||
|
||||
private function processId(): void
|
||||
{
|
||||
if ($this->parent === null) {
|
||||
$this->id = 'index';
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$parentId = $this->parent->id();
|
||||
|
||||
if ($parentId === 'index') {
|
||||
$this->id = str_replace(':', '_', $this->name);
|
||||
} else {
|
||||
$this->id = $parentId . '/' . $this->name;
|
||||
}
|
||||
}
|
||||
|
||||
private function processPath(): void
|
||||
{
|
||||
if ($this->parent === null) {
|
||||
$this->pathAsArray = [$this];
|
||||
$this->pathAsString = $this->name;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->pathAsArray = $this->parent->pathAsArray();
|
||||
$this->pathAsString = $this->parent->pathAsString() . DIRECTORY_SEPARATOR . $this->name;
|
||||
|
||||
$this->pathAsArray[] = $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
<?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\Node;
|
||||
|
||||
use const DIRECTORY_SEPARATOR;
|
||||
use function array_shift;
|
||||
use function basename;
|
||||
use function count;
|
||||
use function dirname;
|
||||
use function explode;
|
||||
use function implode;
|
||||
use function is_file;
|
||||
use function str_ends_with;
|
||||
use function str_replace;
|
||||
use function str_starts_with;
|
||||
use function substr;
|
||||
use SebastianBergmann\CodeCoverage\CodeCoverage;
|
||||
use SebastianBergmann\CodeCoverage\Data\ProcessedCodeCoverageData;
|
||||
use SebastianBergmann\CodeCoverage\StaticAnalysis\FileAnalyser;
|
||||
|
||||
/**
|
||||
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
|
||||
*
|
||||
* @psalm-import-type TestType from \SebastianBergmann\CodeCoverage\CodeCoverage
|
||||
*/
|
||||
final class Builder
|
||||
{
|
||||
private readonly FileAnalyser $analyser;
|
||||
|
||||
public function __construct(FileAnalyser $analyser)
|
||||
{
|
||||
$this->analyser = $analyser;
|
||||
}
|
||||
|
||||
public function build(CodeCoverage $coverage): Directory
|
||||
{
|
||||
$data = clone $coverage->getData(); // clone because path munging is destructive to the original data
|
||||
$commonPath = $this->reducePaths($data);
|
||||
$root = new Directory(
|
||||
$commonPath,
|
||||
null,
|
||||
);
|
||||
|
||||
$this->addItems(
|
||||
$root,
|
||||
$this->buildDirectoryStructure($data),
|
||||
$coverage->getTests(),
|
||||
);
|
||||
|
||||
return $root;
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-param array<string, TestType> $tests
|
||||
*/
|
||||
private function addItems(Directory $root, array $items, array $tests): void
|
||||
{
|
||||
foreach ($items as $key => $value) {
|
||||
$key = (string) $key;
|
||||
|
||||
if (str_ends_with($key, '/f')) {
|
||||
$key = substr($key, 0, -2);
|
||||
$filename = $root->pathAsString() . DIRECTORY_SEPARATOR . $key;
|
||||
|
||||
if (is_file($filename)) {
|
||||
$root->addFile(
|
||||
new File(
|
||||
$key,
|
||||
$root,
|
||||
$value['lineCoverage'],
|
||||
$value['functionCoverage'],
|
||||
$tests,
|
||||
$this->analyser->classesIn($filename),
|
||||
$this->analyser->traitsIn($filename),
|
||||
$this->analyser->functionsIn($filename),
|
||||
$this->analyser->linesOfCodeFor($filename),
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$child = $root->addDirectory($key);
|
||||
|
||||
$this->addItems($child, $value, $tests);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds an array representation of the directory structure.
|
||||
*
|
||||
* For instance,
|
||||
*
|
||||
* <code>
|
||||
* Array
|
||||
* (
|
||||
* [Money.php] => Array
|
||||
* (
|
||||
* ...
|
||||
* )
|
||||
*
|
||||
* [MoneyBag.php] => Array
|
||||
* (
|
||||
* ...
|
||||
* )
|
||||
* )
|
||||
* </code>
|
||||
*
|
||||
* is transformed into
|
||||
*
|
||||
* <code>
|
||||
* Array
|
||||
* (
|
||||
* [.] => Array
|
||||
* (
|
||||
* [Money.php] => Array
|
||||
* (
|
||||
* ...
|
||||
* )
|
||||
*
|
||||
* [MoneyBag.php] => Array
|
||||
* (
|
||||
* ...
|
||||
* )
|
||||
* )
|
||||
* )
|
||||
* </code>
|
||||
*
|
||||
* @psalm-return array<string, array<string, array{lineCoverage: array<int, int>, functionCoverage: array<string, array<int, int>>}>>
|
||||
*/
|
||||
private function buildDirectoryStructure(ProcessedCodeCoverageData $data): array
|
||||
{
|
||||
$result = [];
|
||||
|
||||
foreach ($data->coveredFiles() as $originalPath) {
|
||||
$path = explode(DIRECTORY_SEPARATOR, $originalPath);
|
||||
$pointer = &$result;
|
||||
$max = count($path);
|
||||
|
||||
for ($i = 0; $i < $max; $i++) {
|
||||
$type = '';
|
||||
|
||||
if ($i === ($max - 1)) {
|
||||
$type = '/f';
|
||||
}
|
||||
|
||||
$pointer = &$pointer[$path[$i] . $type];
|
||||
}
|
||||
|
||||
$pointer = [
|
||||
'lineCoverage' => $data->lineCoverage()[$originalPath] ?? [],
|
||||
'functionCoverage' => $data->functionCoverage()[$originalPath] ?? [],
|
||||
];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reduces the paths by cutting the longest common start path.
|
||||
*
|
||||
* For instance,
|
||||
*
|
||||
* <code>
|
||||
* Array
|
||||
* (
|
||||
* [/home/sb/Money/Money.php] => Array
|
||||
* (
|
||||
* ...
|
||||
* )
|
||||
*
|
||||
* [/home/sb/Money/MoneyBag.php] => Array
|
||||
* (
|
||||
* ...
|
||||
* )
|
||||
* )
|
||||
* </code>
|
||||
*
|
||||
* is reduced to
|
||||
*
|
||||
* <code>
|
||||
* Array
|
||||
* (
|
||||
* [Money.php] => Array
|
||||
* (
|
||||
* ...
|
||||
* )
|
||||
*
|
||||
* [MoneyBag.php] => Array
|
||||
* (
|
||||
* ...
|
||||
* )
|
||||
* )
|
||||
* </code>
|
||||
*/
|
||||
private function reducePaths(ProcessedCodeCoverageData $coverage): string
|
||||
{
|
||||
if (empty($coverage->coveredFiles())) {
|
||||
return '.';
|
||||
}
|
||||
|
||||
$commonPath = '';
|
||||
$paths = $coverage->coveredFiles();
|
||||
|
||||
if (count($paths) === 1) {
|
||||
$commonPath = dirname($paths[0]) . DIRECTORY_SEPARATOR;
|
||||
$coverage->renameFile($paths[0], basename($paths[0]));
|
||||
|
||||
return $commonPath;
|
||||
}
|
||||
|
||||
$max = count($paths);
|
||||
|
||||
for ($i = 0; $i < $max; $i++) {
|
||||
// strip phar:// prefixes
|
||||
if (str_starts_with($paths[$i], 'phar://')) {
|
||||
$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])) {
|
||||
$paths[$i][0] = DIRECTORY_SEPARATOR;
|
||||
}
|
||||
}
|
||||
|
||||
$done = false;
|
||||
$max = count($paths);
|
||||
|
||||
while (!$done) {
|
||||
for ($i = 0; $i < $max - 1; $i++) {
|
||||
if (!isset($paths[$i][0]) ||
|
||||
!isset($paths[$i + 1][0]) ||
|
||||
$paths[$i][0] !== $paths[$i + 1][0]) {
|
||||
$done = true;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$done) {
|
||||
$commonPath .= $paths[0][0];
|
||||
|
||||
if ($paths[0][0] !== DIRECTORY_SEPARATOR) {
|
||||
$commonPath .= DIRECTORY_SEPARATOR;
|
||||
}
|
||||
|
||||
for ($i = 0; $i < $max; $i++) {
|
||||
array_shift($paths[$i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$original = $coverage->coveredFiles();
|
||||
$max = count($original);
|
||||
|
||||
for ($i = 0; $i < $max; $i++) {
|
||||
$coverage->renameFile($original[$i], implode(DIRECTORY_SEPARATOR, $paths[$i]));
|
||||
}
|
||||
|
||||
return substr($commonPath, 0, -1);
|
||||
}
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
<?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\Node;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
/**
|
||||
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
|
||||
*/
|
||||
final class CrapIndex
|
||||
{
|
||||
private readonly int $cyclomaticComplexity;
|
||||
private readonly float $codeCoverage;
|
||||
|
||||
public function __construct(int $cyclomaticComplexity, float $codeCoverage)
|
||||
{
|
||||
$this->cyclomaticComplexity = $cyclomaticComplexity;
|
||||
$this->codeCoverage = $codeCoverage;
|
||||
}
|
||||
|
||||
public function asString(): string
|
||||
{
|
||||
if ($this->codeCoverage === 0.0) {
|
||||
return (string) ($this->cyclomaticComplexity ** 2 + $this->cyclomaticComplexity);
|
||||
}
|
||||
|
||||
if ($this->codeCoverage >= 95) {
|
||||
return (string) $this->cyclomaticComplexity;
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
'%01.2F',
|
||||
$this->cyclomaticComplexity ** 2 * (1 - $this->codeCoverage / 100) ** 3 + $this->cyclomaticComplexity,
|
||||
);
|
||||
}
|
||||
}
|
||||
+370
@@ -0,0 +1,370 @@
|
||||
<?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\Node;
|
||||
|
||||
use function array_merge;
|
||||
use function count;
|
||||
use IteratorAggregate;
|
||||
use RecursiveIteratorIterator;
|
||||
|
||||
/**
|
||||
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
|
||||
*
|
||||
* @psalm-import-type LinesOfCodeType from \SebastianBergmann\CodeCoverage\StaticAnalysis\FileAnalyser
|
||||
*/
|
||||
final class Directory extends AbstractNode implements IteratorAggregate
|
||||
{
|
||||
/**
|
||||
* @var list<AbstractNode>
|
||||
*/
|
||||
private array $children = [];
|
||||
|
||||
/**
|
||||
* @var list<Directory>
|
||||
*/
|
||||
private array $directories = [];
|
||||
|
||||
/**
|
||||
* @var list<File>
|
||||
*/
|
||||
private array $files = [];
|
||||
private ?array $classes = null;
|
||||
private ?array $traits = null;
|
||||
private ?array $functions = null;
|
||||
|
||||
/**
|
||||
* @psalm-var null|LinesOfCodeType
|
||||
*/
|
||||
private ?array $linesOfCode = null;
|
||||
private int $numFiles = -1;
|
||||
private int $numExecutableLines = -1;
|
||||
private int $numExecutedLines = -1;
|
||||
private int $numExecutableBranches = -1;
|
||||
private int $numExecutedBranches = -1;
|
||||
private int $numExecutablePaths = -1;
|
||||
private int $numExecutedPaths = -1;
|
||||
private int $numClasses = -1;
|
||||
private int $numTestedClasses = -1;
|
||||
private int $numTraits = -1;
|
||||
private int $numTestedTraits = -1;
|
||||
private int $numMethods = -1;
|
||||
private int $numTestedMethods = -1;
|
||||
private int $numFunctions = -1;
|
||||
private int $numTestedFunctions = -1;
|
||||
|
||||
public function count(): int
|
||||
{
|
||||
if ($this->numFiles === -1) {
|
||||
$this->numFiles = 0;
|
||||
|
||||
foreach ($this->children as $child) {
|
||||
$this->numFiles += count($child);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->numFiles;
|
||||
}
|
||||
|
||||
public function getIterator(): RecursiveIteratorIterator
|
||||
{
|
||||
return new RecursiveIteratorIterator(
|
||||
new Iterator($this),
|
||||
RecursiveIteratorIterator::SELF_FIRST,
|
||||
);
|
||||
}
|
||||
|
||||
public function addDirectory(string $name): self
|
||||
{
|
||||
$directory = new self($name, $this);
|
||||
|
||||
$this->children[] = $directory;
|
||||
$this->directories[] = &$this->children[count($this->children) - 1];
|
||||
|
||||
return $directory;
|
||||
}
|
||||
|
||||
public function addFile(File $file): void
|
||||
{
|
||||
$this->children[] = $file;
|
||||
$this->files[] = &$this->children[count($this->children) - 1];
|
||||
|
||||
$this->numExecutableLines = -1;
|
||||
$this->numExecutedLines = -1;
|
||||
}
|
||||
|
||||
public function directories(): array
|
||||
{
|
||||
return $this->directories;
|
||||
}
|
||||
|
||||
public function files(): array
|
||||
{
|
||||
return $this->files;
|
||||
}
|
||||
|
||||
public function children(): array
|
||||
{
|
||||
return $this->children;
|
||||
}
|
||||
|
||||
public function classes(): array
|
||||
{
|
||||
if ($this->classes === null) {
|
||||
$this->classes = [];
|
||||
|
||||
foreach ($this->children as $child) {
|
||||
$this->classes = array_merge(
|
||||
$this->classes,
|
||||
$child->classes(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->classes;
|
||||
}
|
||||
|
||||
public function traits(): array
|
||||
{
|
||||
if ($this->traits === null) {
|
||||
$this->traits = [];
|
||||
|
||||
foreach ($this->children as $child) {
|
||||
$this->traits = array_merge(
|
||||
$this->traits,
|
||||
$child->traits(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->traits;
|
||||
}
|
||||
|
||||
public function functions(): array
|
||||
{
|
||||
if ($this->functions === null) {
|
||||
$this->functions = [];
|
||||
|
||||
foreach ($this->children as $child) {
|
||||
$this->functions = array_merge(
|
||||
$this->functions,
|
||||
$child->functions(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->functions;
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-return LinesOfCodeType
|
||||
*/
|
||||
public function linesOfCode(): array
|
||||
{
|
||||
if ($this->linesOfCode === null) {
|
||||
$this->linesOfCode = [
|
||||
'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'];
|
||||
}
|
||||
}
|
||||
|
||||
return $this->linesOfCode;
|
||||
}
|
||||
|
||||
public function numberOfExecutableLines(): int
|
||||
{
|
||||
if ($this->numExecutableLines === -1) {
|
||||
$this->numExecutableLines = 0;
|
||||
|
||||
foreach ($this->children as $child) {
|
||||
$this->numExecutableLines += $child->numberOfExecutableLines();
|
||||
}
|
||||
}
|
||||
|
||||
return $this->numExecutableLines;
|
||||
}
|
||||
|
||||
public function numberOfExecutedLines(): int
|
||||
{
|
||||
if ($this->numExecutedLines === -1) {
|
||||
$this->numExecutedLines = 0;
|
||||
|
||||
foreach ($this->children as $child) {
|
||||
$this->numExecutedLines += $child->numberOfExecutedLines();
|
||||
}
|
||||
}
|
||||
|
||||
return $this->numExecutedLines;
|
||||
}
|
||||
|
||||
public function numberOfExecutableBranches(): int
|
||||
{
|
||||
if ($this->numExecutableBranches === -1) {
|
||||
$this->numExecutableBranches = 0;
|
||||
|
||||
foreach ($this->children as $child) {
|
||||
$this->numExecutableBranches += $child->numberOfExecutableBranches();
|
||||
}
|
||||
}
|
||||
|
||||
return $this->numExecutableBranches;
|
||||
}
|
||||
|
||||
public function numberOfExecutedBranches(): int
|
||||
{
|
||||
if ($this->numExecutedBranches === -1) {
|
||||
$this->numExecutedBranches = 0;
|
||||
|
||||
foreach ($this->children as $child) {
|
||||
$this->numExecutedBranches += $child->numberOfExecutedBranches();
|
||||
}
|
||||
}
|
||||
|
||||
return $this->numExecutedBranches;
|
||||
}
|
||||
|
||||
public function numberOfExecutablePaths(): int
|
||||
{
|
||||
if ($this->numExecutablePaths === -1) {
|
||||
$this->numExecutablePaths = 0;
|
||||
|
||||
foreach ($this->children as $child) {
|
||||
$this->numExecutablePaths += $child->numberOfExecutablePaths();
|
||||
}
|
||||
}
|
||||
|
||||
return $this->numExecutablePaths;
|
||||
}
|
||||
|
||||
public function numberOfExecutedPaths(): int
|
||||
{
|
||||
if ($this->numExecutedPaths === -1) {
|
||||
$this->numExecutedPaths = 0;
|
||||
|
||||
foreach ($this->children as $child) {
|
||||
$this->numExecutedPaths += $child->numberOfExecutedPaths();
|
||||
}
|
||||
}
|
||||
|
||||
return $this->numExecutedPaths;
|
||||
}
|
||||
|
||||
public function numberOfClasses(): int
|
||||
{
|
||||
if ($this->numClasses === -1) {
|
||||
$this->numClasses = 0;
|
||||
|
||||
foreach ($this->children as $child) {
|
||||
$this->numClasses += $child->numberOfClasses();
|
||||
}
|
||||
}
|
||||
|
||||
return $this->numClasses;
|
||||
}
|
||||
|
||||
public function numberOfTestedClasses(): int
|
||||
{
|
||||
if ($this->numTestedClasses === -1) {
|
||||
$this->numTestedClasses = 0;
|
||||
|
||||
foreach ($this->children as $child) {
|
||||
$this->numTestedClasses += $child->numberOfTestedClasses();
|
||||
}
|
||||
}
|
||||
|
||||
return $this->numTestedClasses;
|
||||
}
|
||||
|
||||
public function numberOfTraits(): int
|
||||
{
|
||||
if ($this->numTraits === -1) {
|
||||
$this->numTraits = 0;
|
||||
|
||||
foreach ($this->children as $child) {
|
||||
$this->numTraits += $child->numberOfTraits();
|
||||
}
|
||||
}
|
||||
|
||||
return $this->numTraits;
|
||||
}
|
||||
|
||||
public function numberOfTestedTraits(): int
|
||||
{
|
||||
if ($this->numTestedTraits === -1) {
|
||||
$this->numTestedTraits = 0;
|
||||
|
||||
foreach ($this->children as $child) {
|
||||
$this->numTestedTraits += $child->numberOfTestedTraits();
|
||||
}
|
||||
}
|
||||
|
||||
return $this->numTestedTraits;
|
||||
}
|
||||
|
||||
public function numberOfMethods(): int
|
||||
{
|
||||
if ($this->numMethods === -1) {
|
||||
$this->numMethods = 0;
|
||||
|
||||
foreach ($this->children as $child) {
|
||||
$this->numMethods += $child->numberOfMethods();
|
||||
}
|
||||
}
|
||||
|
||||
return $this->numMethods;
|
||||
}
|
||||
|
||||
public function numberOfTestedMethods(): int
|
||||
{
|
||||
if ($this->numTestedMethods === -1) {
|
||||
$this->numTestedMethods = 0;
|
||||
|
||||
foreach ($this->children as $child) {
|
||||
$this->numTestedMethods += $child->numberOfTestedMethods();
|
||||
}
|
||||
}
|
||||
|
||||
return $this->numTestedMethods;
|
||||
}
|
||||
|
||||
public function numberOfFunctions(): int
|
||||
{
|
||||
if ($this->numFunctions === -1) {
|
||||
$this->numFunctions = 0;
|
||||
|
||||
foreach ($this->children as $child) {
|
||||
$this->numFunctions += $child->numberOfFunctions();
|
||||
}
|
||||
}
|
||||
|
||||
return $this->numFunctions;
|
||||
}
|
||||
|
||||
public function numberOfTestedFunctions(): int
|
||||
{
|
||||
if ($this->numTestedFunctions === -1) {
|
||||
$this->numTestedFunctions = 0;
|
||||
|
||||
foreach ($this->children as $child) {
|
||||
$this->numTestedFunctions += $child->numberOfTestedFunctions();
|
||||
}
|
||||
}
|
||||
|
||||
return $this->numTestedFunctions;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,688 @@
|
||||
<?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\Node;
|
||||
|
||||
use function array_filter;
|
||||
use function count;
|
||||
use function range;
|
||||
|
||||
/**
|
||||
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
|
||||
*
|
||||
* @psalm-import-type CodeUnitFunctionType from \SebastianBergmann\CodeCoverage\StaticAnalysis\CodeUnitFindingVisitor
|
||||
* @psalm-import-type CodeUnitMethodType from \SebastianBergmann\CodeCoverage\StaticAnalysis\CodeUnitFindingVisitor
|
||||
* @psalm-import-type CodeUnitClassType from \SebastianBergmann\CodeCoverage\StaticAnalysis\CodeUnitFindingVisitor
|
||||
* @psalm-import-type CodeUnitTraitType from \SebastianBergmann\CodeCoverage\StaticAnalysis\CodeUnitFindingVisitor
|
||||
* @psalm-import-type LinesOfCodeType from \SebastianBergmann\CodeCoverage\StaticAnalysis\FileAnalyser
|
||||
* @psalm-import-type LinesType from \SebastianBergmann\CodeCoverage\StaticAnalysis\FileAnalyser
|
||||
*
|
||||
* @psalm-type ProcessedFunctionType = array{
|
||||
* functionName: string,
|
||||
* namespace: string,
|
||||
* signature: string,
|
||||
* startLine: int,
|
||||
* endLine: int,
|
||||
* executableLines: int,
|
||||
* executedLines: int,
|
||||
* executableBranches: int,
|
||||
* executedBranches: int,
|
||||
* executablePaths: int,
|
||||
* executedPaths: int,
|
||||
* ccn: int,
|
||||
* coverage: int|float,
|
||||
* crap: int|string,
|
||||
* link: string
|
||||
* }
|
||||
* @psalm-type ProcessedMethodType = array{
|
||||
* methodName: string,
|
||||
* visibility: string,
|
||||
* signature: string,
|
||||
* startLine: int,
|
||||
* endLine: int,
|
||||
* executableLines: int,
|
||||
* executedLines: int,
|
||||
* executableBranches: int,
|
||||
* executedBranches: int,
|
||||
* executablePaths: int,
|
||||
* executedPaths: int,
|
||||
* ccn: int,
|
||||
* coverage: float|int,
|
||||
* crap: int|string,
|
||||
* link: string
|
||||
* }
|
||||
* @psalm-type ProcessedClassType = array{
|
||||
* className: string,
|
||||
* namespace: string,
|
||||
* methods: array<string, ProcessedMethodType>,
|
||||
* startLine: int,
|
||||
* executableLines: int,
|
||||
* executedLines: int,
|
||||
* executableBranches: int,
|
||||
* executedBranches: int,
|
||||
* executablePaths: int,
|
||||
* executedPaths: int,
|
||||
* ccn: int,
|
||||
* coverage: int|float,
|
||||
* crap: int|string,
|
||||
* link: string
|
||||
* }
|
||||
* @psalm-type ProcessedTraitType = array{
|
||||
* traitName: string,
|
||||
* namespace: string,
|
||||
* methods: array<string, ProcessedMethodType>,
|
||||
* startLine: int,
|
||||
* executableLines: int,
|
||||
* executedLines: int,
|
||||
* executableBranches: int,
|
||||
* executedBranches: int,
|
||||
* executablePaths: int,
|
||||
* executedPaths: int,
|
||||
* ccn: int,
|
||||
* coverage: float|int,
|
||||
* crap: int|string,
|
||||
* link: string
|
||||
* }
|
||||
*/
|
||||
final class File extends AbstractNode
|
||||
{
|
||||
/**
|
||||
* @psalm-var array<int, ?list<non-empty-string>>
|
||||
*/
|
||||
private array $lineCoverageData;
|
||||
private array $functionCoverageData;
|
||||
private readonly array $testData;
|
||||
private int $numExecutableLines = 0;
|
||||
private int $numExecutedLines = 0;
|
||||
private int $numExecutableBranches = 0;
|
||||
private int $numExecutedBranches = 0;
|
||||
private int $numExecutablePaths = 0;
|
||||
private int $numExecutedPaths = 0;
|
||||
|
||||
/**
|
||||
* @psalm-var array<string, ProcessedClassType>
|
||||
*/
|
||||
private array $classes = [];
|
||||
|
||||
/**
|
||||
* @psalm-var array<string, ProcessedTraitType>
|
||||
*/
|
||||
private array $traits = [];
|
||||
|
||||
/**
|
||||
* @psalm-var array<string, ProcessedFunctionType>
|
||||
*/
|
||||
private array $functions = [];
|
||||
|
||||
/**
|
||||
* @psalm-var LinesOfCodeType
|
||||
*/
|
||||
private readonly array $linesOfCode;
|
||||
private ?int $numClasses = null;
|
||||
private int $numTestedClasses = 0;
|
||||
private ?int $numTraits = null;
|
||||
private int $numTestedTraits = 0;
|
||||
private ?int $numMethods = null;
|
||||
private ?int $numTestedMethods = null;
|
||||
private ?int $numTestedFunctions = null;
|
||||
|
||||
/**
|
||||
* @var array<int, array|array{0: CodeUnitClassType, 1: string}|array{0: CodeUnitFunctionType}|array{0: CodeUnitTraitType, 1: string}>
|
||||
*/
|
||||
private array $codeUnitsByLine = [];
|
||||
|
||||
/**
|
||||
* @psalm-param array<int, ?list<non-empty-string>> $lineCoverageData
|
||||
* @psalm-param LinesOfCodeType $linesOfCode
|
||||
* @psalm-param array<string, CodeUnitClassType> $classes
|
||||
* @psalm-param array<string, CodeUnitTraitType> $traits
|
||||
* @psalm-param array<string, CodeUnitFunctionType> $functions
|
||||
*/
|
||||
public function __construct(string $name, AbstractNode $parent, array $lineCoverageData, array $functionCoverageData, array $testData, array $classes, array $traits, array $functions, array $linesOfCode)
|
||||
{
|
||||
parent::__construct($name, $parent);
|
||||
|
||||
$this->lineCoverageData = $lineCoverageData;
|
||||
$this->functionCoverageData = $functionCoverageData;
|
||||
$this->testData = $testData;
|
||||
$this->linesOfCode = $linesOfCode;
|
||||
|
||||
$this->calculateStatistics($classes, $traits, $functions);
|
||||
}
|
||||
|
||||
public function count(): int
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-return array<int, ?list<non-empty-string>>
|
||||
*/
|
||||
public function lineCoverageData(): array
|
||||
{
|
||||
return $this->lineCoverageData;
|
||||
}
|
||||
|
||||
public function functionCoverageData(): array
|
||||
{
|
||||
return $this->functionCoverageData;
|
||||
}
|
||||
|
||||
public function testData(): array
|
||||
{
|
||||
return $this->testData;
|
||||
}
|
||||
|
||||
public function classes(): array
|
||||
{
|
||||
return $this->classes;
|
||||
}
|
||||
|
||||
public function traits(): array
|
||||
{
|
||||
return $this->traits;
|
||||
}
|
||||
|
||||
public function functions(): array
|
||||
{
|
||||
return $this->functions;
|
||||
}
|
||||
|
||||
public function linesOfCode(): array
|
||||
{
|
||||
return $this->linesOfCode;
|
||||
}
|
||||
|
||||
public function numberOfExecutableLines(): int
|
||||
{
|
||||
return $this->numExecutableLines;
|
||||
}
|
||||
|
||||
public function numberOfExecutedLines(): int
|
||||
{
|
||||
return $this->numExecutedLines;
|
||||
}
|
||||
|
||||
public function numberOfExecutableBranches(): int
|
||||
{
|
||||
return $this->numExecutableBranches;
|
||||
}
|
||||
|
||||
public function numberOfExecutedBranches(): int
|
||||
{
|
||||
return $this->numExecutedBranches;
|
||||
}
|
||||
|
||||
public function numberOfExecutablePaths(): int
|
||||
{
|
||||
return $this->numExecutablePaths;
|
||||
}
|
||||
|
||||
public function numberOfExecutedPaths(): int
|
||||
{
|
||||
return $this->numExecutedPaths;
|
||||
}
|
||||
|
||||
public function numberOfClasses(): int
|
||||
{
|
||||
if ($this->numClasses === null) {
|
||||
$this->numClasses = 0;
|
||||
|
||||
foreach ($this->classes as $class) {
|
||||
foreach ($class['methods'] as $method) {
|
||||
if ($method['executableLines'] > 0) {
|
||||
$this->numClasses++;
|
||||
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->numClasses;
|
||||
}
|
||||
|
||||
public function numberOfTestedClasses(): int
|
||||
{
|
||||
return $this->numTestedClasses;
|
||||
}
|
||||
|
||||
public function numberOfTraits(): int
|
||||
{
|
||||
if ($this->numTraits === null) {
|
||||
$this->numTraits = 0;
|
||||
|
||||
foreach ($this->traits as $trait) {
|
||||
foreach ($trait['methods'] as $method) {
|
||||
if ($method['executableLines'] > 0) {
|
||||
$this->numTraits++;
|
||||
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->numTraits;
|
||||
}
|
||||
|
||||
public function numberOfTestedTraits(): int
|
||||
{
|
||||
return $this->numTestedTraits;
|
||||
}
|
||||
|
||||
public function numberOfMethods(): int
|
||||
{
|
||||
if ($this->numMethods === null) {
|
||||
$this->numMethods = 0;
|
||||
|
||||
foreach ($this->classes as $class) {
|
||||
foreach ($class['methods'] as $method) {
|
||||
if ($method['executableLines'] > 0) {
|
||||
$this->numMethods++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($this->traits as $trait) {
|
||||
foreach ($trait['methods'] as $method) {
|
||||
if ($method['executableLines'] > 0) {
|
||||
$this->numMethods++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->numMethods;
|
||||
}
|
||||
|
||||
public function numberOfTestedMethods(): int
|
||||
{
|
||||
if ($this->numTestedMethods === null) {
|
||||
$this->numTestedMethods = 0;
|
||||
|
||||
foreach ($this->classes as $class) {
|
||||
foreach ($class['methods'] as $method) {
|
||||
if ($method['executableLines'] > 0 &&
|
||||
$method['coverage'] === 100) {
|
||||
$this->numTestedMethods++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($this->traits as $trait) {
|
||||
foreach ($trait['methods'] as $method) {
|
||||
if ($method['executableLines'] > 0 &&
|
||||
$method['coverage'] === 100) {
|
||||
$this->numTestedMethods++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->numTestedMethods;
|
||||
}
|
||||
|
||||
public function numberOfFunctions(): int
|
||||
{
|
||||
return count($this->functions);
|
||||
}
|
||||
|
||||
public function numberOfTestedFunctions(): int
|
||||
{
|
||||
if ($this->numTestedFunctions === null) {
|
||||
$this->numTestedFunctions = 0;
|
||||
|
||||
foreach ($this->functions as $function) {
|
||||
if ($function['executableLines'] > 0 &&
|
||||
$function['coverage'] === 100) {
|
||||
$this->numTestedFunctions++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->numTestedFunctions;
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-param array<string, CodeUnitClassType> $classes
|
||||
* @psalm-param array<string, CodeUnitTraitType> $traits
|
||||
* @psalm-param array<string, CodeUnitFunctionType> $functions
|
||||
*/
|
||||
private function calculateStatistics(array $classes, array $traits, array $functions): void
|
||||
{
|
||||
foreach (range(1, $this->linesOfCode['linesOfCode']) as $lineNumber) {
|
||||
$this->codeUnitsByLine[$lineNumber] = [];
|
||||
}
|
||||
|
||||
$this->processClasses($classes);
|
||||
$this->processTraits($traits);
|
||||
$this->processFunctions($functions);
|
||||
|
||||
foreach (range(1, $this->linesOfCode['linesOfCode']) as $lineNumber) {
|
||||
if (isset($this->lineCoverageData[$lineNumber])) {
|
||||
foreach ($this->codeUnitsByLine[$lineNumber] as &$codeUnit) {
|
||||
$codeUnit['executableLines']++;
|
||||
}
|
||||
|
||||
unset($codeUnit);
|
||||
|
||||
$this->numExecutableLines++;
|
||||
|
||||
if (count($this->lineCoverageData[$lineNumber]) > 0) {
|
||||
foreach ($this->codeUnitsByLine[$lineNumber] as &$codeUnit) {
|
||||
$codeUnit['executedLines']++;
|
||||
}
|
||||
|
||||
unset($codeUnit);
|
||||
|
||||
$this->numExecutedLines++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
$method['coverage'] = $methodBranchCoverage ?: $methodLineCoverage;
|
||||
$method['crap'] = (new CrapIndex($method['ccn'], $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;
|
||||
|
||||
$trait['coverage'] = $traitBranchCoverage ?: $traitLineCoverage;
|
||||
$trait['crap'] = (new CrapIndex($trait['ccn'], $traitPathCoverage ?: $traitLineCoverage))->asString();
|
||||
|
||||
if ($trait['executableLines'] > 0 && $trait['coverage'] === 100) {
|
||||
$this->numTestedClasses++;
|
||||
}
|
||||
}
|
||||
|
||||
unset($trait);
|
||||
|
||||
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;
|
||||
|
||||
$method['coverage'] = $methodBranchCoverage ?: $methodLineCoverage;
|
||||
$method['crap'] = (new CrapIndex($method['ccn'], $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;
|
||||
|
||||
$class['coverage'] = $classBranchCoverage ?: $classLineCoverage;
|
||||
$class['crap'] = (new CrapIndex($class['ccn'], $classPathCoverage ?: $classLineCoverage))->asString();
|
||||
|
||||
if ($class['executableLines'] > 0 && $class['coverage'] === 100) {
|
||||
$this->numTestedClasses++;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
$function['coverage'] = $functionBranchCoverage ?: $functionLineCoverage;
|
||||
$function['crap'] = (new CrapIndex($function['ccn'], $functionPathCoverage ?: $functionLineCoverage))->asString();
|
||||
|
||||
if ($function['coverage'] === 100) {
|
||||
$this->numTestedFunctions++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-param array<string, CodeUnitClassType> $classes
|
||||
*/
|
||||
private function processClasses(array $classes): void
|
||||
{
|
||||
$link = $this->id() . '.html#';
|
||||
|
||||
foreach ($classes as $className => $class) {
|
||||
$this->classes[$className] = [
|
||||
'className' => $className,
|
||||
'namespace' => $class['namespace'],
|
||||
'methods' => [],
|
||||
'startLine' => $class['startLine'],
|
||||
'executableLines' => 0,
|
||||
'executedLines' => 0,
|
||||
'executableBranches' => 0,
|
||||
'executedBranches' => 0,
|
||||
'executablePaths' => 0,
|
||||
'executedPaths' => 0,
|
||||
'ccn' => 0,
|
||||
'coverage' => 0,
|
||||
'crap' => 0,
|
||||
'link' => $link . $class['startLine'],
|
||||
];
|
||||
|
||||
foreach ($class['methods'] as $methodName => $method) {
|
||||
$methodData = $this->newMethod($className, $methodName, $method, $link);
|
||||
$this->classes[$className]['methods'][$methodName] = $methodData;
|
||||
|
||||
$this->classes[$className]['executableBranches'] += $methodData['executableBranches'];
|
||||
$this->classes[$className]['executedBranches'] += $methodData['executedBranches'];
|
||||
$this->classes[$className]['executablePaths'] += $methodData['executablePaths'];
|
||||
$this->classes[$className]['executedPaths'] += $methodData['executedPaths'];
|
||||
|
||||
$this->numExecutableBranches += $methodData['executableBranches'];
|
||||
$this->numExecutedBranches += $methodData['executedBranches'];
|
||||
$this->numExecutablePaths += $methodData['executablePaths'];
|
||||
$this->numExecutedPaths += $methodData['executedPaths'];
|
||||
|
||||
foreach (range($method['startLine'], $method['endLine']) as $lineNumber) {
|
||||
$this->codeUnitsByLine[$lineNumber] = [
|
||||
&$this->classes[$className],
|
||||
&$this->classes[$className]['methods'][$methodName],
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-param array<string, CodeUnitTraitType> $traits
|
||||
*/
|
||||
private function processTraits(array $traits): void
|
||||
{
|
||||
$link = $this->id() . '.html#';
|
||||
|
||||
foreach ($traits as $traitName => $trait) {
|
||||
$this->traits[$traitName] = [
|
||||
'traitName' => $traitName,
|
||||
'namespace' => $trait['namespace'],
|
||||
'methods' => [],
|
||||
'startLine' => $trait['startLine'],
|
||||
'executableLines' => 0,
|
||||
'executedLines' => 0,
|
||||
'executableBranches' => 0,
|
||||
'executedBranches' => 0,
|
||||
'executablePaths' => 0,
|
||||
'executedPaths' => 0,
|
||||
'ccn' => 0,
|
||||
'coverage' => 0,
|
||||
'crap' => 0,
|
||||
'link' => $link . $trait['startLine'],
|
||||
];
|
||||
|
||||
foreach ($trait['methods'] as $methodName => $method) {
|
||||
$methodData = $this->newMethod($traitName, $methodName, $method, $link);
|
||||
$this->traits[$traitName]['methods'][$methodName] = $methodData;
|
||||
|
||||
$this->traits[$traitName]['executableBranches'] += $methodData['executableBranches'];
|
||||
$this->traits[$traitName]['executedBranches'] += $methodData['executedBranches'];
|
||||
$this->traits[$traitName]['executablePaths'] += $methodData['executablePaths'];
|
||||
$this->traits[$traitName]['executedPaths'] += $methodData['executedPaths'];
|
||||
|
||||
$this->numExecutableBranches += $methodData['executableBranches'];
|
||||
$this->numExecutedBranches += $methodData['executedBranches'];
|
||||
$this->numExecutablePaths += $methodData['executablePaths'];
|
||||
$this->numExecutedPaths += $methodData['executedPaths'];
|
||||
|
||||
foreach (range($method['startLine'], $method['endLine']) as $lineNumber) {
|
||||
$this->codeUnitsByLine[$lineNumber] = [
|
||||
&$this->traits[$traitName],
|
||||
&$this->traits[$traitName]['methods'][$methodName],
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-param array<string, CodeUnitFunctionType> $functions
|
||||
*/
|
||||
private function processFunctions(array $functions): void
|
||||
{
|
||||
$link = $this->id() . '.html#';
|
||||
|
||||
foreach ($functions as $functionName => $function) {
|
||||
$this->functions[$functionName] = [
|
||||
'functionName' => $functionName,
|
||||
'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'],
|
||||
'coverage' => 0,
|
||||
'crap' => 0,
|
||||
'link' => $link . $function['startLine'],
|
||||
];
|
||||
|
||||
foreach (range($function['startLine'], $function['endLine']) as $lineNumber) {
|
||||
$this->codeUnitsByLine[$lineNumber] = [&$this->functions[$functionName]];
|
||||
}
|
||||
|
||||
if (isset($this->functionCoverageData[$functionName]['branches'])) {
|
||||
$this->functions[$functionName]['executableBranches'] = count(
|
||||
$this->functionCoverageData[$functionName]['branches'],
|
||||
);
|
||||
|
||||
$this->functions[$functionName]['executedBranches'] = count(
|
||||
array_filter(
|
||||
$this->functionCoverageData[$functionName]['branches'],
|
||||
static function (array $branch)
|
||||
{
|
||||
return (bool) $branch['hit'];
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (isset($this->functionCoverageData[$functionName]['paths'])) {
|
||||
$this->functions[$functionName]['executablePaths'] = count(
|
||||
$this->functionCoverageData[$functionName]['paths'],
|
||||
);
|
||||
|
||||
$this->functions[$functionName]['executedPaths'] = count(
|
||||
array_filter(
|
||||
$this->functionCoverageData[$functionName]['paths'],
|
||||
static function (array $path)
|
||||
{
|
||||
return (bool) $path['hit'];
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
$this->numExecutableBranches += $this->functions[$functionName]['executableBranches'];
|
||||
$this->numExecutedBranches += $this->functions[$functionName]['executedBranches'];
|
||||
$this->numExecutablePaths += $this->functions[$functionName]['executablePaths'];
|
||||
$this->numExecutedPaths += $this->functions[$functionName]['executedPaths'];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-param CodeUnitMethodType $method
|
||||
*
|
||||
* @psalm-return ProcessedMethodType
|
||||
*/
|
||||
private function newMethod(string $className, string $methodName, array $method, string $link): array
|
||||
{
|
||||
$methodData = [
|
||||
'methodName' => $methodName,
|
||||
'visibility' => $method['visibility'],
|
||||
'signature' => $method['signature'],
|
||||
'startLine' => $method['startLine'],
|
||||
'endLine' => $method['endLine'],
|
||||
'executableLines' => 0,
|
||||
'executedLines' => 0,
|
||||
'executableBranches' => 0,
|
||||
'executedBranches' => 0,
|
||||
'executablePaths' => 0,
|
||||
'executedPaths' => 0,
|
||||
'ccn' => $method['ccn'],
|
||||
'coverage' => 0,
|
||||
'crap' => 0,
|
||||
'link' => $link . $method['startLine'],
|
||||
];
|
||||
|
||||
$key = $className . '->' . $methodName;
|
||||
|
||||
if (isset($this->functionCoverageData[$key]['branches'])) {
|
||||
$methodData['executableBranches'] = count(
|
||||
$this->functionCoverageData[$key]['branches'],
|
||||
);
|
||||
|
||||
$methodData['executedBranches'] = count(
|
||||
array_filter(
|
||||
$this->functionCoverageData[$key]['branches'],
|
||||
static function (array $branch)
|
||||
{
|
||||
return (bool) $branch['hit'];
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (isset($this->functionCoverageData[$key]['paths'])) {
|
||||
$methodData['executablePaths'] = count(
|
||||
$this->functionCoverageData[$key]['paths'],
|
||||
);
|
||||
|
||||
$methodData['executedPaths'] = count(
|
||||
array_filter(
|
||||
$this->functionCoverageData[$key]['paths'],
|
||||
static function (array $path)
|
||||
{
|
||||
return (bool) $path['hit'];
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return $methodData;
|
||||
}
|
||||
}
|
||||
+258
@@ -0,0 +1,258 @@
|
||||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of phpunit/php-code-coverage.
|
||||
*
|
||||
* (c) Sebastian Bergmann <sebastian@phpunit.de>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
namespace SebastianBergmann\CodeCoverage\Report;
|
||||
|
||||
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;
|
||||
|
||||
final class Clover
|
||||
{
|
||||
/**
|
||||
* @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;
|
||||
|
||||
$xmlCoverage = $xmlDocument->createElement('coverage');
|
||||
$xmlCoverage->setAttribute('generated', $time);
|
||||
$xmlDocument->appendChild($xmlCoverage);
|
||||
|
||||
$xmlProject = $xmlDocument->createElement('project');
|
||||
$xmlProject->setAttribute('timestamp', $time);
|
||||
|
||||
if (is_string($name)) {
|
||||
$xmlProject->setAttribute('name', $name);
|
||||
}
|
||||
|
||||
$xmlCoverage->appendChild($xmlProject);
|
||||
|
||||
$packages = [];
|
||||
$report = $coverage->getReport();
|
||||
|
||||
foreach ($report as $item) {
|
||||
if (!$item instanceof File) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/* @var File $item */
|
||||
|
||||
$xmlFile = $xmlDocument->createElement('file');
|
||||
$xmlFile->setAttribute('name', $item->pathAsString());
|
||||
|
||||
$classes = $item->classesAndTraits();
|
||||
$coverageData = $item->lineCoverageData();
|
||||
$lines = [];
|
||||
$namespace = 'global';
|
||||
|
||||
foreach ($classes as $className => $class) {
|
||||
$classStatements = 0;
|
||||
$coveredClassStatements = 0;
|
||||
$coveredMethods = 0;
|
||||
$classMethods = 0;
|
||||
|
||||
foreach ($class['methods'] as $methodName => $method) {
|
||||
if ($method['executableLines'] == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$classMethods++;
|
||||
$classStatements += $method['executableLines'];
|
||||
$coveredClassStatements += $method['executedLines'];
|
||||
|
||||
if ($method['coverage'] == 100) {
|
||||
$coveredMethods++;
|
||||
}
|
||||
|
||||
$methodCount = 0;
|
||||
|
||||
foreach (range($method['startLine'], $method['endLine']) as $line) {
|
||||
if (isset($coverageData[$line])) {
|
||||
$methodCount = max($methodCount, count($coverageData[$line]));
|
||||
}
|
||||
}
|
||||
|
||||
$lines[$method['startLine']] = [
|
||||
'ccn' => $method['ccn'],
|
||||
'count' => $methodCount,
|
||||
'crap' => $method['crap'],
|
||||
'type' => 'method',
|
||||
'visibility' => $method['visibility'],
|
||||
'name' => $methodName,
|
||||
];
|
||||
}
|
||||
|
||||
if (!empty($class['package']['namespace'])) {
|
||||
$namespace = $class['package']['namespace'];
|
||||
}
|
||||
|
||||
$xmlClass = $xmlDocument->createElement('class');
|
||||
$xmlClass->setAttribute('name', $className);
|
||||
$xmlClass->setAttribute('namespace', $namespace);
|
||||
|
||||
if (!empty($class['package']['fullPackage'])) {
|
||||
$xmlClass->setAttribute(
|
||||
'fullPackage',
|
||||
$class['package']['fullPackage'],
|
||||
);
|
||||
}
|
||||
|
||||
if (!empty($class['package']['category'])) {
|
||||
$xmlClass->setAttribute(
|
||||
'category',
|
||||
$class['package']['category'],
|
||||
);
|
||||
}
|
||||
|
||||
if (!empty($class['package']['package'])) {
|
||||
$xmlClass->setAttribute(
|
||||
'package',
|
||||
$class['package']['package'],
|
||||
);
|
||||
}
|
||||
|
||||
if (!empty($class['package']['subpackage'])) {
|
||||
$xmlClass->setAttribute(
|
||||
'subpackage',
|
||||
$class['package']['subpackage'],
|
||||
);
|
||||
}
|
||||
|
||||
$xmlFile->appendChild($xmlClass);
|
||||
|
||||
$xmlMetrics = $xmlDocument->createElement('metrics');
|
||||
$xmlMetrics->setAttribute('complexity', (string) $class['ccn']);
|
||||
$xmlMetrics->setAttribute('methods', (string) $classMethods);
|
||||
$xmlMetrics->setAttribute('coveredmethods', (string) $coveredMethods);
|
||||
$xmlMetrics->setAttribute('conditionals', (string) $class['executableBranches']);
|
||||
$xmlMetrics->setAttribute('coveredconditionals', (string) $class['executedBranches']);
|
||||
$xmlMetrics->setAttribute('statements', (string) $classStatements);
|
||||
$xmlMetrics->setAttribute('coveredstatements', (string) $coveredClassStatements);
|
||||
$xmlMetrics->setAttribute('elements', (string) ($classMethods + $classStatements + $class['executableBranches']));
|
||||
$xmlMetrics->setAttribute('coveredelements', (string) ($coveredMethods + $coveredClassStatements + $class['executedBranches']));
|
||||
$xmlClass->appendChild($xmlMetrics);
|
||||
}
|
||||
|
||||
foreach ($coverageData as $line => $data) {
|
||||
if ($data === null || isset($lines[$line])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$lines[$line] = [
|
||||
'count' => count($data), 'type' => 'stmt',
|
||||
];
|
||||
}
|
||||
|
||||
ksort($lines);
|
||||
|
||||
foreach ($lines as $line => $data) {
|
||||
$xmlLine = $xmlDocument->createElement('line');
|
||||
$xmlLine->setAttribute('num', (string) $line);
|
||||
$xmlLine->setAttribute('type', $data['type']);
|
||||
|
||||
if (isset($data['name'])) {
|
||||
$xmlLine->setAttribute('name', $data['name']);
|
||||
}
|
||||
|
||||
if (isset($data['visibility'])) {
|
||||
$xmlLine->setAttribute('visibility', $data['visibility']);
|
||||
}
|
||||
|
||||
if (isset($data['ccn'])) {
|
||||
$xmlLine->setAttribute('complexity', (string) $data['ccn']);
|
||||
}
|
||||
|
||||
if (isset($data['crap'])) {
|
||||
$xmlLine->setAttribute('crap', (string) $data['crap']);
|
||||
}
|
||||
|
||||
$xmlLine->setAttribute('count', (string) $data['count']);
|
||||
$xmlFile->appendChild($xmlLine);
|
||||
}
|
||||
|
||||
$linesOfCode = $item->linesOfCode();
|
||||
|
||||
$xmlMetrics = $xmlDocument->createElement('metrics');
|
||||
$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());
|
||||
$xmlMetrics->setAttribute('conditionals', (string) $item->numberOfExecutableBranches());
|
||||
$xmlMetrics->setAttribute('coveredconditionals', (string) $item->numberOfExecutedBranches());
|
||||
$xmlMetrics->setAttribute('statements', (string) $item->numberOfExecutableLines());
|
||||
$xmlMetrics->setAttribute('coveredstatements', (string) $item->numberOfExecutedLines());
|
||||
$xmlMetrics->setAttribute('elements', (string) ($item->numberOfMethods() + $item->numberOfExecutableLines() + $item->numberOfExecutableBranches()));
|
||||
$xmlMetrics->setAttribute('coveredelements', (string) ($item->numberOfTestedMethods() + $item->numberOfExecutedLines() + $item->numberOfExecutedBranches()));
|
||||
$xmlFile->appendChild($xmlMetrics);
|
||||
|
||||
if ($namespace === 'global') {
|
||||
$xmlProject->appendChild($xmlFile);
|
||||
} else {
|
||||
if (!isset($packages[$namespace])) {
|
||||
$packages[$namespace] = $xmlDocument->createElement(
|
||||
'package',
|
||||
);
|
||||
|
||||
$packages[$namespace]->setAttribute('name', $namespace);
|
||||
$xmlProject->appendChild($packages[$namespace]);
|
||||
}
|
||||
|
||||
$packages[$namespace]->appendChild($xmlFile);
|
||||
}
|
||||
}
|
||||
|
||||
$linesOfCode = $report->linesOfCode();
|
||||
|
||||
$xmlMetrics = $xmlDocument->createElement('metrics');
|
||||
$xmlMetrics->setAttribute('files', (string) count($report));
|
||||
$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());
|
||||
$xmlMetrics->setAttribute('conditionals', (string) $report->numberOfExecutableBranches());
|
||||
$xmlMetrics->setAttribute('coveredconditionals', (string) $report->numberOfExecutedBranches());
|
||||
$xmlMetrics->setAttribute('statements', (string) $report->numberOfExecutableLines());
|
||||
$xmlMetrics->setAttribute('coveredstatements', (string) $report->numberOfExecutedLines());
|
||||
$xmlMetrics->setAttribute('elements', (string) ($report->numberOfMethods() + $report->numberOfExecutableLines() + $report->numberOfExecutableBranches()));
|
||||
$xmlMetrics->setAttribute('coveredelements', (string) ($report->numberOfTestedMethods() + $report->numberOfExecutedLines() + $report->numberOfExecutedBranches()));
|
||||
$xmlProject->appendChild($xmlMetrics);
|
||||
|
||||
$buffer = $xmlDocument->saveXML();
|
||||
|
||||
if ($target !== null) {
|
||||
if (!str_contains($target, '://')) {
|
||||
Filesystem::createDirectory(dirname($target));
|
||||
}
|
||||
|
||||
if (@file_put_contents($target, $buffer) === false) {
|
||||
throw new WriteOperationFailedException($target);
|
||||
}
|
||||
}
|
||||
|
||||
return $buffer;
|
||||
}
|
||||
}
|
||||
+309
@@ -0,0 +1,309 @@
|
||||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of phpunit/php-code-coverage.
|
||||
*
|
||||
* (c) Sebastian Bergmann <sebastian@phpunit.de>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
namespace SebastianBergmann\CodeCoverage\Report;
|
||||
|
||||
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;
|
||||
|
||||
final class Cobertura
|
||||
{
|
||||
/**
|
||||
* @throws WriteOperationFailedException
|
||||
*/
|
||||
public function process(CodeCoverage $coverage, ?string $target = null): string
|
||||
{
|
||||
$time = (string) time();
|
||||
|
||||
$report = $coverage->getReport();
|
||||
|
||||
$implementation = new DOMImplementation;
|
||||
|
||||
$documentType = $implementation->createDocumentType(
|
||||
'coverage',
|
||||
'',
|
||||
'http://cobertura.sourceforge.net/xml/coverage-04.dtd',
|
||||
);
|
||||
|
||||
$document = $implementation->createDocument('', '', $documentType);
|
||||
$document->xmlVersion = '1.0';
|
||||
$document->encoding = 'UTF-8';
|
||||
$document->formatOutput = true;
|
||||
|
||||
$coverageElement = $document->createElement('coverage');
|
||||
|
||||
$linesValid = $report->numberOfExecutableLines();
|
||||
$linesCovered = $report->numberOfExecutedLines();
|
||||
$lineRate = $linesValid === 0 ? 0 : ($linesCovered / $linesValid);
|
||||
$coverageElement->setAttribute('line-rate', (string) $lineRate);
|
||||
|
||||
$branchesValid = $report->numberOfExecutableBranches();
|
||||
$branchesCovered = $report->numberOfExecutedBranches();
|
||||
$branchRate = $branchesValid === 0 ? 0 : ($branchesCovered / $branchesValid);
|
||||
$coverageElement->setAttribute('branch-rate', (string) $branchRate);
|
||||
|
||||
$coverageElement->setAttribute('lines-covered', (string) $report->numberOfExecutedLines());
|
||||
$coverageElement->setAttribute('lines-valid', (string) $report->numberOfExecutableLines());
|
||||
$coverageElement->setAttribute('branches-covered', (string) $report->numberOfExecutedBranches());
|
||||
$coverageElement->setAttribute('branches-valid', (string) $report->numberOfExecutableBranches());
|
||||
$coverageElement->setAttribute('complexity', '');
|
||||
$coverageElement->setAttribute('version', '0.4');
|
||||
$coverageElement->setAttribute('timestamp', $time);
|
||||
|
||||
$document->appendChild($coverageElement);
|
||||
|
||||
$sourcesElement = $document->createElement('sources');
|
||||
$coverageElement->appendChild($sourcesElement);
|
||||
|
||||
$sourceElement = $document->createElement('source', $report->pathAsString());
|
||||
$sourcesElement->appendChild($sourceElement);
|
||||
|
||||
$packagesElement = $document->createElement('packages');
|
||||
$coverageElement->appendChild($packagesElement);
|
||||
|
||||
$complexity = 0;
|
||||
|
||||
foreach ($report as $item) {
|
||||
if (!$item instanceof File) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$packageElement = $document->createElement('package');
|
||||
$packageComplexity = 0;
|
||||
|
||||
$packageElement->setAttribute('name', str_replace($report->pathAsString() . DIRECTORY_SEPARATOR, '', $item->pathAsString()));
|
||||
|
||||
$linesValid = $item->numberOfExecutableLines();
|
||||
$linesCovered = $item->numberOfExecutedLines();
|
||||
$lineRate = $linesValid === 0 ? 0 : ($linesCovered / $linesValid);
|
||||
|
||||
$packageElement->setAttribute('line-rate', (string) $lineRate);
|
||||
|
||||
$branchesValid = $item->numberOfExecutableBranches();
|
||||
$branchesCovered = $item->numberOfExecutedBranches();
|
||||
$branchRate = $branchesValid === 0 ? 0 : ($branchesCovered / $branchesValid);
|
||||
|
||||
$packageElement->setAttribute('branch-rate', (string) $branchRate);
|
||||
|
||||
$packageElement->setAttribute('complexity', '');
|
||||
$packagesElement->appendChild($packageElement);
|
||||
|
||||
$classesElement = $document->createElement('classes');
|
||||
|
||||
$packageElement->appendChild($classesElement);
|
||||
|
||||
$classes = $item->classesAndTraits();
|
||||
$coverageData = $item->lineCoverageData();
|
||||
|
||||
foreach ($classes as $className => $class) {
|
||||
$complexity += $class['ccn'];
|
||||
$packageComplexity += $class['ccn'];
|
||||
|
||||
if (!empty($class['package']['namespace'])) {
|
||||
$className = $class['package']['namespace'] . '\\' . $className;
|
||||
}
|
||||
|
||||
$linesValid = $class['executableLines'];
|
||||
$linesCovered = $class['executedLines'];
|
||||
$lineRate = $linesValid === 0 ? 0 : ($linesCovered / $linesValid);
|
||||
|
||||
$branchesValid = $class['executableBranches'];
|
||||
$branchesCovered = $class['executedBranches'];
|
||||
$branchRate = $branchesValid === 0 ? 0 : ($branchesCovered / $branchesValid);
|
||||
|
||||
$classElement = $document->createElement('class');
|
||||
|
||||
$classElement->setAttribute('name', $className);
|
||||
$classElement->setAttribute('filename', str_replace($report->pathAsString() . DIRECTORY_SEPARATOR, '', $item->pathAsString()));
|
||||
$classElement->setAttribute('line-rate', (string) $lineRate);
|
||||
$classElement->setAttribute('branch-rate', (string) $branchRate);
|
||||
$classElement->setAttribute('complexity', (string) $class['ccn']);
|
||||
|
||||
$classesElement->appendChild($classElement);
|
||||
|
||||
$methodsElement = $document->createElement('methods');
|
||||
|
||||
$classElement->appendChild($methodsElement);
|
||||
|
||||
$classLinesElement = $document->createElement('lines');
|
||||
|
||||
$classElement->appendChild($classLinesElement);
|
||||
|
||||
foreach ($class['methods'] as $methodName => $method) {
|
||||
if ($method['executableLines'] === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
preg_match("/\((.*?)\)/", $method['signature'], $signature);
|
||||
|
||||
$linesValid = $method['executableLines'];
|
||||
$linesCovered = $method['executedLines'];
|
||||
$lineRate = $linesValid === 0 ? 0 : ($linesCovered / $linesValid);
|
||||
|
||||
$branchesValid = $method['executableBranches'];
|
||||
$branchesCovered = $method['executedBranches'];
|
||||
$branchRate = $branchesValid === 0 ? 0 : ($branchesCovered / $branchesValid);
|
||||
|
||||
$methodElement = $document->createElement('method');
|
||||
|
||||
$methodElement->setAttribute('name', $methodName);
|
||||
$methodElement->setAttribute('signature', $signature[1]);
|
||||
$methodElement->setAttribute('line-rate', (string) $lineRate);
|
||||
$methodElement->setAttribute('branch-rate', (string) $branchRate);
|
||||
$methodElement->setAttribute('complexity', (string) $method['ccn']);
|
||||
|
||||
$methodLinesElement = $document->createElement('lines');
|
||||
|
||||
$methodElement->appendChild($methodLinesElement);
|
||||
|
||||
foreach (range($method['startLine'], $method['endLine']) as $line) {
|
||||
if (!isset($coverageData[$line])) {
|
||||
continue;
|
||||
}
|
||||
$methodLineElement = $document->createElement('line');
|
||||
|
||||
$methodLineElement->setAttribute('number', (string) $line);
|
||||
$methodLineElement->setAttribute('hits', (string) count($coverageData[$line]));
|
||||
|
||||
$methodLinesElement->appendChild($methodLineElement);
|
||||
|
||||
$classLineElement = $methodLineElement->cloneNode();
|
||||
|
||||
$classLinesElement->appendChild($classLineElement);
|
||||
}
|
||||
|
||||
$methodsElement->appendChild($methodElement);
|
||||
}
|
||||
}
|
||||
|
||||
if ($item->numberOfFunctions() === 0) {
|
||||
$packageElement->setAttribute('complexity', (string) $packageComplexity);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$functionsComplexity = 0;
|
||||
$functionsLinesValid = 0;
|
||||
$functionsLinesCovered = 0;
|
||||
$functionsBranchesValid = 0;
|
||||
$functionsBranchesCovered = 0;
|
||||
|
||||
$classElement = $document->createElement('class');
|
||||
$classElement->setAttribute('name', basename($item->pathAsString()));
|
||||
$classElement->setAttribute('filename', str_replace($report->pathAsString() . DIRECTORY_SEPARATOR, '', $item->pathAsString()));
|
||||
|
||||
$methodsElement = $document->createElement('methods');
|
||||
|
||||
$classElement->appendChild($methodsElement);
|
||||
|
||||
$classLinesElement = $document->createElement('lines');
|
||||
|
||||
$classElement->appendChild($classLinesElement);
|
||||
|
||||
$functions = $item->functions();
|
||||
|
||||
foreach ($functions as $functionName => $function) {
|
||||
if ($function['executableLines'] === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$complexity += $function['ccn'];
|
||||
$packageComplexity += $function['ccn'];
|
||||
$functionsComplexity += $function['ccn'];
|
||||
|
||||
$linesValid = $function['executableLines'];
|
||||
$linesCovered = $function['executedLines'];
|
||||
$lineRate = $linesValid === 0 ? 0 : ($linesCovered / $linesValid);
|
||||
|
||||
$functionsLinesValid += $linesValid;
|
||||
$functionsLinesCovered += $linesCovered;
|
||||
|
||||
$branchesValid = $function['executableBranches'];
|
||||
$branchesCovered = $function['executedBranches'];
|
||||
$branchRate = $branchesValid === 0 ? 0 : ($branchesCovered / $branchesValid);
|
||||
|
||||
$functionsBranchesValid += $branchesValid;
|
||||
$functionsBranchesCovered += $branchesValid;
|
||||
|
||||
$methodElement = $document->createElement('method');
|
||||
|
||||
$methodElement->setAttribute('name', $functionName);
|
||||
$methodElement->setAttribute('signature', $function['signature']);
|
||||
$methodElement->setAttribute('line-rate', (string) $lineRate);
|
||||
$methodElement->setAttribute('branch-rate', (string) $branchRate);
|
||||
$methodElement->setAttribute('complexity', (string) $function['ccn']);
|
||||
|
||||
$methodLinesElement = $document->createElement('lines');
|
||||
|
||||
$methodElement->appendChild($methodLinesElement);
|
||||
|
||||
foreach (range($function['startLine'], $function['endLine']) as $line) {
|
||||
if (!isset($coverageData[$line])) {
|
||||
continue;
|
||||
}
|
||||
$methodLineElement = $document->createElement('line');
|
||||
|
||||
$methodLineElement->setAttribute('number', (string) $line);
|
||||
$methodLineElement->setAttribute('hits', (string) count($coverageData[$line]));
|
||||
|
||||
$methodLinesElement->appendChild($methodLineElement);
|
||||
|
||||
$classLineElement = $methodLineElement->cloneNode();
|
||||
|
||||
$classLinesElement->appendChild($classLineElement);
|
||||
}
|
||||
|
||||
$methodsElement->appendChild($methodElement);
|
||||
}
|
||||
|
||||
$packageElement->setAttribute('complexity', (string) $packageComplexity);
|
||||
|
||||
if ($functionsLinesValid === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$lineRate = $functionsLinesCovered / $functionsLinesValid;
|
||||
$branchRate = $functionsBranchesValid === 0 ? 0 : ($functionsBranchesCovered / $functionsBranchesValid);
|
||||
|
||||
$classElement->setAttribute('line-rate', (string) $lineRate);
|
||||
$classElement->setAttribute('branch-rate', (string) $branchRate);
|
||||
$classElement->setAttribute('complexity', (string) $functionsComplexity);
|
||||
|
||||
$classesElement->appendChild($classElement);
|
||||
}
|
||||
|
||||
$coverageElement->setAttribute('complexity', (string) $complexity);
|
||||
|
||||
$buffer = $document->saveXML();
|
||||
|
||||
if ($target !== null) {
|
||||
if (!str_contains($target, '://')) {
|
||||
Filesystem::createDirectory(dirname($target));
|
||||
}
|
||||
|
||||
if (@file_put_contents($target, $buffer) === false) {
|
||||
throw new WriteOperationFailedException($target);
|
||||
}
|
||||
}
|
||||
|
||||
return $buffer;
|
||||
}
|
||||
}
|
||||
+153
@@ -0,0 +1,153 @@
|
||||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of phpunit/php-code-coverage.
|
||||
*
|
||||
* (c) Sebastian Bergmann <sebastian@phpunit.de>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
namespace SebastianBergmann\CodeCoverage\Report;
|
||||
|
||||
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;
|
||||
|
||||
final class Crap4j
|
||||
{
|
||||
private readonly int $threshold;
|
||||
|
||||
public function __construct(int $threshold = 30)
|
||||
{
|
||||
$this->threshold = $threshold;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws WriteOperationFailedException
|
||||
*/
|
||||
public function process(CodeCoverage $coverage, ?string $target = null, ?string $name = null): string
|
||||
{
|
||||
$document = new DOMDocument('1.0', 'UTF-8');
|
||||
$document->formatOutput = true;
|
||||
|
||||
$root = $document->createElement('crap_result');
|
||||
$document->appendChild($root);
|
||||
|
||||
$project = $document->createElement('project', is_string($name) ? $name : '');
|
||||
$root->appendChild($project);
|
||||
$root->appendChild($document->createElement('timestamp', date('Y-m-d H:i:s')));
|
||||
|
||||
$stats = $document->createElement('stats');
|
||||
$methodsNode = $document->createElement('methods');
|
||||
|
||||
$report = $coverage->getReport();
|
||||
unset($coverage);
|
||||
|
||||
$fullMethodCount = 0;
|
||||
$fullCrapMethodCount = 0;
|
||||
$fullCrapLoad = 0;
|
||||
$fullCrap = 0;
|
||||
|
||||
foreach ($report as $item) {
|
||||
$namespace = 'global';
|
||||
|
||||
if (!$item instanceof File) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$file = $document->createElement('file');
|
||||
$file->setAttribute('name', $item->pathAsString());
|
||||
|
||||
$classes = $item->classesAndTraits();
|
||||
|
||||
foreach ($classes as $className => $class) {
|
||||
foreach ($class['methods'] as $methodName => $method) {
|
||||
$crapLoad = $this->crapLoad((float) $method['crap'], $method['ccn'], $method['coverage']);
|
||||
|
||||
$fullCrap += $method['crap'];
|
||||
$fullCrapLoad += $crapLoad;
|
||||
$fullMethodCount++;
|
||||
|
||||
if ($method['crap'] >= $this->threshold) {
|
||||
$fullCrapMethodCount++;
|
||||
}
|
||||
|
||||
$methodNode = $document->createElement('method');
|
||||
|
||||
if (!empty($class['namespace'])) {
|
||||
$namespace = $class['namespace'];
|
||||
}
|
||||
|
||||
$methodNode->appendChild($document->createElement('package', $namespace));
|
||||
$methodNode->appendChild($document->createElement('className', $className));
|
||||
$methodNode->appendChild($document->createElement('methodName', $methodName));
|
||||
$methodNode->appendChild($document->createElement('methodSignature', htmlspecialchars($method['signature'])));
|
||||
$methodNode->appendChild($document->createElement('fullMethod', htmlspecialchars($method['signature'])));
|
||||
$methodNode->appendChild($document->createElement('crap', (string) $this->roundValue((float) $method['crap'])));
|
||||
$methodNode->appendChild($document->createElement('complexity', (string) $method['ccn']));
|
||||
$methodNode->appendChild($document->createElement('coverage', (string) $this->roundValue($method['coverage'])));
|
||||
$methodNode->appendChild($document->createElement('crapLoad', (string) round($crapLoad)));
|
||||
|
||||
$methodsNode->appendChild($methodNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$stats->appendChild($document->createElement('name', 'Method Crap Stats'));
|
||||
$stats->appendChild($document->createElement('methodCount', (string) $fullMethodCount));
|
||||
$stats->appendChild($document->createElement('crapMethodCount', (string) $fullCrapMethodCount));
|
||||
$stats->appendChild($document->createElement('crapLoad', (string) round($fullCrapLoad)));
|
||||
$stats->appendChild($document->createElement('totalCrap', (string) $fullCrap));
|
||||
|
||||
$crapMethodPercent = 0;
|
||||
|
||||
if ($fullMethodCount > 0) {
|
||||
$crapMethodPercent = $this->roundValue((100 * $fullCrapMethodCount) / $fullMethodCount);
|
||||
}
|
||||
|
||||
$stats->appendChild($document->createElement('crapMethodPercent', (string) $crapMethodPercent));
|
||||
|
||||
$root->appendChild($stats);
|
||||
$root->appendChild($methodsNode);
|
||||
|
||||
$buffer = $document->saveXML();
|
||||
|
||||
if ($target !== null) {
|
||||
if (!str_contains($target, '://')) {
|
||||
Filesystem::createDirectory(dirname($target));
|
||||
}
|
||||
|
||||
if (@file_put_contents($target, $buffer) === false) {
|
||||
throw new WriteOperationFailedException($target);
|
||||
}
|
||||
}
|
||||
|
||||
return $buffer;
|
||||
}
|
||||
|
||||
private function crapLoad(float $crapValue, int $cyclomaticComplexity, float $coveragePercent): float
|
||||
{
|
||||
$crapLoad = 0;
|
||||
|
||||
if ($crapValue >= $this->threshold) {
|
||||
$crapLoad += $cyclomaticComplexity * (1.0 - $coveragePercent / 100);
|
||||
$crapLoad += $cyclomaticComplexity / $this->threshold;
|
||||
}
|
||||
|
||||
return $crapLoad;
|
||||
}
|
||||
|
||||
private function roundValue(float $value): float
|
||||
{
|
||||
return round($value, 2);
|
||||
}
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of phpunit/php-code-coverage.
|
||||
*
|
||||
* (c) Sebastian Bergmann <sebastian@phpunit.de>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
namespace SebastianBergmann\CodeCoverage\Report\Html;
|
||||
|
||||
use function is_file;
|
||||
use SebastianBergmann\CodeCoverage\InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* @psalm-immutable
|
||||
*/
|
||||
final class CustomCssFile
|
||||
{
|
||||
private readonly string $path;
|
||||
|
||||
public static function default(): self
|
||||
{
|
||||
return new self(__DIR__ . '/Renderer/Template/css/custom.css');
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public static function from(string $path): self
|
||||
{
|
||||
if (!is_file($path)) {
|
||||
throw new InvalidArgumentException(
|
||||
'$path does not exist',
|
||||
);
|
||||
}
|
||||
|
||||
return new self($path);
|
||||
}
|
||||
|
||||
private function __construct(string $path)
|
||||
{
|
||||
$this->path = $path;
|
||||
}
|
||||
|
||||
public function path(): string
|
||||
{
|
||||
return $this->path;
|
||||
}
|
||||
}
|
||||
+153
@@ -0,0 +1,153 @@
|
||||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of phpunit/php-code-coverage.
|
||||
*
|
||||
* (c) Sebastian Bergmann <sebastian@phpunit.de>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
namespace SebastianBergmann\CodeCoverage\Report\Html;
|
||||
|
||||
use const DIRECTORY_SEPARATOR;
|
||||
use function copy;
|
||||
use function date;
|
||||
use function dirname;
|
||||
use function str_ends_with;
|
||||
use SebastianBergmann\CodeCoverage\CodeCoverage;
|
||||
use SebastianBergmann\CodeCoverage\FileCouldNotBeWrittenException;
|
||||
use SebastianBergmann\CodeCoverage\Node\Directory as DirectoryNode;
|
||||
use SebastianBergmann\CodeCoverage\Report\Thresholds;
|
||||
use SebastianBergmann\CodeCoverage\Util\Filesystem;
|
||||
use SebastianBergmann\Template\Exception;
|
||||
use SebastianBergmann\Template\Template;
|
||||
|
||||
final class Facade
|
||||
{
|
||||
private readonly string $templatePath;
|
||||
private readonly string $generator;
|
||||
private readonly Colors $colors;
|
||||
private readonly Thresholds $thresholds;
|
||||
private readonly CustomCssFile $customCssFile;
|
||||
|
||||
public function __construct(string $generator = '', ?Colors $colors = null, ?Thresholds $thresholds = null, ?CustomCssFile $customCssFile = null)
|
||||
{
|
||||
$this->generator = $generator;
|
||||
$this->colors = $colors ?? Colors::default();
|
||||
$this->thresholds = $thresholds ?? Thresholds::default();
|
||||
$this->customCssFile = $customCssFile ?? CustomCssFile::default();
|
||||
$this->templatePath = __DIR__ . '/Renderer/Template/';
|
||||
}
|
||||
|
||||
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');
|
||||
|
||||
$dashboard = new Dashboard(
|
||||
$this->templatePath,
|
||||
$this->generator,
|
||||
$date,
|
||||
$this->thresholds,
|
||||
$coverage->collectsBranchAndPathCoverage(),
|
||||
);
|
||||
|
||||
$directory = new Directory(
|
||||
$this->templatePath,
|
||||
$this->generator,
|
||||
$date,
|
||||
$this->thresholds,
|
||||
$coverage->collectsBranchAndPathCoverage(),
|
||||
);
|
||||
|
||||
$file = new File(
|
||||
$this->templatePath,
|
||||
$this->generator,
|
||||
$date,
|
||||
$this->thresholds,
|
||||
$coverage->collectsBranchAndPathCoverage(),
|
||||
);
|
||||
|
||||
$directory->render($report, $target . 'index.html');
|
||||
$dashboard->render($report, $target . 'dashboard.html');
|
||||
|
||||
foreach ($report as $node) {
|
||||
$id = $node->id();
|
||||
|
||||
if ($node instanceof DirectoryNode) {
|
||||
Filesystem::createDirectory($target . $id);
|
||||
|
||||
$directory->render($node, $target . $id . '/index.html');
|
||||
$dashboard->render($node, $target . $id . '/dashboard.html');
|
||||
} else {
|
||||
$dir = dirname($target . $id);
|
||||
|
||||
Filesystem::createDirectory($dir);
|
||||
|
||||
$file->render($node, $target . $id);
|
||||
}
|
||||
}
|
||||
|
||||
$this->copyFiles($target);
|
||||
$this->renderCss($target);
|
||||
}
|
||||
|
||||
private function copyFiles(string $target): void
|
||||
{
|
||||
$dir = $this->directory($target . '_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');
|
||||
|
||||
$dir = $this->directory($target . '_icons');
|
||||
copy($this->templatePath . 'icons/file-code.svg', $dir . 'file-code.svg');
|
||||
copy($this->templatePath . 'icons/file-directory.svg', $dir . 'file-directory.svg');
|
||||
|
||||
$dir = $this->directory($target . '_js');
|
||||
copy($this->templatePath . 'js/bootstrap.min.js', $dir . 'bootstrap.min.js');
|
||||
copy($this->templatePath . 'js/popper.min.js', $dir . 'popper.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');
|
||||
}
|
||||
|
||||
private function renderCss(string $target): void
|
||||
{
|
||||
$template = new Template($this->templatePath . 'css/style.css', '{{', '}}');
|
||||
|
||||
$template->setVar(
|
||||
[
|
||||
'success-low' => $this->colors->successLow(),
|
||||
'success-medium' => $this->colors->successMedium(),
|
||||
'success-high' => $this->colors->successHigh(),
|
||||
'warning' => $this->colors->warning(),
|
||||
'danger' => $this->colors->danger(),
|
||||
],
|
||||
);
|
||||
|
||||
try {
|
||||
$template->renderTo($this->directory($target . '_css') . 'style.css');
|
||||
} catch (Exception $e) {
|
||||
throw new FileCouldNotBeWrittenException(
|
||||
$e->getMessage(),
|
||||
$e->getCode(),
|
||||
$e,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private function directory(string $directory): string
|
||||
{
|
||||
if (!str_ends_with($directory, DIRECTORY_SEPARATOR)) {
|
||||
$directory .= DIRECTORY_SEPARATOR;
|
||||
}
|
||||
|
||||
Filesystem::createDirectory($directory);
|
||||
|
||||
return $directory;
|
||||
}
|
||||
}
|
||||
+286
@@ -0,0 +1,286 @@
|
||||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of phpunit/php-code-coverage.
|
||||
*
|
||||
* (c) Sebastian Bergmann <sebastian@phpunit.de>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
namespace SebastianBergmann\CodeCoverage\Report\Html;
|
||||
|
||||
use function array_pop;
|
||||
use function count;
|
||||
use function sprintf;
|
||||
use function str_repeat;
|
||||
use function substr_count;
|
||||
use SebastianBergmann\CodeCoverage\Node\AbstractNode;
|
||||
use SebastianBergmann\CodeCoverage\Node\Directory as DirectoryNode;
|
||||
use SebastianBergmann\CodeCoverage\Node\File as FileNode;
|
||||
use SebastianBergmann\CodeCoverage\Report\Thresholds;
|
||||
use SebastianBergmann\CodeCoverage\Version;
|
||||
use SebastianBergmann\Environment\Runtime;
|
||||
use SebastianBergmann\Template\Template;
|
||||
|
||||
/**
|
||||
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
|
||||
*/
|
||||
abstract class Renderer
|
||||
{
|
||||
protected string $templatePath;
|
||||
protected string $generator;
|
||||
protected string $date;
|
||||
protected Thresholds $thresholds;
|
||||
protected bool $hasBranchCoverage;
|
||||
protected string $version;
|
||||
|
||||
public function __construct(string $templatePath, string $generator, string $date, Thresholds $thresholds, bool $hasBranchCoverage)
|
||||
{
|
||||
$this->templatePath = $templatePath;
|
||||
$this->generator = $generator;
|
||||
$this->date = $date;
|
||||
$this->thresholds = $thresholds;
|
||||
$this->version = Version::id();
|
||||
$this->hasBranchCoverage = $hasBranchCoverage;
|
||||
}
|
||||
|
||||
protected function renderItemTemplate(Template $template, array $data): string
|
||||
{
|
||||
$numSeparator = ' / ';
|
||||
|
||||
if (isset($data['numClasses']) && $data['numClasses'] > 0) {
|
||||
$classesLevel = $this->colorLevel($data['testedClassesPercent']);
|
||||
|
||||
$classesNumber = $data['numTestedClasses'] . $numSeparator .
|
||||
$data['numClasses'];
|
||||
|
||||
$classesBar = $this->coverageBar(
|
||||
$data['testedClassesPercent'],
|
||||
);
|
||||
} else {
|
||||
$classesLevel = '';
|
||||
$classesNumber = '0' . $numSeparator . '0';
|
||||
$classesBar = '';
|
||||
$data['testedClassesPercentAsString'] = 'n/a';
|
||||
}
|
||||
|
||||
if ($data['numMethods'] > 0) {
|
||||
$methodsLevel = $this->colorLevel($data['testedMethodsPercent']);
|
||||
|
||||
$methodsNumber = $data['numTestedMethods'] . $numSeparator .
|
||||
$data['numMethods'];
|
||||
|
||||
$methodsBar = $this->coverageBar(
|
||||
$data['testedMethodsPercent'],
|
||||
);
|
||||
} else {
|
||||
$methodsLevel = '';
|
||||
$methodsNumber = '0' . $numSeparator . '0';
|
||||
$methodsBar = '';
|
||||
$data['testedMethodsPercentAsString'] = 'n/a';
|
||||
}
|
||||
|
||||
if ($data['numExecutableLines'] > 0) {
|
||||
$linesLevel = $this->colorLevel($data['linesExecutedPercent']);
|
||||
|
||||
$linesNumber = $data['numExecutedLines'] . $numSeparator .
|
||||
$data['numExecutableLines'];
|
||||
|
||||
$linesBar = $this->coverageBar(
|
||||
$data['linesExecutedPercent'],
|
||||
);
|
||||
} else {
|
||||
$linesLevel = '';
|
||||
$linesNumber = '0' . $numSeparator . '0';
|
||||
$linesBar = '';
|
||||
$data['linesExecutedPercentAsString'] = 'n/a';
|
||||
}
|
||||
|
||||
if ($data['numExecutablePaths'] > 0) {
|
||||
$pathsLevel = $this->colorLevel($data['pathsExecutedPercent']);
|
||||
|
||||
$pathsNumber = $data['numExecutedPaths'] . $numSeparator .
|
||||
$data['numExecutablePaths'];
|
||||
|
||||
$pathsBar = $this->coverageBar(
|
||||
$data['pathsExecutedPercent'],
|
||||
);
|
||||
} else {
|
||||
$pathsLevel = '';
|
||||
$pathsNumber = '0' . $numSeparator . '0';
|
||||
$pathsBar = '';
|
||||
$data['pathsExecutedPercentAsString'] = 'n/a';
|
||||
}
|
||||
|
||||
if ($data['numExecutableBranches'] > 0) {
|
||||
$branchesLevel = $this->colorLevel($data['branchesExecutedPercent']);
|
||||
|
||||
$branchesNumber = $data['numExecutedBranches'] . $numSeparator .
|
||||
$data['numExecutableBranches'];
|
||||
|
||||
$branchesBar = $this->coverageBar(
|
||||
$data['branchesExecutedPercent'],
|
||||
);
|
||||
} else {
|
||||
$branchesLevel = '';
|
||||
$branchesNumber = '0' . $numSeparator . '0';
|
||||
$branchesBar = '';
|
||||
$data['branchesExecutedPercentAsString'] = 'n/a';
|
||||
}
|
||||
|
||||
$template->setVar(
|
||||
[
|
||||
'icon' => $data['icon'] ?? '',
|
||||
'crap' => $data['crap'] ?? '',
|
||||
'name' => $data['name'],
|
||||
'lines_bar' => $linesBar,
|
||||
'lines_executed_percent' => $data['linesExecutedPercentAsString'],
|
||||
'lines_level' => $linesLevel,
|
||||
'lines_number' => $linesNumber,
|
||||
'paths_bar' => $pathsBar,
|
||||
'paths_executed_percent' => $data['pathsExecutedPercentAsString'],
|
||||
'paths_level' => $pathsLevel,
|
||||
'paths_number' => $pathsNumber,
|
||||
'branches_bar' => $branchesBar,
|
||||
'branches_executed_percent' => $data['branchesExecutedPercentAsString'],
|
||||
'branches_level' => $branchesLevel,
|
||||
'branches_number' => $branchesNumber,
|
||||
'methods_bar' => $methodsBar,
|
||||
'methods_tested_percent' => $data['testedMethodsPercentAsString'],
|
||||
'methods_level' => $methodsLevel,
|
||||
'methods_number' => $methodsNumber,
|
||||
'classes_bar' => $classesBar,
|
||||
'classes_tested_percent' => $data['testedClassesPercentAsString'] ?? '',
|
||||
'classes_level' => $classesLevel,
|
||||
'classes_number' => $classesNumber,
|
||||
],
|
||||
);
|
||||
|
||||
return $template->render();
|
||||
}
|
||||
|
||||
protected function setCommonTemplateVariables(Template $template, AbstractNode $node): void
|
||||
{
|
||||
$template->setVar(
|
||||
[
|
||||
'id' => $node->id(),
|
||||
'full_path' => $node->pathAsString(),
|
||||
'path_to_root' => $this->pathToRoot($node),
|
||||
'breadcrumbs' => $this->breadcrumbs($node),
|
||||
'date' => $this->date,
|
||||
'version' => $this->version,
|
||||
'runtime' => $this->runtimeString(),
|
||||
'generator' => $this->generator,
|
||||
'low_upper_bound' => $this->thresholds->lowUpperBound(),
|
||||
'high_lower_bound' => $this->thresholds->highLowerBound(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
protected function breadcrumbs(AbstractNode $node): string
|
||||
{
|
||||
$breadcrumbs = '';
|
||||
$path = $node->pathAsArray();
|
||||
$pathToRoot = [];
|
||||
$max = count($path);
|
||||
|
||||
if ($node instanceof FileNode) {
|
||||
$max--;
|
||||
}
|
||||
|
||||
for ($i = 0; $i < $max; $i++) {
|
||||
$pathToRoot[] = str_repeat('../', $i);
|
||||
}
|
||||
|
||||
foreach ($path as $step) {
|
||||
if ($step !== $node) {
|
||||
$breadcrumbs .= $this->inactiveBreadcrumb(
|
||||
$step,
|
||||
array_pop($pathToRoot),
|
||||
);
|
||||
} else {
|
||||
$breadcrumbs .= $this->activeBreadcrumb($step);
|
||||
}
|
||||
}
|
||||
|
||||
return $breadcrumbs;
|
||||
}
|
||||
|
||||
protected function activeBreadcrumb(AbstractNode $node): string
|
||||
{
|
||||
$buffer = sprintf(
|
||||
' <li class="breadcrumb-item active">%s</li>' . "\n",
|
||||
$node->name(),
|
||||
);
|
||||
|
||||
if ($node instanceof DirectoryNode) {
|
||||
$buffer .= ' <li class="breadcrumb-item">(<a href="dashboard.html">Dashboard</a>)</li>' . "\n";
|
||||
}
|
||||
|
||||
return $buffer;
|
||||
}
|
||||
|
||||
protected function inactiveBreadcrumb(AbstractNode $node, string $pathToRoot): string
|
||||
{
|
||||
return sprintf(
|
||||
' <li class="breadcrumb-item"><a href="%sindex.html">%s</a></li>' . "\n",
|
||||
$pathToRoot,
|
||||
$node->name(),
|
||||
);
|
||||
}
|
||||
|
||||
protected function pathToRoot(AbstractNode $node): string
|
||||
{
|
||||
$id = $node->id();
|
||||
$depth = substr_count($id, '/');
|
||||
|
||||
if ($id !== 'index' &&
|
||||
$node instanceof DirectoryNode) {
|
||||
$depth++;
|
||||
}
|
||||
|
||||
return str_repeat('../', $depth);
|
||||
}
|
||||
|
||||
protected function coverageBar(float $percent): string
|
||||
{
|
||||
$level = $this->colorLevel($percent);
|
||||
|
||||
$templateName = $this->templatePath . ($this->hasBranchCoverage ? 'coverage_bar_branch.html' : 'coverage_bar.html');
|
||||
$template = new Template(
|
||||
$templateName,
|
||||
'{{',
|
||||
'}}',
|
||||
);
|
||||
|
||||
$template->setVar(['level' => $level, 'percent' => sprintf('%.2F', $percent)]);
|
||||
|
||||
return $template->render();
|
||||
}
|
||||
|
||||
protected function colorLevel(float $percent): string
|
||||
{
|
||||
if ($percent <= $this->thresholds->lowUpperBound()) {
|
||||
return 'danger';
|
||||
}
|
||||
|
||||
if ($percent > $this->thresholds->lowUpperBound() &&
|
||||
$percent < $this->thresholds->highLowerBound()) {
|
||||
return 'warning';
|
||||
}
|
||||
|
||||
return 'success';
|
||||
}
|
||||
|
||||
private function runtimeString(): string
|
||||
{
|
||||
$runtime = new Runtime;
|
||||
|
||||
return sprintf(
|
||||
'<a href="%s" target="_top">%s %s</a>',
|
||||
$runtime->getVendorUrl(),
|
||||
$runtime->getName(),
|
||||
$runtime->getVersion(),
|
||||
);
|
||||
}
|
||||
}
|
||||
Vendored
+298
@@ -0,0 +1,298 @@
|
||||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of phpunit/php-code-coverage.
|
||||
*
|
||||
* (c) Sebastian Bergmann <sebastian@phpunit.de>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
namespace SebastianBergmann\CodeCoverage\Report\Html;
|
||||
|
||||
use function array_values;
|
||||
use function arsort;
|
||||
use function asort;
|
||||
use function count;
|
||||
use function explode;
|
||||
use function floor;
|
||||
use function json_encode;
|
||||
use function sprintf;
|
||||
use function str_replace;
|
||||
use SebastianBergmann\CodeCoverage\FileCouldNotBeWrittenException;
|
||||
use SebastianBergmann\CodeCoverage\Node\AbstractNode;
|
||||
use SebastianBergmann\CodeCoverage\Node\Directory as DirectoryNode;
|
||||
use SebastianBergmann\Template\Exception;
|
||||
use SebastianBergmann\Template\Template;
|
||||
|
||||
/**
|
||||
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
|
||||
*/
|
||||
final class Dashboard extends Renderer
|
||||
{
|
||||
public function render(DirectoryNode $node, string $file): void
|
||||
{
|
||||
$classes = $node->classesAndTraits();
|
||||
$templateName = $this->templatePath . ($this->hasBranchCoverage ? 'dashboard_branch.html' : 'dashboard.html');
|
||||
$template = new Template(
|
||||
$templateName,
|
||||
'{{',
|
||||
'}}',
|
||||
);
|
||||
|
||||
$this->setCommonTemplateVariables($template, $node);
|
||||
|
||||
$baseLink = $node->id() . '/';
|
||||
$complexity = $this->complexity($classes, $baseLink);
|
||||
$coverageDistribution = $this->coverageDistribution($classes);
|
||||
$insufficientCoverage = $this->insufficientCoverage($classes, $baseLink);
|
||||
$projectRisks = $this->projectRisks($classes, $baseLink);
|
||||
|
||||
$template->setVar(
|
||||
[
|
||||
'insufficient_coverage_classes' => $insufficientCoverage['class'],
|
||||
'insufficient_coverage_methods' => $insufficientCoverage['method'],
|
||||
'project_risks_classes' => $projectRisks['class'],
|
||||
'project_risks_methods' => $projectRisks['method'],
|
||||
'complexity_class' => $complexity['class'],
|
||||
'complexity_method' => $complexity['method'],
|
||||
'class_coverage_distribution' => $coverageDistribution['class'],
|
||||
'method_coverage_distribution' => $coverageDistribution['method'],
|
||||
],
|
||||
);
|
||||
|
||||
try {
|
||||
$template->renderTo($file);
|
||||
} catch (Exception $e) {
|
||||
throw new FileCouldNotBeWrittenException(
|
||||
$e->getMessage(),
|
||||
$e->getCode(),
|
||||
$e,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
protected function activeBreadcrumb(AbstractNode $node): string
|
||||
{
|
||||
return sprintf(
|
||||
' <li class="breadcrumb-item"><a href="index.html">%s</a></li>' . "\n" .
|
||||
' <li class="breadcrumb-item active">(Dashboard)</li>' . "\n",
|
||||
$node->name(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the data for the Class/Method Complexity charts.
|
||||
*/
|
||||
private function complexity(array $classes, string $baseLink): array
|
||||
{
|
||||
$result = ['class' => [], 'method' => []];
|
||||
|
||||
foreach ($classes as $className => $class) {
|
||||
foreach ($class['methods'] as $methodName => $method) {
|
||||
if ($className !== '*') {
|
||||
$methodName = $className . '::' . $methodName;
|
||||
}
|
||||
|
||||
$result['method'][] = [
|
||||
$method['coverage'],
|
||||
$method['ccn'],
|
||||
sprintf(
|
||||
'<a href="%s">%s</a>',
|
||||
str_replace($baseLink, '', $method['link']),
|
||||
$methodName,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
$result['class'][] = [
|
||||
$class['coverage'],
|
||||
$class['ccn'],
|
||||
sprintf(
|
||||
'<a href="%s">%s</a>',
|
||||
str_replace($baseLink, '', $class['link']),
|
||||
$className,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'class' => json_encode($result['class']),
|
||||
'method' => json_encode($result['method']),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the data for the Class / Method Coverage Distribution chart.
|
||||
*/
|
||||
private function coverageDistribution(array $classes): array
|
||||
{
|
||||
$result = [
|
||||
'class' => [
|
||||
'0%' => 0,
|
||||
'0-10%' => 0,
|
||||
'10-20%' => 0,
|
||||
'20-30%' => 0,
|
||||
'30-40%' => 0,
|
||||
'40-50%' => 0,
|
||||
'50-60%' => 0,
|
||||
'60-70%' => 0,
|
||||
'70-80%' => 0,
|
||||
'80-90%' => 0,
|
||||
'90-100%' => 0,
|
||||
'100%' => 0,
|
||||
],
|
||||
'method' => [
|
||||
'0%' => 0,
|
||||
'0-10%' => 0,
|
||||
'10-20%' => 0,
|
||||
'20-30%' => 0,
|
||||
'30-40%' => 0,
|
||||
'40-50%' => 0,
|
||||
'50-60%' => 0,
|
||||
'60-70%' => 0,
|
||||
'70-80%' => 0,
|
||||
'80-90%' => 0,
|
||||
'90-100%' => 0,
|
||||
'100%' => 0,
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($classes as $class) {
|
||||
foreach ($class['methods'] as $methodName => $method) {
|
||||
if ($method['coverage'] === 0) {
|
||||
$result['method']['0%']++;
|
||||
} elseif ($method['coverage'] === 100) {
|
||||
$result['method']['100%']++;
|
||||
} else {
|
||||
$key = floor($method['coverage'] / 10) * 10;
|
||||
$key = $key . '-' . ($key + 10) . '%';
|
||||
$result['method'][$key]++;
|
||||
}
|
||||
}
|
||||
|
||||
if ($class['coverage'] === 0) {
|
||||
$result['class']['0%']++;
|
||||
} elseif ($class['coverage'] === 100) {
|
||||
$result['class']['100%']++;
|
||||
} else {
|
||||
$key = floor($class['coverage'] / 10) * 10;
|
||||
$key = $key . '-' . ($key + 10) . '%';
|
||||
$result['class'][$key]++;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'class' => json_encode(array_values($result['class'])),
|
||||
'method' => json_encode(array_values($result['method'])),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the classes / methods with insufficient coverage.
|
||||
*/
|
||||
private function insufficientCoverage(array $classes, string $baseLink): array
|
||||
{
|
||||
$leastTestedClasses = [];
|
||||
$leastTestedMethods = [];
|
||||
$result = ['class' => '', 'method' => ''];
|
||||
|
||||
foreach ($classes as $className => $class) {
|
||||
foreach ($class['methods'] as $methodName => $method) {
|
||||
if ($method['coverage'] < $this->thresholds->highLowerBound()) {
|
||||
$key = $methodName;
|
||||
|
||||
if ($className !== '*') {
|
||||
$key = $className . '::' . $methodName;
|
||||
}
|
||||
|
||||
$leastTestedMethods[$key] = $method['coverage'];
|
||||
}
|
||||
}
|
||||
|
||||
if ($class['coverage'] < $this->thresholds->highLowerBound()) {
|
||||
$leastTestedClasses[$className] = $class['coverage'];
|
||||
}
|
||||
}
|
||||
|
||||
asort($leastTestedClasses);
|
||||
asort($leastTestedMethods);
|
||||
|
||||
foreach ($leastTestedClasses as $className => $coverage) {
|
||||
$result['class'] .= sprintf(
|
||||
' <tr><td><a href="%s">%s</a></td><td class="text-right">%d%%</td></tr>' . "\n",
|
||||
str_replace($baseLink, '', $classes[$className]['link']),
|
||||
$className,
|
||||
$coverage,
|
||||
);
|
||||
}
|
||||
|
||||
foreach ($leastTestedMethods as $methodName => $coverage) {
|
||||
[$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",
|
||||
str_replace($baseLink, '', $classes[$class]['methods'][$method]['link']),
|
||||
$methodName,
|
||||
$method,
|
||||
$coverage,
|
||||
);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the project risks according to the CRAP index.
|
||||
*/
|
||||
private function projectRisks(array $classes, string $baseLink): array
|
||||
{
|
||||
$classRisks = [];
|
||||
$methodRisks = [];
|
||||
$result = ['class' => '', 'method' => ''];
|
||||
|
||||
foreach ($classes as $className => $class) {
|
||||
foreach ($class['methods'] as $methodName => $method) {
|
||||
if ($method['coverage'] < $this->thresholds->highLowerBound() && $method['ccn'] > 1) {
|
||||
$key = $methodName;
|
||||
|
||||
if ($className !== '*') {
|
||||
$key = $className . '::' . $methodName;
|
||||
}
|
||||
|
||||
$methodRisks[$key] = $method['crap'];
|
||||
}
|
||||
}
|
||||
|
||||
if ($class['coverage'] < $this->thresholds->highLowerBound() &&
|
||||
$class['ccn'] > count($class['methods'])) {
|
||||
$classRisks[$className] = $class['crap'];
|
||||
}
|
||||
}
|
||||
|
||||
arsort($classRisks);
|
||||
arsort($methodRisks);
|
||||
|
||||
foreach ($classRisks as $className => $crap) {
|
||||
$result['class'] .= sprintf(
|
||||
' <tr><td><a href="%s">%s</a></td><td class="text-right">%d</td></tr>' . "\n",
|
||||
str_replace($baseLink, '', $classes[$className]['link']),
|
||||
$className,
|
||||
$crap,
|
||||
);
|
||||
}
|
||||
|
||||
foreach ($methodRisks as $methodName => $crap) {
|
||||
[$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",
|
||||
str_replace($baseLink, '', $classes[$class]['methods'][$method]['link']),
|
||||
$methodName,
|
||||
$method,
|
||||
$crap,
|
||||
);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
Vendored
+123
@@ -0,0 +1,123 @@
|
||||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of phpunit/php-code-coverage.
|
||||
*
|
||||
* (c) Sebastian Bergmann <sebastian@phpunit.de>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
namespace SebastianBergmann\CodeCoverage\Report\Html;
|
||||
|
||||
use function count;
|
||||
use function sprintf;
|
||||
use function str_repeat;
|
||||
use SebastianBergmann\CodeCoverage\FileCouldNotBeWrittenException;
|
||||
use SebastianBergmann\CodeCoverage\Node\AbstractNode as Node;
|
||||
use SebastianBergmann\CodeCoverage\Node\Directory as DirectoryNode;
|
||||
use SebastianBergmann\Template\Exception;
|
||||
use SebastianBergmann\Template\Template;
|
||||
|
||||
/**
|
||||
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
|
||||
*/
|
||||
final class Directory extends Renderer
|
||||
{
|
||||
public function render(DirectoryNode $node, string $file): void
|
||||
{
|
||||
$templateName = $this->templatePath . ($this->hasBranchCoverage ? 'directory_branch.html' : 'directory.html');
|
||||
$template = new Template($templateName, '{{', '}}');
|
||||
|
||||
$this->setCommonTemplateVariables($template, $node);
|
||||
|
||||
$items = $this->renderItem($node, true);
|
||||
|
||||
foreach ($node->directories() as $item) {
|
||||
$items .= $this->renderItem($item);
|
||||
}
|
||||
|
||||
foreach ($node->files() as $item) {
|
||||
$items .= $this->renderItem($item);
|
||||
}
|
||||
|
||||
$template->setVar(
|
||||
[
|
||||
'id' => $node->id(),
|
||||
'items' => $items,
|
||||
],
|
||||
);
|
||||
|
||||
try {
|
||||
$template->renderTo($file);
|
||||
} catch (Exception $e) {
|
||||
throw new FileCouldNotBeWrittenException(
|
||||
$e->getMessage(),
|
||||
$e->getCode(),
|
||||
$e,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private function renderItem(Node $node, bool $total = false): string
|
||||
{
|
||||
$data = [
|
||||
'numClasses' => $node->numberOfClassesAndTraits(),
|
||||
'numTestedClasses' => $node->numberOfTestedClassesAndTraits(),
|
||||
'numMethods' => $node->numberOfFunctionsAndMethods(),
|
||||
'numTestedMethods' => $node->numberOfTestedFunctionsAndMethods(),
|
||||
'linesExecutedPercent' => $node->percentageOfExecutedLines()->asFloat(),
|
||||
'linesExecutedPercentAsString' => $node->percentageOfExecutedLines()->asString(),
|
||||
'numExecutedLines' => $node->numberOfExecutedLines(),
|
||||
'numExecutableLines' => $node->numberOfExecutableLines(),
|
||||
'branchesExecutedPercent' => $node->percentageOfExecutedBranches()->asFloat(),
|
||||
'branchesExecutedPercentAsString' => $node->percentageOfExecutedBranches()->asString(),
|
||||
'numExecutedBranches' => $node->numberOfExecutedBranches(),
|
||||
'numExecutableBranches' => $node->numberOfExecutableBranches(),
|
||||
'pathsExecutedPercent' => $node->percentageOfExecutedPaths()->asFloat(),
|
||||
'pathsExecutedPercentAsString' => $node->percentageOfExecutedPaths()->asString(),
|
||||
'numExecutedPaths' => $node->numberOfExecutedPaths(),
|
||||
'numExecutablePaths' => $node->numberOfExecutablePaths(),
|
||||
'testedMethodsPercent' => $node->percentageOfTestedFunctionsAndMethods()->asFloat(),
|
||||
'testedMethodsPercentAsString' => $node->percentageOfTestedFunctionsAndMethods()->asString(),
|
||||
'testedClassesPercent' => $node->percentageOfTestedClassesAndTraits()->asFloat(),
|
||||
'testedClassesPercentAsString' => $node->percentageOfTestedClassesAndTraits()->asString(),
|
||||
];
|
||||
|
||||
if ($total) {
|
||||
$data['name'] = 'Total';
|
||||
} else {
|
||||
$up = str_repeat('../', count($node->pathAsArray()) - 2);
|
||||
$data['icon'] = sprintf('<img src="%s_icons/file-code.svg" class="octicon" />', $up);
|
||||
|
||||
if ($node instanceof DirectoryNode) {
|
||||
$data['name'] = sprintf(
|
||||
'<a href="%s/index.html">%s</a>',
|
||||
$node->name(),
|
||||
$node->name(),
|
||||
);
|
||||
$data['icon'] = sprintf('<img src="%s_icons/file-directory.svg" class="octicon" />', $up);
|
||||
} elseif ($this->hasBranchCoverage) {
|
||||
$data['name'] = sprintf(
|
||||
'%s <a class="small" href="%s.html">[line]</a> <a class="small" href="%s_branch.html">[branch]</a> <a class="small" href="%s_path.html">[path]</a>',
|
||||
$node->name(),
|
||||
$node->name(),
|
||||
$node->name(),
|
||||
$node->name(),
|
||||
);
|
||||
} else {
|
||||
$data['name'] = sprintf(
|
||||
'<a href="%s.html">%s</a>',
|
||||
$node->name(),
|
||||
$node->name(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$templateName = $this->templatePath . ($this->hasBranchCoverage ? 'directory_item_branch.html' : 'directory_item.html');
|
||||
|
||||
return $this->renderItemTemplate(
|
||||
new Template($templateName, '{{', '}}'),
|
||||
$data,
|
||||
);
|
||||
}
|
||||
}
|
||||
+1130
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,41 @@
|
||||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of phpunit/php-code-coverage.
|
||||
*
|
||||
* (c) Sebastian Bergmann <sebastian@phpunit.de>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
namespace SebastianBergmann\CodeCoverage\Report;
|
||||
|
||||
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;
|
||||
|
||||
final class PHP
|
||||
{
|
||||
public function process(CodeCoverage $coverage, ?string $target = null): string
|
||||
{
|
||||
$coverage->clearCache();
|
||||
|
||||
$buffer = "<?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);
|
||||
}
|
||||
}
|
||||
|
||||
return $buffer;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,315 @@
|
||||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of phpunit/php-code-coverage.
|
||||
*
|
||||
* (c) Sebastian Bergmann <sebastian@phpunit.de>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
namespace SebastianBergmann\CodeCoverage\Report;
|
||||
|
||||
use const PHP_EOL;
|
||||
use function array_map;
|
||||
use function date;
|
||||
use function ksort;
|
||||
use function max;
|
||||
use function sprintf;
|
||||
use function str_pad;
|
||||
use function strlen;
|
||||
use SebastianBergmann\CodeCoverage\CodeCoverage;
|
||||
use SebastianBergmann\CodeCoverage\Node\File;
|
||||
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 readonly Thresholds $thresholds;
|
||||
private readonly bool $showUncoveredFiles;
|
||||
private readonly bool $showOnlySummary;
|
||||
|
||||
public function __construct(Thresholds $thresholds, bool $showUncoveredFiles = false, bool $showOnlySummary = false)
|
||||
{
|
||||
$this->thresholds = $thresholds;
|
||||
$this->showUncoveredFiles = $showUncoveredFiles;
|
||||
$this->showOnlySummary = $showOnlySummary;
|
||||
}
|
||||
|
||||
public function process(CodeCoverage $coverage, bool $showColors = false): string
|
||||
{
|
||||
$hasBranchCoverage = !empty($coverage->getData(true)->functionCoverage());
|
||||
|
||||
$output = PHP_EOL . PHP_EOL;
|
||||
$report = $coverage->getReport();
|
||||
|
||||
$colors = [
|
||||
'header' => '',
|
||||
'classes' => '',
|
||||
'methods' => '',
|
||||
'lines' => '',
|
||||
'branches' => '',
|
||||
'paths' => '',
|
||||
'reset' => '',
|
||||
];
|
||||
|
||||
if ($showColors) {
|
||||
$colors['classes'] = $this->coverageColor(
|
||||
$report->numberOfTestedClassesAndTraits(),
|
||||
$report->numberOfClassesAndTraits(),
|
||||
);
|
||||
|
||||
$colors['methods'] = $this->coverageColor(
|
||||
$report->numberOfTestedMethods(),
|
||||
$report->numberOfMethods(),
|
||||
);
|
||||
|
||||
$colors['lines'] = $this->coverageColor(
|
||||
$report->numberOfExecutedLines(),
|
||||
$report->numberOfExecutableLines(),
|
||||
);
|
||||
|
||||
$colors['branches'] = $this->coverageColor(
|
||||
$report->numberOfExecutedBranches(),
|
||||
$report->numberOfExecutableBranches(),
|
||||
);
|
||||
|
||||
$colors['paths'] = $this->coverageColor(
|
||||
$report->numberOfExecutedPaths(),
|
||||
$report->numberOfExecutablePaths(),
|
||||
);
|
||||
|
||||
$colors['reset'] = self::COLOR_RESET;
|
||||
$colors['header'] = self::COLOR_HEADER;
|
||||
}
|
||||
|
||||
$classes = sprintf(
|
||||
' Classes: %6s (%d/%d)',
|
||||
Percentage::fromFractionAndTotal(
|
||||
$report->numberOfTestedClassesAndTraits(),
|
||||
$report->numberOfClassesAndTraits(),
|
||||
)->asString(),
|
||||
$report->numberOfTestedClassesAndTraits(),
|
||||
$report->numberOfClassesAndTraits(),
|
||||
);
|
||||
|
||||
$methods = sprintf(
|
||||
' Methods: %6s (%d/%d)',
|
||||
Percentage::fromFractionAndTotal(
|
||||
$report->numberOfTestedMethods(),
|
||||
$report->numberOfMethods(),
|
||||
)->asString(),
|
||||
$report->numberOfTestedMethods(),
|
||||
$report->numberOfMethods(),
|
||||
);
|
||||
|
||||
$paths = '';
|
||||
$branches = '';
|
||||
|
||||
if ($hasBranchCoverage) {
|
||||
$paths = sprintf(
|
||||
' Paths: %6s (%d/%d)',
|
||||
Percentage::fromFractionAndTotal(
|
||||
$report->numberOfExecutedPaths(),
|
||||
$report->numberOfExecutablePaths(),
|
||||
)->asString(),
|
||||
$report->numberOfExecutedPaths(),
|
||||
$report->numberOfExecutablePaths(),
|
||||
);
|
||||
|
||||
$branches = sprintf(
|
||||
' Branches: %6s (%d/%d)',
|
||||
Percentage::fromFractionAndTotal(
|
||||
$report->numberOfExecutedBranches(),
|
||||
$report->numberOfExecutableBranches(),
|
||||
)->asString(),
|
||||
$report->numberOfExecutedBranches(),
|
||||
$report->numberOfExecutableBranches(),
|
||||
);
|
||||
}
|
||||
|
||||
$lines = sprintf(
|
||||
' Lines: %6s (%d/%d)',
|
||||
Percentage::fromFractionAndTotal(
|
||||
$report->numberOfExecutedLines(),
|
||||
$report->numberOfExecutableLines(),
|
||||
)->asString(),
|
||||
$report->numberOfExecutedLines(),
|
||||
$report->numberOfExecutableLines(),
|
||||
);
|
||||
|
||||
$padding = max(array_map('strlen', [$classes, $methods, $lines]));
|
||||
|
||||
if ($this->showOnlySummary) {
|
||||
$title = 'Code Coverage Report Summary:';
|
||||
$padding = max($padding, strlen($title));
|
||||
|
||||
$output .= $this->format($colors['header'], $padding, $title);
|
||||
} else {
|
||||
$date = date(' Y-m-d H:i:s');
|
||||
$title = 'Code Coverage Report:';
|
||||
|
||||
$output .= $this->format($colors['header'], $padding, $title);
|
||||
$output .= $this->format($colors['header'], $padding, $date);
|
||||
$output .= $this->format($colors['header'], $padding, '');
|
||||
$output .= $this->format($colors['header'], $padding, ' Summary:');
|
||||
}
|
||||
|
||||
$output .= $this->format($colors['classes'], $padding, $classes);
|
||||
$output .= $this->format($colors['methods'], $padding, $methods);
|
||||
|
||||
if ($hasBranchCoverage) {
|
||||
$output .= $this->format($colors['paths'], $padding, $paths);
|
||||
$output .= $this->format($colors['branches'], $padding, $branches);
|
||||
}
|
||||
$output .= $this->format($colors['lines'], $padding, $lines);
|
||||
|
||||
if ($this->showOnlySummary) {
|
||||
return $output . PHP_EOL;
|
||||
}
|
||||
|
||||
$classCoverage = [];
|
||||
|
||||
foreach ($report as $item) {
|
||||
if (!$item instanceof File) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$classes = $item->classesAndTraits();
|
||||
|
||||
foreach ($classes as $className => $class) {
|
||||
$classExecutableLines = 0;
|
||||
$classExecutedLines = 0;
|
||||
$classExecutableBranches = 0;
|
||||
$classExecutedBranches = 0;
|
||||
$classExecutablePaths = 0;
|
||||
$classExecutedPaths = 0;
|
||||
$coveredMethods = 0;
|
||||
$classMethods = 0;
|
||||
|
||||
foreach ($class['methods'] as $method) {
|
||||
if ($method['executableLines'] == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$classMethods++;
|
||||
$classExecutableLines += $method['executableLines'];
|
||||
$classExecutedLines += $method['executedLines'];
|
||||
$classExecutableBranches += $method['executableBranches'];
|
||||
$classExecutedBranches += $method['executedBranches'];
|
||||
$classExecutablePaths += $method['executablePaths'];
|
||||
$classExecutedPaths += $method['executedPaths'];
|
||||
|
||||
if ($method['coverage'] == 100) {
|
||||
$coveredMethods++;
|
||||
}
|
||||
}
|
||||
|
||||
$classCoverage[$className] = [
|
||||
'namespace' => $class['namespace'],
|
||||
'className' => $className,
|
||||
'methodsCovered' => $coveredMethods,
|
||||
'methodCount' => $classMethods,
|
||||
'statementsCovered' => $classExecutedLines,
|
||||
'statementCount' => $classExecutableLines,
|
||||
'branchesCovered' => $classExecutedBranches,
|
||||
'branchesCount' => $classExecutableBranches,
|
||||
'pathsCovered' => $classExecutedPaths,
|
||||
'pathsCount' => $classExecutablePaths,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
ksort($classCoverage);
|
||||
|
||||
$methodColor = '';
|
||||
$pathsColor = '';
|
||||
$branchesColor = '';
|
||||
$linesColor = '';
|
||||
$resetColor = '';
|
||||
|
||||
foreach ($classCoverage as $fullQualifiedPath => $classInfo) {
|
||||
if ($this->showUncoveredFiles || $classInfo['statementsCovered'] != 0) {
|
||||
if ($showColors) {
|
||||
$methodColor = $this->coverageColor($classInfo['methodsCovered'], $classInfo['methodCount']);
|
||||
$pathsColor = $this->coverageColor($classInfo['pathsCovered'], $classInfo['pathsCount']);
|
||||
$branchesColor = $this->coverageColor($classInfo['branchesCovered'], $classInfo['branchesCount']);
|
||||
$linesColor = $this->coverageColor($classInfo['statementsCovered'], $classInfo['statementCount']);
|
||||
$resetColor = $colors['reset'];
|
||||
}
|
||||
|
||||
$output .= PHP_EOL . $fullQualifiedPath . PHP_EOL
|
||||
. ' ' . $methodColor . 'Methods: ' . $this->printCoverageCounts($classInfo['methodsCovered'], $classInfo['methodCount'], 2) . $resetColor . ' ';
|
||||
|
||||
if ($hasBranchCoverage) {
|
||||
$output .= ' ' . $pathsColor . 'Paths: ' . $this->printCoverageCounts($classInfo['pathsCovered'], $classInfo['pathsCount'], 3) . $resetColor . ' '
|
||||
. ' ' . $branchesColor . 'Branches: ' . $this->printCoverageCounts($classInfo['branchesCovered'], $classInfo['branchesCount'], 3) . $resetColor . ' ';
|
||||
}
|
||||
$output .= ' ' . $linesColor . 'Lines: ' . $this->printCoverageCounts($classInfo['statementsCovered'], $classInfo['statementCount'], 3) . $resetColor;
|
||||
}
|
||||
}
|
||||
|
||||
return $output . PHP_EOL;
|
||||
}
|
||||
|
||||
private function coverageColor(int $numberOfCoveredElements, int $totalNumberOfElements): string
|
||||
{
|
||||
$coverage = Percentage::fromFractionAndTotal(
|
||||
$numberOfCoveredElements,
|
||||
$totalNumberOfElements,
|
||||
);
|
||||
|
||||
if ($coverage->asFloat() >= $this->thresholds->highLowerBound()) {
|
||||
return self::COLOR_GREEN;
|
||||
}
|
||||
|
||||
if ($coverage->asFloat() > $this->thresholds->lowUpperBound()) {
|
||||
return self::COLOR_YELLOW;
|
||||
}
|
||||
|
||||
return self::COLOR_RED;
|
||||
}
|
||||
|
||||
private function printCoverageCounts(int $numberOfCoveredElements, int $totalNumberOfElements, int $precision): string
|
||||
{
|
||||
$format = '%' . $precision . 's';
|
||||
|
||||
return Percentage::fromFractionAndTotal(
|
||||
$numberOfCoveredElements,
|
||||
$totalNumberOfElements,
|
||||
)->asFixedWidthString() .
|
||||
' (' . sprintf($format, $numberOfCoveredElements) . '/' .
|
||||
sprintf($format, $totalNumberOfElements) . ')';
|
||||
}
|
||||
|
||||
private function format(string $color, int $padding, false|string $string): string
|
||||
{
|
||||
if ($color === '') {
|
||||
return (string) $string . PHP_EOL;
|
||||
}
|
||||
|
||||
return $color . str_pad((string) $string, $padding) . self::COLOR_RESET . PHP_EOL;
|
||||
}
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of phpunit/php-code-coverage.
|
||||
*
|
||||
* (c) Sebastian Bergmann <sebastian@phpunit.de>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
namespace SebastianBergmann\CodeCoverage\Report;
|
||||
|
||||
use SebastianBergmann\CodeCoverage\InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* @psalm-immutable
|
||||
*/
|
||||
final class Thresholds
|
||||
{
|
||||
private readonly int $lowUpperBound;
|
||||
private readonly int $highLowerBound;
|
||||
|
||||
public static function default(): self
|
||||
{
|
||||
return new self(50, 90);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public static function from(int $lowUpperBound, int $highLowerBound): self
|
||||
{
|
||||
if ($lowUpperBound > $highLowerBound) {
|
||||
throw new InvalidArgumentException(
|
||||
'$lowUpperBound must not be larger than $highLowerBound',
|
||||
);
|
||||
}
|
||||
|
||||
return new self($lowUpperBound, $highLowerBound);
|
||||
}
|
||||
|
||||
private function __construct(int $lowUpperBound, int $highLowerBound)
|
||||
{
|
||||
$this->lowUpperBound = $lowUpperBound;
|
||||
$this->highLowerBound = $highLowerBound;
|
||||
}
|
||||
|
||||
public function lowUpperBound(): int
|
||||
{
|
||||
return $this->lowUpperBound;
|
||||
}
|
||||
|
||||
public function highLowerBound(): int
|
||||
{
|
||||
return $this->highLowerBound;
|
||||
}
|
||||
}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of phpunit/php-code-coverage.
|
||||
*
|
||||
* (c) Sebastian Bergmann <sebastian@phpunit.de>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
namespace SebastianBergmann\CodeCoverage\Report\Xml;
|
||||
|
||||
use function phpversion;
|
||||
use DateTimeImmutable;
|
||||
use DOMElement;
|
||||
use SebastianBergmann\Environment\Runtime;
|
||||
|
||||
/**
|
||||
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
|
||||
*/
|
||||
final class BuildInformation
|
||||
{
|
||||
private readonly DOMElement $contextNode;
|
||||
|
||||
public function __construct(DOMElement $contextNode)
|
||||
{
|
||||
$this->contextNode = $contextNode;
|
||||
}
|
||||
|
||||
public function setRuntimeInformation(Runtime $runtime): void
|
||||
{
|
||||
$runtimeNode = $this->nodeByName('runtime');
|
||||
|
||||
$runtimeNode->setAttribute('name', $runtime->getName());
|
||||
$runtimeNode->setAttribute('version', $runtime->getVersion());
|
||||
$runtimeNode->setAttribute('url', $runtime->getVendorUrl());
|
||||
|
||||
$driverNode = $this->nodeByName('driver');
|
||||
|
||||
if ($runtime->hasXdebug()) {
|
||||
$driverNode->setAttribute('name', 'xdebug');
|
||||
$driverNode->setAttribute('version', phpversion('xdebug'));
|
||||
}
|
||||
|
||||
if ($runtime->hasPCOV()) {
|
||||
$driverNode->setAttribute('name', 'pcov');
|
||||
$driverNode->setAttribute('version', phpversion('pcov'));
|
||||
}
|
||||
}
|
||||
|
||||
public function setBuildTime(DateTimeImmutable $date): void
|
||||
{
|
||||
$this->contextNode->setAttribute('time', $date->format('D M j G:i:s T Y'));
|
||||
}
|
||||
|
||||
public function setGeneratorVersions(string $phpUnitVersion, string $coverageVersion): void
|
||||
{
|
||||
$this->contextNode->setAttribute('phpunit', $phpUnitVersion);
|
||||
$this->contextNode->setAttribute('coverage', $coverageVersion);
|
||||
}
|
||||
|
||||
private function nodeByName(string $name): DOMElement
|
||||
{
|
||||
$node = $this->contextNode->getElementsByTagNameNS(
|
||||
'https://schema.phpunit.de/coverage/1.0',
|
||||
$name,
|
||||
)->item(0);
|
||||
|
||||
if (!$node) {
|
||||
$node = $this->contextNode->appendChild(
|
||||
$this->contextNode->ownerDocument->createElementNS(
|
||||
'https://schema.phpunit.de/coverage/1.0',
|
||||
$name,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return $node;
|
||||
}
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of phpunit/php-code-coverage.
|
||||
*
|
||||
* (c) Sebastian Bergmann <sebastian@phpunit.de>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
namespace SebastianBergmann\CodeCoverage\Report\Xml;
|
||||
|
||||
use DOMElement;
|
||||
use SebastianBergmann\CodeCoverage\ReportAlreadyFinalizedException;
|
||||
use XMLWriter;
|
||||
|
||||
/**
|
||||
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
|
||||
*/
|
||||
final class Coverage
|
||||
{
|
||||
private readonly XMLWriter $writer;
|
||||
private readonly DOMElement $contextNode;
|
||||
private bool $finalized = false;
|
||||
|
||||
public function __construct(DOMElement $context, string $line)
|
||||
{
|
||||
$this->contextNode = $context;
|
||||
|
||||
$this->writer = new XMLWriter;
|
||||
$this->writer->openMemory();
|
||||
$this->writer->startElementNS(null, $context->nodeName, 'https://schema.phpunit.de/coverage/1.0');
|
||||
$this->writer->writeAttribute('nr', $line);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ReportAlreadyFinalizedException
|
||||
*/
|
||||
public function addTest(string $test): void
|
||||
{
|
||||
if ($this->finalized) {
|
||||
throw new ReportAlreadyFinalizedException;
|
||||
}
|
||||
|
||||
$this->writer->startElement('covered');
|
||||
$this->writer->writeAttribute('by', $test);
|
||||
$this->writer->endElement();
|
||||
}
|
||||
|
||||
public function finalize(): void
|
||||
{
|
||||
$this->writer->endElement();
|
||||
|
||||
$fragment = $this->contextNode->ownerDocument->createDocumentFragment();
|
||||
$fragment->appendXML($this->writer->outputMemory());
|
||||
|
||||
$this->contextNode->parentNode->replaceChild(
|
||||
$fragment,
|
||||
$this->contextNode,
|
||||
);
|
||||
|
||||
$this->finalized = true;
|
||||
}
|
||||
}
|
||||
+304
@@ -0,0 +1,304 @@
|
||||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of phpunit/php-code-coverage.
|
||||
*
|
||||
* (c) Sebastian Bergmann <sebastian@phpunit.de>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
namespace SebastianBergmann\CodeCoverage\Report\Xml;
|
||||
|
||||
use const DIRECTORY_SEPARATOR;
|
||||
use 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 as FileNode;
|
||||
use SebastianBergmann\CodeCoverage\Util\Filesystem as DirectoryUtil;
|
||||
use SebastianBergmann\CodeCoverage\Version;
|
||||
use SebastianBergmann\CodeCoverage\XmlException;
|
||||
use SebastianBergmann\Environment\Runtime;
|
||||
|
||||
final class Facade
|
||||
{
|
||||
private string $target;
|
||||
private Project $project;
|
||||
private readonly string $phpUnitVersion;
|
||||
|
||||
public function __construct(string $version)
|
||||
{
|
||||
$this->phpUnitVersion = $version;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws XmlException
|
||||
*/
|
||||
public function process(CodeCoverage $coverage, string $target): void
|
||||
{
|
||||
if (substr($target, -1, 1) !== DIRECTORY_SEPARATOR) {
|
||||
$target .= DIRECTORY_SEPARATOR;
|
||||
}
|
||||
|
||||
$this->target = $target;
|
||||
$this->initTargetDirectory($target);
|
||||
|
||||
$report = $coverage->getReport();
|
||||
|
||||
$this->project = new Project(
|
||||
$coverage->getReport()->name(),
|
||||
);
|
||||
|
||||
$this->setBuildInformation();
|
||||
$this->processTests($coverage->getTests());
|
||||
$this->processDirectory($report, $this->project);
|
||||
|
||||
$this->saveDocument($this->project->asDom(), 'index');
|
||||
}
|
||||
|
||||
private function setBuildInformation(): void
|
||||
{
|
||||
$buildNode = $this->project->buildInformation();
|
||||
$buildNode->setRuntimeInformation(new Runtime);
|
||||
$buildNode->setBuildTime(new DateTimeImmutable);
|
||||
$buildNode->setGeneratorVersions($this->phpUnitVersion, Version::id());
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws PathExistsButIsNotDirectoryException
|
||||
* @throws WriteOperationFailedException
|
||||
*/
|
||||
private function initTargetDirectory(string $directory): void
|
||||
{
|
||||
if (is_file($directory)) {
|
||||
if (!is_dir($directory)) {
|
||||
throw new PathExistsButIsNotDirectoryException($directory);
|
||||
}
|
||||
|
||||
if (!is_writable($directory)) {
|
||||
throw new WriteOperationFailedException($directory);
|
||||
}
|
||||
}
|
||||
|
||||
DirectoryUtil::createDirectory($directory);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws XmlException
|
||||
*/
|
||||
private function processDirectory(DirectoryNode $directory, Node $context): void
|
||||
{
|
||||
$directoryName = $directory->name();
|
||||
|
||||
if ($this->project->projectSourceDirectory() === $directoryName) {
|
||||
$directoryName = '/';
|
||||
}
|
||||
|
||||
$directoryObject = $context->addDirectory($directoryName);
|
||||
|
||||
$this->setTotals($directory, $directoryObject->totals());
|
||||
|
||||
foreach ($directory->directories() as $node) {
|
||||
$this->processDirectory($node, $directoryObject);
|
||||
}
|
||||
|
||||
foreach ($directory->files() as $node) {
|
||||
$this->processFile($node, $directoryObject);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws XmlException
|
||||
*/
|
||||
private function processFile(FileNode $file, Directory $context): void
|
||||
{
|
||||
$fileObject = $context->addFile(
|
||||
$file->name(),
|
||||
$file->id() . '.xml',
|
||||
);
|
||||
|
||||
$this->setTotals($file, $fileObject->totals());
|
||||
|
||||
$path = substr(
|
||||
$file->pathAsString(),
|
||||
strlen($this->project->projectSourceDirectory()),
|
||||
);
|
||||
|
||||
$fileReport = new Report($path);
|
||||
|
||||
$this->setTotals($file, $fileReport->totals());
|
||||
|
||||
foreach ($file->classesAndTraits() as $unit) {
|
||||
$this->processUnit($unit, $fileReport);
|
||||
}
|
||||
|
||||
foreach ($file->functions() as $function) {
|
||||
$this->processFunction($function, $fileReport);
|
||||
}
|
||||
|
||||
foreach ($file->lineCoverageData() as $line => $tests) {
|
||||
if (!is_array($tests) || count($tests) === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$coverage = $fileReport->lineCoverage((string) $line);
|
||||
|
||||
foreach ($tests as $test) {
|
||||
$coverage->addTest($test);
|
||||
}
|
||||
|
||||
$coverage->finalize();
|
||||
}
|
||||
|
||||
$fileReport->source()->setSourceCode(
|
||||
file_get_contents($file->pathAsString()),
|
||||
);
|
||||
|
||||
$this->saveDocument($fileReport->asDom(), $file->id());
|
||||
}
|
||||
|
||||
private function processUnit(array $unit, Report $report): void
|
||||
{
|
||||
if (isset($unit['className'])) {
|
||||
$unitObject = $report->classObject($unit['className']);
|
||||
} else {
|
||||
$unitObject = $report->traitObject($unit['traitName']);
|
||||
}
|
||||
|
||||
$unitObject->setLines(
|
||||
$unit['startLine'],
|
||||
$unit['executableLines'],
|
||||
$unit['executedLines'],
|
||||
);
|
||||
|
||||
$unitObject->setCrap((float) $unit['crap']);
|
||||
$unitObject->setNamespace($unit['namespace']);
|
||||
|
||||
foreach ($unit['methods'] as $method) {
|
||||
$methodObject = $unitObject->addMethod($method['methodName']);
|
||||
$methodObject->setSignature($method['signature']);
|
||||
$methodObject->setLines((string) $method['startLine'], (string) $method['endLine']);
|
||||
$methodObject->setCrap($method['crap']);
|
||||
$methodObject->setTotals(
|
||||
(string) $method['executableLines'],
|
||||
(string) $method['executedLines'],
|
||||
(string) $method['coverage'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private function processFunction(array $function, Report $report): void
|
||||
{
|
||||
$functionObject = $report->functionObject($function['functionName']);
|
||||
|
||||
$functionObject->setSignature($function['signature']);
|
||||
$functionObject->setLines((string) $function['startLine']);
|
||||
$functionObject->setCrap($function['crap']);
|
||||
$functionObject->setTotals((string) $function['executableLines'], (string) $function['executedLines'], (string) $function['coverage']);
|
||||
}
|
||||
|
||||
private function processTests(array $tests): void
|
||||
{
|
||||
$testsObject = $this->project->tests();
|
||||
|
||||
foreach ($tests as $test => $result) {
|
||||
$testsObject->addTest($test, $result);
|
||||
}
|
||||
}
|
||||
|
||||
private function setTotals(AbstractNode $node, Totals $totals): void
|
||||
{
|
||||
$loc = $node->linesOfCode();
|
||||
|
||||
$totals->setNumLines(
|
||||
$loc['linesOfCode'],
|
||||
$loc['commentLinesOfCode'],
|
||||
$loc['nonCommentLinesOfCode'],
|
||||
$node->numberOfExecutableLines(),
|
||||
$node->numberOfExecutedLines(),
|
||||
);
|
||||
|
||||
$totals->setNumClasses(
|
||||
$node->numberOfClasses(),
|
||||
$node->numberOfTestedClasses(),
|
||||
);
|
||||
|
||||
$totals->setNumTraits(
|
||||
$node->numberOfTraits(),
|
||||
$node->numberOfTestedTraits(),
|
||||
);
|
||||
|
||||
$totals->setNumMethods(
|
||||
$node->numberOfMethods(),
|
||||
$node->numberOfTestedMethods(),
|
||||
);
|
||||
|
||||
$totals->setNumFunctions(
|
||||
$node->numberOfFunctions(),
|
||||
$node->numberOfTestedFunctions(),
|
||||
);
|
||||
}
|
||||
|
||||
private function targetDirectory(): string
|
||||
{
|
||||
return $this->target;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws XmlException
|
||||
*/
|
||||
private function saveDocument(DOMDocument $document, string $name): void
|
||||
{
|
||||
$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;
|
||||
}
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of phpunit/php-code-coverage.
|
||||
*
|
||||
* (c) Sebastian Bergmann <sebastian@phpunit.de>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
namespace SebastianBergmann\CodeCoverage\Report\Xml;
|
||||
|
||||
use DOMDocument;
|
||||
use DOMElement;
|
||||
|
||||
/**
|
||||
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
|
||||
*/
|
||||
class File
|
||||
{
|
||||
private readonly DOMDocument $dom;
|
||||
private readonly DOMElement $contextNode;
|
||||
|
||||
public function __construct(DOMElement $context)
|
||||
{
|
||||
$this->dom = $context->ownerDocument;
|
||||
$this->contextNode = $context;
|
||||
}
|
||||
|
||||
public function totals(): Totals
|
||||
{
|
||||
$totalsContainer = $this->contextNode->firstChild;
|
||||
|
||||
if (!$totalsContainer) {
|
||||
$totalsContainer = $this->contextNode->appendChild(
|
||||
$this->dom->createElementNS(
|
||||
'https://schema.phpunit.de/coverage/1.0',
|
||||
'totals',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return new Totals($totalsContainer);
|
||||
}
|
||||
|
||||
public function lineCoverage(string $line): Coverage
|
||||
{
|
||||
$coverage = $this->contextNode->getElementsByTagNameNS(
|
||||
'https://schema.phpunit.de/coverage/1.0',
|
||||
'coverage',
|
||||
)->item(0);
|
||||
|
||||
if (!$coverage) {
|
||||
$coverage = $this->contextNode->appendChild(
|
||||
$this->dom->createElementNS(
|
||||
'https://schema.phpunit.de/coverage/1.0',
|
||||
'coverage',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
$lineNode = $coverage->appendChild(
|
||||
$this->dom->createElementNS(
|
||||
'https://schema.phpunit.de/coverage/1.0',
|
||||
'line',
|
||||
),
|
||||
);
|
||||
|
||||
return new Coverage($lineNode, $line);
|
||||
}
|
||||
|
||||
protected function contextNode(): DOMElement
|
||||
{
|
||||
return $this->contextNode;
|
||||
}
|
||||
|
||||
protected function dom(): DOMDocument
|
||||
{
|
||||
return $this->dom;
|
||||
}
|
||||
}
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of phpunit/php-code-coverage.
|
||||
*
|
||||
* (c) Sebastian Bergmann <sebastian@phpunit.de>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
namespace SebastianBergmann\CodeCoverage\Report\Xml;
|
||||
|
||||
use DOMDocument;
|
||||
use DOMElement;
|
||||
|
||||
/**
|
||||
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
|
||||
*/
|
||||
abstract class Node
|
||||
{
|
||||
private DOMDocument $dom;
|
||||
private DOMElement $contextNode;
|
||||
|
||||
public function __construct(DOMElement $context)
|
||||
{
|
||||
$this->setContextNode($context);
|
||||
}
|
||||
|
||||
public function dom(): DOMDocument
|
||||
{
|
||||
return $this->dom;
|
||||
}
|
||||
|
||||
public function totals(): Totals
|
||||
{
|
||||
$totalsContainer = $this->contextNode()->firstChild;
|
||||
|
||||
if (!$totalsContainer) {
|
||||
$totalsContainer = $this->contextNode()->appendChild(
|
||||
$this->dom->createElementNS(
|
||||
'https://schema.phpunit.de/coverage/1.0',
|
||||
'totals',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return new Totals($totalsContainer);
|
||||
}
|
||||
|
||||
public function addDirectory(string $name): Directory
|
||||
{
|
||||
$dirNode = $this->dom()->createElementNS(
|
||||
'https://schema.phpunit.de/coverage/1.0',
|
||||
'directory',
|
||||
);
|
||||
|
||||
$dirNode->setAttribute('name', $name);
|
||||
$this->contextNode()->appendChild($dirNode);
|
||||
|
||||
return new Directory($dirNode);
|
||||
}
|
||||
|
||||
public function addFile(string $name, string $href): File
|
||||
{
|
||||
$fileNode = $this->dom()->createElementNS(
|
||||
'https://schema.phpunit.de/coverage/1.0',
|
||||
'file',
|
||||
);
|
||||
|
||||
$fileNode->setAttribute('name', $name);
|
||||
$fileNode->setAttribute('href', $href);
|
||||
$this->contextNode()->appendChild($fileNode);
|
||||
|
||||
return new File($fileNode);
|
||||
}
|
||||
|
||||
protected function setContextNode(DOMElement $context): void
|
||||
{
|
||||
$this->dom = $context->ownerDocument;
|
||||
$this->contextNode = $context;
|
||||
}
|
||||
|
||||
protected function contextNode(): DOMElement
|
||||
{
|
||||
return $this->contextNode;
|
||||
}
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of phpunit/php-code-coverage.
|
||||
*
|
||||
* (c) Sebastian Bergmann <sebastian@phpunit.de>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
namespace SebastianBergmann\CodeCoverage\Report\Xml;
|
||||
|
||||
use DOMDocument;
|
||||
|
||||
/**
|
||||
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
|
||||
*/
|
||||
final class Project extends Node
|
||||
{
|
||||
public function __construct(string $directory)
|
||||
{
|
||||
$this->init();
|
||||
$this->setProjectSourceDirectory($directory);
|
||||
}
|
||||
|
||||
public function projectSourceDirectory(): string
|
||||
{
|
||||
return $this->contextNode()->getAttribute('source');
|
||||
}
|
||||
|
||||
public function buildInformation(): BuildInformation
|
||||
{
|
||||
$buildNode = $this->dom()->getElementsByTagNameNS(
|
||||
'https://schema.phpunit.de/coverage/1.0',
|
||||
'build',
|
||||
)->item(0);
|
||||
|
||||
if (!$buildNode) {
|
||||
$buildNode = $this->dom()->documentElement->appendChild(
|
||||
$this->dom()->createElementNS(
|
||||
'https://schema.phpunit.de/coverage/1.0',
|
||||
'build',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return new BuildInformation($buildNode);
|
||||
}
|
||||
|
||||
public function tests(): Tests
|
||||
{
|
||||
$testsNode = $this->contextNode()->getElementsByTagNameNS(
|
||||
'https://schema.phpunit.de/coverage/1.0',
|
||||
'tests',
|
||||
)->item(0);
|
||||
|
||||
if (!$testsNode) {
|
||||
$testsNode = $this->contextNode()->appendChild(
|
||||
$this->dom()->createElementNS(
|
||||
'https://schema.phpunit.de/coverage/1.0',
|
||||
'tests',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return new Tests($testsNode);
|
||||
}
|
||||
|
||||
public function asDom(): DOMDocument
|
||||
{
|
||||
return $this->dom();
|
||||
}
|
||||
|
||||
private function init(): void
|
||||
{
|
||||
$dom = new DOMDocument;
|
||||
$dom->loadXML('<?xml version="1.0" ?><phpunit xmlns="https://schema.phpunit.de/coverage/1.0"><build/><project/></phpunit>');
|
||||
|
||||
$this->setContextNode(
|
||||
$dom->getElementsByTagNameNS(
|
||||
'https://schema.phpunit.de/coverage/1.0',
|
||||
'project',
|
||||
)->item(0),
|
||||
);
|
||||
}
|
||||
|
||||
private function setProjectSourceDirectory(string $name): void
|
||||
{
|
||||
$this->contextNode()->setAttribute('source', $name);
|
||||
}
|
||||
}
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of phpunit/php-code-coverage.
|
||||
*
|
||||
* (c) Sebastian Bergmann <sebastian@phpunit.de>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
namespace SebastianBergmann\CodeCoverage\Report\Xml;
|
||||
|
||||
use function basename;
|
||||
use function dirname;
|
||||
use DOMDocument;
|
||||
|
||||
/**
|
||||
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
|
||||
*/
|
||||
final class Report extends File
|
||||
{
|
||||
public function __construct(string $name)
|
||||
{
|
||||
$dom = new DOMDocument;
|
||||
$dom->loadXML('<?xml version="1.0" ?><phpunit xmlns="https://schema.phpunit.de/coverage/1.0"><file /></phpunit>');
|
||||
|
||||
$contextNode = $dom->getElementsByTagNameNS(
|
||||
'https://schema.phpunit.de/coverage/1.0',
|
||||
'file',
|
||||
)->item(0);
|
||||
|
||||
parent::__construct($contextNode);
|
||||
|
||||
$this->setName($name);
|
||||
}
|
||||
|
||||
public function asDom(): DOMDocument
|
||||
{
|
||||
return $this->dom();
|
||||
}
|
||||
|
||||
public function functionObject($name): Method
|
||||
{
|
||||
$node = $this->contextNode()->appendChild(
|
||||
$this->dom()->createElementNS(
|
||||
'https://schema.phpunit.de/coverage/1.0',
|
||||
'function',
|
||||
),
|
||||
);
|
||||
|
||||
return new Method($node, $name);
|
||||
}
|
||||
|
||||
public function classObject($name): Unit
|
||||
{
|
||||
return $this->unitObject('class', $name);
|
||||
}
|
||||
|
||||
public function traitObject($name): Unit
|
||||
{
|
||||
return $this->unitObject('trait', $name);
|
||||
}
|
||||
|
||||
public function source(): Source
|
||||
{
|
||||
$source = $this->contextNode()->getElementsByTagNameNS(
|
||||
'https://schema.phpunit.de/coverage/1.0',
|
||||
'source',
|
||||
)->item(0);
|
||||
|
||||
if (!$source) {
|
||||
$source = $this->contextNode()->appendChild(
|
||||
$this->dom()->createElementNS(
|
||||
'https://schema.phpunit.de/coverage/1.0',
|
||||
'source',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return new Source($source);
|
||||
}
|
||||
|
||||
private function setName(string $name): void
|
||||
{
|
||||
$this->contextNode()->setAttribute('name', basename($name));
|
||||
$this->contextNode()->setAttribute('path', dirname($name));
|
||||
}
|
||||
|
||||
private function unitObject(string $tagName, $name): Unit
|
||||
{
|
||||
$node = $this->contextNode()->appendChild(
|
||||
$this->dom()->createElementNS(
|
||||
'https://schema.phpunit.de/coverage/1.0',
|
||||
$tagName,
|
||||
),
|
||||
);
|
||||
|
||||
return new Unit($node, $name);
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of phpunit/php-code-coverage.
|
||||
*
|
||||
* (c) Sebastian Bergmann <sebastian@phpunit.de>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
namespace SebastianBergmann\CodeCoverage\Report\Xml;
|
||||
|
||||
use DOMElement;
|
||||
use TheSeer\Tokenizer\NamespaceUri;
|
||||
use TheSeer\Tokenizer\Tokenizer;
|
||||
use TheSeer\Tokenizer\XMLSerializer;
|
||||
|
||||
/**
|
||||
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
|
||||
*/
|
||||
final class Source
|
||||
{
|
||||
private readonly DOMElement $context;
|
||||
|
||||
public function __construct(DOMElement $context)
|
||||
{
|
||||
$this->context = $context;
|
||||
}
|
||||
|
||||
public function setSourceCode(string $source): void
|
||||
{
|
||||
$context = $this->context;
|
||||
|
||||
$tokens = (new Tokenizer)->parse($source);
|
||||
$srcDom = (new XMLSerializer(new NamespaceUri($context->namespaceURI)))->toDom($tokens);
|
||||
|
||||
$context->parentNode->replaceChild(
|
||||
$context->ownerDocument->importNode($srcDom->documentElement, true),
|
||||
$context,
|
||||
);
|
||||
}
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of phpunit/php-code-coverage.
|
||||
*
|
||||
* (c) Sebastian Bergmann <sebastian@phpunit.de>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
namespace SebastianBergmann\CodeCoverage\Report\Xml;
|
||||
|
||||
use DOMElement;
|
||||
|
||||
/**
|
||||
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
|
||||
*
|
||||
* @psalm-import-type TestType from \SebastianBergmann\CodeCoverage\CodeCoverage
|
||||
*/
|
||||
final class Tests
|
||||
{
|
||||
private readonly DOMElement $contextNode;
|
||||
|
||||
public function __construct(DOMElement $context)
|
||||
{
|
||||
$this->contextNode = $context;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TestType $result
|
||||
*/
|
||||
public function addTest(string $test, array $result): void
|
||||
{
|
||||
$node = $this->contextNode->appendChild(
|
||||
$this->contextNode->ownerDocument->createElementNS(
|
||||
'https://schema.phpunit.de/coverage/1.0',
|
||||
'test',
|
||||
),
|
||||
);
|
||||
|
||||
$node->setAttribute('name', $test);
|
||||
$node->setAttribute('size', $result['size']);
|
||||
$node->setAttribute('status', $result['status']);
|
||||
}
|
||||
}
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of phpunit/php-code-coverage.
|
||||
*
|
||||
* (c) Sebastian Bergmann <sebastian@phpunit.de>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
namespace SebastianBergmann\CodeCoverage\Report\Xml;
|
||||
|
||||
use 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
|
||||
{
|
||||
private readonly DOMNode $container;
|
||||
private readonly DOMElement $linesNode;
|
||||
private readonly DOMElement $methodsNode;
|
||||
private readonly DOMElement $functionsNode;
|
||||
private readonly DOMElement $classesNode;
|
||||
private readonly DOMElement $traitsNode;
|
||||
|
||||
public function __construct(DOMElement $container)
|
||||
{
|
||||
$this->container = $container;
|
||||
$dom = $container->ownerDocument;
|
||||
|
||||
$this->linesNode = $dom->createElementNS(
|
||||
'https://schema.phpunit.de/coverage/1.0',
|
||||
'lines',
|
||||
);
|
||||
|
||||
$this->methodsNode = $dom->createElementNS(
|
||||
'https://schema.phpunit.de/coverage/1.0',
|
||||
'methods',
|
||||
);
|
||||
|
||||
$this->functionsNode = $dom->createElementNS(
|
||||
'https://schema.phpunit.de/coverage/1.0',
|
||||
'functions',
|
||||
);
|
||||
|
||||
$this->classesNode = $dom->createElementNS(
|
||||
'https://schema.phpunit.de/coverage/1.0',
|
||||
'classes',
|
||||
);
|
||||
|
||||
$this->traitsNode = $dom->createElementNS(
|
||||
'https://schema.phpunit.de/coverage/1.0',
|
||||
'traits',
|
||||
);
|
||||
|
||||
$container->appendChild($this->linesNode);
|
||||
$container->appendChild($this->methodsNode);
|
||||
$container->appendChild($this->functionsNode);
|
||||
$container->appendChild($this->classesNode);
|
||||
$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);
|
||||
$this->linesNode->setAttribute('comments', (string) $cloc);
|
||||
$this->linesNode->setAttribute('code', (string) $ncloc);
|
||||
$this->linesNode->setAttribute('executable', (string) $executable);
|
||||
$this->linesNode->setAttribute('executed', (string) $executed);
|
||||
$this->linesNode->setAttribute(
|
||||
'percent',
|
||||
$executable === 0 ? '0' : sprintf('%01.2F', Percentage::fromFractionAndTotal($executed, $executable)->asFloat()),
|
||||
);
|
||||
}
|
||||
|
||||
public function setNumClasses(int $count, int $tested): void
|
||||
{
|
||||
$this->classesNode->setAttribute('count', (string) $count);
|
||||
$this->classesNode->setAttribute('tested', (string) $tested);
|
||||
$this->classesNode->setAttribute(
|
||||
'percent',
|
||||
$count === 0 ? '0' : sprintf('%01.2F', Percentage::fromFractionAndTotal($tested, $count)->asFloat()),
|
||||
);
|
||||
}
|
||||
|
||||
public function setNumTraits(int $count, int $tested): void
|
||||
{
|
||||
$this->traitsNode->setAttribute('count', (string) $count);
|
||||
$this->traitsNode->setAttribute('tested', (string) $tested);
|
||||
$this->traitsNode->setAttribute(
|
||||
'percent',
|
||||
$count === 0 ? '0' : sprintf('%01.2F', Percentage::fromFractionAndTotal($tested, $count)->asFloat()),
|
||||
);
|
||||
}
|
||||
|
||||
public function setNumMethods(int $count, int $tested): void
|
||||
{
|
||||
$this->methodsNode->setAttribute('count', (string) $count);
|
||||
$this->methodsNode->setAttribute('tested', (string) $tested);
|
||||
$this->methodsNode->setAttribute(
|
||||
'percent',
|
||||
$count === 0 ? '0' : sprintf('%01.2F', Percentage::fromFractionAndTotal($tested, $count)->asFloat()),
|
||||
);
|
||||
}
|
||||
|
||||
public function setNumFunctions(int $count, int $tested): void
|
||||
{
|
||||
$this->functionsNode->setAttribute('count', (string) $count);
|
||||
$this->functionsNode->setAttribute('tested', (string) $tested);
|
||||
$this->functionsNode->setAttribute(
|
||||
'percent',
|
||||
$count === 0 ? '0' : sprintf('%01.2F', Percentage::fromFractionAndTotal($tested, $count)->asFloat()),
|
||||
);
|
||||
}
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of phpunit/php-code-coverage.
|
||||
*
|
||||
* (c) Sebastian Bergmann <sebastian@phpunit.de>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
namespace SebastianBergmann\CodeCoverage\Report\Xml;
|
||||
|
||||
use DOMElement;
|
||||
|
||||
/**
|
||||
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
|
||||
*/
|
||||
final class Unit
|
||||
{
|
||||
private readonly DOMElement $contextNode;
|
||||
|
||||
public function __construct(DOMElement $context, string $name)
|
||||
{
|
||||
$this->contextNode = $context;
|
||||
|
||||
$this->setName($name);
|
||||
}
|
||||
|
||||
public function setLines(int $start, int $executable, int $executed): void
|
||||
{
|
||||
$this->contextNode->setAttribute('start', (string) $start);
|
||||
$this->contextNode->setAttribute('executable', (string) $executable);
|
||||
$this->contextNode->setAttribute('executed', (string) $executed);
|
||||
}
|
||||
|
||||
public function setCrap(float $crap): void
|
||||
{
|
||||
$this->contextNode->setAttribute('crap', (string) $crap);
|
||||
}
|
||||
|
||||
public function setNamespace(string $namespace): void
|
||||
{
|
||||
$node = $this->contextNode->getElementsByTagNameNS(
|
||||
'https://schema.phpunit.de/coverage/1.0',
|
||||
'namespace',
|
||||
)->item(0);
|
||||
|
||||
if (!$node) {
|
||||
$node = $this->contextNode->appendChild(
|
||||
$this->contextNode->ownerDocument->createElementNS(
|
||||
'https://schema.phpunit.de/coverage/1.0',
|
||||
'namespace',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
$node->setAttribute('name', $namespace);
|
||||
}
|
||||
|
||||
public function addMethod(string $name): Method
|
||||
{
|
||||
$node = $this->contextNode->appendChild(
|
||||
$this->contextNode->ownerDocument->createElementNS(
|
||||
'https://schema.phpunit.de/coverage/1.0',
|
||||
'method',
|
||||
),
|
||||
);
|
||||
|
||||
return new Method($node, $name);
|
||||
}
|
||||
|
||||
private function setName(string $name): void
|
||||
{
|
||||
$this->contextNode->setAttribute('name', $name);
|
||||
}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
<?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 SebastianBergmann\CodeCoverage\Filter;
|
||||
|
||||
final class CacheWarmer
|
||||
{
|
||||
public function warmCache(string $cacheDirectory, bool $useAnnotationsForIgnoringCode, bool $ignoreDeprecatedCode, Filter $filter): void
|
||||
{
|
||||
$analyser = new CachingFileAnalyser(
|
||||
$cacheDirectory,
|
||||
new ParsingFileAnalyser(
|
||||
$useAnnotationsForIgnoringCode,
|
||||
$ignoreDeprecatedCode,
|
||||
),
|
||||
$useAnnotationsForIgnoringCode,
|
||||
$ignoreDeprecatedCode,
|
||||
);
|
||||
|
||||
foreach ($filter->files() as $file) {
|
||||
$analyser->process($file);
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+182
@@ -0,0 +1,182 @@
|
||||
<?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 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\FileIterator\Facade as FileIteratorFacade;
|
||||
|
||||
/**
|
||||
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
|
||||
*
|
||||
* @psalm-import-type LinesOfCodeType from \SebastianBergmann\CodeCoverage\StaticAnalysis\FileAnalyser
|
||||
*/
|
||||
final class CachingFileAnalyser implements FileAnalyser
|
||||
{
|
||||
private static ?string $cacheVersion = null;
|
||||
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;
|
||||
}
|
||||
|
||||
public function classesIn(string $filename): array
|
||||
{
|
||||
if (!isset($this->cache[$filename])) {
|
||||
$this->process($filename);
|
||||
}
|
||||
|
||||
return $this->cache[$filename]['classesIn'];
|
||||
}
|
||||
|
||||
public function traitsIn(string $filename): array
|
||||
{
|
||||
if (!isset($this->cache[$filename])) {
|
||||
$this->process($filename);
|
||||
}
|
||||
|
||||
return $this->cache[$filename]['traitsIn'];
|
||||
}
|
||||
|
||||
public function functionsIn(string $filename): array
|
||||
{
|
||||
if (!isset($this->cache[$filename])) {
|
||||
$this->process($filename);
|
||||
}
|
||||
|
||||
return $this->cache[$filename]['functionsIn'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-return LinesOfCodeType
|
||||
*/
|
||||
public function linesOfCodeFor(string $filename): array
|
||||
{
|
||||
if (!isset($this->cache[$filename])) {
|
||||
$this->process($filename);
|
||||
}
|
||||
|
||||
return $this->cache[$filename]['linesOfCodeFor'];
|
||||
}
|
||||
|
||||
public function executableLinesIn(string $filename): array
|
||||
{
|
||||
if (!isset($this->cache[$filename])) {
|
||||
$this->process($filename);
|
||||
}
|
||||
|
||||
return $this->cache[$filename]['executableLinesIn'];
|
||||
}
|
||||
|
||||
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),
|
||||
self::cacheVersion(),
|
||||
$this->useAnnotationsForIgnoringCode,
|
||||
$this->ignoreDeprecatedCode,
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
return $this->directory . DIRECTORY_SEPARATOR . $cacheKey;
|
||||
}
|
||||
|
||||
private static function cacheVersion(): string
|
||||
{
|
||||
if (self::$cacheVersion !== null) {
|
||||
return self::$cacheVersion;
|
||||
}
|
||||
|
||||
$buffer = [];
|
||||
|
||||
foreach ((new FileIteratorFacade)->getFilesAsArray(__DIR__, '.php') as $file) {
|
||||
$buffer[] = $file;
|
||||
$buffer[] = file_get_contents($file);
|
||||
}
|
||||
|
||||
self::$cacheVersion = md5(implode("\0", $buffer));
|
||||
|
||||
return self::$cacheVersion;
|
||||
}
|
||||
}
|
||||
docker/streamline-src/vendor/phpunit/php-code-coverage/src/StaticAnalysis/CodeUnitFindingVisitor.php
Vendored
+360
@@ -0,0 +1,360 @@
|
||||
<?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
|
||||
*
|
||||
* @psalm-type CodeUnitFunctionType = array{
|
||||
* name: string,
|
||||
* namespacedName: string,
|
||||
* namespace: string,
|
||||
* signature: string,
|
||||
* startLine: int,
|
||||
* endLine: int,
|
||||
* ccn: int
|
||||
* }
|
||||
* @psalm-type CodeUnitMethodType = array{
|
||||
* methodName: string,
|
||||
* signature: string,
|
||||
* visibility: string,
|
||||
* startLine: int,
|
||||
* endLine: int,
|
||||
* ccn: int
|
||||
* }
|
||||
* @psalm-type CodeUnitClassType = array{
|
||||
* name: string,
|
||||
* namespacedName: string,
|
||||
* namespace: string,
|
||||
* startLine: int,
|
||||
* endLine: int,
|
||||
* methods: array<string, CodeUnitMethodType>
|
||||
* }
|
||||
* @psalm-type CodeUnitTraitType = array{
|
||||
* name: string,
|
||||
* namespacedName: string,
|
||||
* namespace: string,
|
||||
* startLine: int,
|
||||
* endLine: int,
|
||||
* methods: array<string, CodeUnitMethodType>
|
||||
* }
|
||||
*/
|
||||
final class CodeUnitFindingVisitor extends NodeVisitorAbstract
|
||||
{
|
||||
/**
|
||||
* @psalm-var array<string, CodeUnitClassType>
|
||||
*/
|
||||
private array $classes = [];
|
||||
|
||||
/**
|
||||
* @psalm-var array<string, CodeUnitTraitType>
|
||||
*/
|
||||
private array $traits = [];
|
||||
|
||||
/**
|
||||
* @psalm-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);
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-return array<string, CodeUnitClassType>
|
||||
*/
|
||||
public function classes(): array
|
||||
{
|
||||
return $this->classes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-return array<string, CodeUnitTraitType>
|
||||
*/
|
||||
public function traits(): array
|
||||
{
|
||||
return $this->traits;
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-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();
|
||||
}
|
||||
}
|
||||
+413
@@ -0,0 +1,413 @@
|
||||
<?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
|
||||
*
|
||||
* @psalm-import-type LinesType from \SebastianBergmann\CodeCoverage\StaticAnalysis\FileAnalyser
|
||||
*/
|
||||
final class ExecutableLinesFindingVisitor extends NodeVisitorAbstract
|
||||
{
|
||||
private int $nextBranch = 0;
|
||||
private readonly string $source;
|
||||
|
||||
/**
|
||||
* @psalm-var LinesType
|
||||
*/
|
||||
private array $executableLinesGroupedByBranch = [];
|
||||
|
||||
/**
|
||||
* @psalm-var array<int, bool>
|
||||
*/
|
||||
private array $unsets = [];
|
||||
|
||||
/**
|
||||
* @psalm-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;
|
||||
}
|
||||
|
||||
/*
|
||||
* nikic/php-parser ^4.18 represents <code>throw</code> statements
|
||||
* as <code>Stmt\Throw_</code> objects
|
||||
*/
|
||||
if ($node instanceof Node\Stmt\Throw_) {
|
||||
$this->setLineBranch($node->expr->getEndLine(), $node->expr->getEndLine(), ++$this->nextBranch);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
* nikic/php-parser ^5 represents <code>throw</code> statements
|
||||
* as <code>Stmt\Expression</code> objects that contain an
|
||||
* <code>Expr\Throw_</code> object
|
||||
*/
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
<?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
|
||||
{
|
||||
/**
|
||||
* @psalm-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);
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+247
@@ -0,0 +1,247 @@
|
||||
<?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_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
|
||||
*
|
||||
* @psalm-import-type CodeUnitFunctionType from \SebastianBergmann\CodeCoverage\StaticAnalysis\CodeUnitFindingVisitor
|
||||
* @psalm-import-type CodeUnitMethodType from \SebastianBergmann\CodeCoverage\StaticAnalysis\CodeUnitFindingVisitor
|
||||
* @psalm-import-type CodeUnitClassType from \SebastianBergmann\CodeCoverage\StaticAnalysis\CodeUnitFindingVisitor
|
||||
* @psalm-import-type CodeUnitTraitType from \SebastianBergmann\CodeCoverage\StaticAnalysis\CodeUnitFindingVisitor
|
||||
* @psalm-import-type LinesOfCodeType from \SebastianBergmann\CodeCoverage\StaticAnalysis\FileAnalyser
|
||||
* @psalm-import-type LinesType from \SebastianBergmann\CodeCoverage\StaticAnalysis\FileAnalyser
|
||||
*/
|
||||
final class ParsingFileAnalyser implements FileAnalyser
|
||||
{
|
||||
/**
|
||||
* @psalm-var array<string, array<string, CodeUnitClassType>>
|
||||
*/
|
||||
private array $classes = [];
|
||||
|
||||
/**
|
||||
* @psalm-var array<string, array<string, CodeUnitTraitType>>
|
||||
*/
|
||||
private array $traits = [];
|
||||
|
||||
/**
|
||||
* @psalm-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;
|
||||
}
|
||||
|
||||
public function classesIn(string $filename): array
|
||||
{
|
||||
$this->analyse($filename);
|
||||
|
||||
return $this->classes[$filename];
|
||||
}
|
||||
|
||||
public function traitsIn(string $filename): array
|
||||
{
|
||||
$this->analyse($filename);
|
||||
|
||||
return $this->traits[$filename];
|
||||
}
|
||||
|
||||
public function functionsIn(string $filename): array
|
||||
{
|
||||
$this->analyse($filename);
|
||||
|
||||
return $this->functions[$filename];
|
||||
}
|
||||
|
||||
public function linesOfCodeFor(string $filename): array
|
||||
{
|
||||
$this->analyse($filename);
|
||||
|
||||
return $this->linesOfCode[$filename];
|
||||
}
|
||||
|
||||
public function executableLinesIn(string $filename): array
|
||||
{
|
||||
$this->analyse($filename);
|
||||
|
||||
return $this->executableLines[$filename];
|
||||
}
|
||||
|
||||
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]),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
<?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\Util;
|
||||
|
||||
use function is_dir;
|
||||
use function mkdir;
|
||||
use function sprintf;
|
||||
|
||||
/**
|
||||
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
|
||||
*/
|
||||
final class Filesystem
|
||||
{
|
||||
/**
|
||||
* @throws DirectoryCouldNotBeCreatedException
|
||||
*/
|
||||
public static function createDirectory(string $directory): void
|
||||
{
|
||||
$success = !(!is_dir($directory) && !@mkdir($directory, 0o777, true) && !is_dir($directory));
|
||||
|
||||
if (!$success) {
|
||||
throw new DirectoryCouldNotBeCreatedException(
|
||||
sprintf(
|
||||
'Directory "%s" could not be created',
|
||||
$directory,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?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 function dirname;
|
||||
use SebastianBergmann\Version as VersionId;
|
||||
|
||||
final class Version
|
||||
{
|
||||
private static string $version = '';
|
||||
|
||||
public static function id(): string
|
||||
{
|
||||
if (self::$version === '') {
|
||||
self::$version = (new VersionId('10.1.16', dirname(__DIR__)))->asString();
|
||||
}
|
||||
|
||||
return self::$version;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,406 @@
|
||||
# Changes in PHPUnit 10.5
|
||||
|
||||
All notable changes of the PHPUnit 10.5 release series are documented in this file using the [Keep a CHANGELOG](https://keepachangelog.com/) principles.
|
||||
|
||||
## [10.5.40] - 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
|
||||
|
||||
## [10.5.39] - 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
|
||||
|
||||
## [10.5.38] - 2024-10-28
|
||||
|
||||
### Changed
|
||||
|
||||
* [#6012](https://github.com/sebastianbergmann/phpunit/pull/6012): Remove empty lines between TeamCity events
|
||||
|
||||
## [10.5.37] - 2024-10-19
|
||||
|
||||
### Fixed
|
||||
|
||||
* [#5982](https://github.com/sebastianbergmann/phpunit/pull/5982): Typo in exception message
|
||||
|
||||
## [10.5.36] - 2024-10-08
|
||||
|
||||
### Changed
|
||||
|
||||
* [#5957](https://github.com/sebastianbergmann/phpunit/pull/5957): Skip data provider build when requirements are not satisfied
|
||||
* [#5969](https://github.com/sebastianbergmann/phpunit/pull/5969): Check for requirements before creating a separate process
|
||||
* Updated regular expressions used by `StringMatchesFormatDescription` constraint to be consistent with PHP's `run-tests.php`
|
||||
|
||||
### Fixed
|
||||
|
||||
* [#5965](https://github.com/sebastianbergmann/phpunit/issues/5965): `PHPUnit\Framework\Exception` does not handle string error codes (`PDOException` with error code `'HY000'`, for example)
|
||||
|
||||
## [10.5.35] - 2024-09-19
|
||||
|
||||
### Changed
|
||||
|
||||
* [#5956](https://github.com/sebastianbergmann/phpunit/issues/5956): Deprecation of the `E_STRICT` constant in PHP 8.4
|
||||
|
||||
### Fixed
|
||||
|
||||
* [#5950](https://github.com/sebastianbergmann/phpunit/pull/5950): TestDox text should not be `trim()`med when it contains `$` character
|
||||
* The attribute parser will no longer try to instantiate attribute classes that do not exist
|
||||
|
||||
## [10.5.34] - 2024-09-13
|
||||
|
||||
### Fixed
|
||||
|
||||
* [#5931](https://github.com/sebastianbergmann/phpunit/pull/5931): Reverted addition of `name` property on `<testsuites>` element in JUnit XML logfile
|
||||
* [#5946](https://github.com/sebastianbergmann/phpunit/issues/5946): `Callback` throws a `TypeError` when checking a `callable` has variadic parameters
|
||||
|
||||
## [10.5.33] - 2024-09-09
|
||||
|
||||
### Fixed
|
||||
|
||||
* [#4584](https://github.com/sebastianbergmann/phpunit/issues/4584): `assertJsonStringEqualsJsonString()` considers objects with sequential numeric keys equal to be arrays
|
||||
* [#4625](https://github.com/sebastianbergmann/phpunit/issues/4625): Generator yielding keys that are neither integer or string leads to hard-to-understand error message when used as data provider
|
||||
* [#4674](https://github.com/sebastianbergmann/phpunit/issues/4674): JSON assertions should treat objects as unordered
|
||||
* [#5891](https://github.com/sebastianbergmann/phpunit/issues/5891): `Callback` constraint does not handle variadic arguments correctly when used for mock object expectations
|
||||
* [#5929](https://github.com/sebastianbergmann/phpunit/issues/5929): TestDox output containing `$` at the beginning gets truncated when used with a data provider
|
||||
|
||||
## [10.5.32] - 2024-09-04
|
||||
|
||||
### Added
|
||||
|
||||
* [#5937](https://github.com/sebastianbergmann/phpunit/issues/5937): `failOnPhpunitDeprecation` attribute on the `<phpunit>` element of the XML configuration file and `--fail-on-phpunit-deprecation` CLI option for controlling whether PHPUnit deprecations should be considered when determining the test runner's shell exit code (default: do not consider)
|
||||
* `displayDetailsOnPhpunitDeprecations` attribute on the `<phpunit>` element of the XML configuration file and `--display-phpunit-deprecations` CLI option for controlling whether details on PHPUnit deprecations should be displayed (default: do not display)
|
||||
|
||||
### Changed
|
||||
|
||||
* [#5937](https://github.com/sebastianbergmann/phpunit/issues/5937): PHPUnit deprecations will, by default, no longer affect the test runner's shell exit code. This can optionally be turned back on using the `--fail-on-phpunit-deprecation` CLI option or the `failOnPhpunitDeprecation="true"` attribute on the `<phpunit>` element of the XML configuration file.
|
||||
* Details for PHPUnit deprecations will, by default, no longer be displayed. This can optionally be turned back on using the `--display-phpunit-deprecations` CLI option or the `displayDetailsOnPhpunitDeprecations` attribute on the `<phpunit>` element of the XML configuration file.
|
||||
|
||||
## [10.5.31] - 2024-09-03
|
||||
|
||||
### Changed
|
||||
|
||||
* [#5931](https://github.com/sebastianbergmann/phpunit/pull/5931): `name` property on `<testsuites>` element in JUnit XML logfile
|
||||
* Removed `.phpstorm.meta.php` file as methods such as `TestCase::createStub()` use generics / template types for their return types and PhpStorm, for example, uses that information
|
||||
|
||||
### Fixed
|
||||
|
||||
* [#5884](https://github.com/sebastianbergmann/phpunit/issues/5884): TestDox printer does not consider that issues can be suppressed by attribute, baseline, source location, or `@` operator
|
||||
|
||||
## [10.5.30] - 2024-08-13
|
||||
|
||||
### Changed
|
||||
|
||||
* Improved error message when stubbed method is called more often than return values were configured for it
|
||||
|
||||
## [10.5.29] - 2024-07-30
|
||||
|
||||
### Fixed
|
||||
|
||||
* [#5887](https://github.com/sebastianbergmann/phpunit/pull/5887): Issue baseline generator does not correctly handle ignoring suppressed issues
|
||||
* [#5908](https://github.com/sebastianbergmann/phpunit/issues/5908): `--list-tests` and `--list-tests-xml` CLI options do not report error when data provider method throws exception
|
||||
|
||||
## [10.5.28] - 2024-07-18
|
||||
|
||||
### Fixed
|
||||
|
||||
* [#5898](https://github.com/sebastianbergmann/phpunit/issues/5898): `Test\Passed` event is not emitted for PHPT tests
|
||||
* `--coverage-filter` CLI option could not be used multiple times
|
||||
|
||||
## [10.5.27] - 2024-07-10
|
||||
|
||||
### Changed
|
||||
|
||||
* Updated dependencies (so that users that install using Composer's `--prefer-lowest` CLI option also get recent versions)
|
||||
|
||||
### Fixed
|
||||
|
||||
* [#5892](https://github.com/sebastianbergmann/phpunit/issues/5892): Errors during write of `phpunit.xml` are not handled correctly when `--generate-configuration` is used
|
||||
|
||||
## [10.5.26] - 2024-07-08
|
||||
|
||||
### Added
|
||||
|
||||
* `--only-summary-for-coverage-text` CLI option to reduce the code coverage report in text format to a summary
|
||||
* `--show-uncovered-for-coverage-text` CLI option to expand the code coverage report in text format to include a list of uncovered files
|
||||
|
||||
## [10.5.25] - 2024-07-03
|
||||
|
||||
### Changed
|
||||
|
||||
* Updated dependencies for PHAR distribution
|
||||
|
||||
## [10.5.24] - 2024-06-20
|
||||
|
||||
### Changed
|
||||
|
||||
* [#5877](https://github.com/sebastianbergmann/phpunit/pull/5877): Use `array_pop()` instead of `array_shift()` for processing `Test` objects in `TestSuite::run()` and optimize `TestSuite::isEmpty()`
|
||||
|
||||
## [10.5.23] - 2024-06-20
|
||||
|
||||
### Changed
|
||||
|
||||
* [#5875](https://github.com/sebastianbergmann/phpunit/pull/5875): Also destruct `TestCase` objects early that use a data provider
|
||||
|
||||
## [10.5.22] - 2024-06-19
|
||||
|
||||
### Changed
|
||||
|
||||
* [#5871](https://github.com/sebastianbergmann/phpunit/pull/5871): Do not collect unnecessary information using `debug_backtrace()`
|
||||
|
||||
## [10.5.21] - 2024-06-15
|
||||
|
||||
### Changed
|
||||
|
||||
* [#5861](https://github.com/sebastianbergmann/phpunit/pull/5861): Destroy `TestCase` object after its test was run
|
||||
|
||||
## [10.5.20] - 2024-04-24
|
||||
|
||||
* [#5771](https://github.com/sebastianbergmann/phpunit/issues/5771): JUnit XML logger may crash when test that is run in separate process exits unexpectedly
|
||||
* [#5819](https://github.com/sebastianbergmann/phpunit/issues/5819): Duplicate keys from different data providers are not handled properly
|
||||
|
||||
## [10.5.19] - 2024-04-17
|
||||
|
||||
### Fixed
|
||||
|
||||
* [#5818](https://github.com/sebastianbergmann/phpunit/issues/5818): Calling `method()` on a test stub created using `createStubForIntersectionOfInterfaces()` throws an unexpected exception
|
||||
|
||||
## [10.5.18] - 2024-04-14
|
||||
|
||||
### Deprecated
|
||||
|
||||
* [#5812](https://github.com/sebastianbergmann/phpunit/pull/5812): Support for string array keys in data sets returned by data provider methods that do not match the parameter names of the test method(s) that use(s) them
|
||||
|
||||
### Fixed
|
||||
|
||||
* [#5795](https://github.com/sebastianbergmann/phpunit/issues/5795): Using `@testWith` annotation may generate `PHP Warning: Uninitialized string offset 0`
|
||||
|
||||
## [10.5.17] - 2024-04-05
|
||||
|
||||
### Changed
|
||||
|
||||
* The namespaces of dependencies are now prefixed with `PHPUnitPHAR` instead of just `PHPUnit` for the PHAR distribution of PHPUnit
|
||||
|
||||
## [10.5.16] - 2024-03-28
|
||||
|
||||
### Changed
|
||||
|
||||
* [#5766](https://github.com/sebastianbergmann/phpunit/pull/5766): Do not use a shell in `proc_open()` if not really needed
|
||||
* [#5772](https://github.com/sebastianbergmann/phpunit/pull/5772): Cleanup process handling after dropping temp-file handling
|
||||
|
||||
### Fixed
|
||||
|
||||
* [#5570](https://github.com/sebastianbergmann/phpunit/pull/5570): Windows does not support exclusive locks on stdout
|
||||
|
||||
## [10.5.15] - 2024-03-22
|
||||
|
||||
### Fixed
|
||||
|
||||
* [#5765](https://github.com/sebastianbergmann/phpunit/pull/5765): Be more forgiving with error handlers that do not respect error suppression
|
||||
|
||||
## [10.5.14] - 2024-03-21
|
||||
|
||||
### Changed
|
||||
|
||||
* [#5747](https://github.com/sebastianbergmann/phpunit/pull/5747): Cache result of `Groups::groups()`
|
||||
* [#5748](https://github.com/sebastianbergmann/phpunit/pull/5748): Improve performance of `NamePrettifier::prettifyTestMethodName()`
|
||||
* [#5750](https://github.com/sebastianbergmann/phpunit/pull/5750): Micro-optimize `NamePrettifier::prettifyTestMethodName()` once again
|
||||
|
||||
### Fixed
|
||||
|
||||
* [#5760](https://github.com/sebastianbergmann/phpunit/issues/5760): TestDox printer does not display details about exceptions raised in before-test methods
|
||||
|
||||
## [10.5.13] - 2024-03-12
|
||||
|
||||
### Changed
|
||||
|
||||
* [#5727](https://github.com/sebastianbergmann/phpunit/pull/5727): Prevent duplicate call of `NamePrettifier::prettifyTestMethodName()`
|
||||
* [#5739](https://github.com/sebastianbergmann/phpunit/pull/5739): Micro-optimize `NamePrettifier::prettifyTestMethodName()`
|
||||
* [#5740](https://github.com/sebastianbergmann/phpunit/pull/5740): Micro-optimize `TestRunner::runTestWithTimeout()`
|
||||
* [#5741](https://github.com/sebastianbergmann/phpunit/pull/5741): Save call to `Telemetry\System::snapshot()`
|
||||
* [#5742](https://github.com/sebastianbergmann/phpunit/pull/5742): Prevent file IO when not strictly necessary
|
||||
* [#5743](https://github.com/sebastianbergmann/phpunit/pull/5743): Prevent unnecessary `ExecutionOrderDependency::getTarget()` call
|
||||
* [#5744](https://github.com/sebastianbergmann/phpunit/pull/5744): Simplify `NamePrettifier::prettifyTestMethodName()`
|
||||
|
||||
### Fixed
|
||||
|
||||
* [#5351](https://github.com/sebastianbergmann/phpunit/issues/5351): Incorrect code coverage metadata does not prevent code coverage data from being collected
|
||||
* [#5746](https://github.com/sebastianbergmann/phpunit/issues/5746): Using `-d` CLI option multiple times triggers warning
|
||||
|
||||
## [10.5.12] - 2024-03-09
|
||||
|
||||
### Fixed
|
||||
|
||||
* [#5652](https://github.com/sebastianbergmann/phpunit/issues/5652): `HRTime::duration()` throws `InvalidArgumentException`
|
||||
|
||||
## [10.5.11] - 2024-02-25
|
||||
|
||||
### Fixed
|
||||
|
||||
* [#5704](https://github.com/sebastianbergmann/phpunit/issues/5704#issuecomment-1951105254): No warning when CLI options are used multiple times
|
||||
* [#5707](https://github.com/sebastianbergmann/phpunit/issues/5707): `--fail-on-empty-test-suite` CLI option is not documented in `--help` output
|
||||
* No warning when the `#[CoversClass]` and `#[UsesClass]` attributes are used with the name of an interface
|
||||
* Resource usage information is printed when the `--debug` CLI option is used
|
||||
|
||||
## [10.5.10] - 2024-02-04
|
||||
|
||||
### Changed
|
||||
|
||||
* Improve output of `--check-version` CLI option
|
||||
* Improve description of `--check-version` CLI option
|
||||
|
||||
### Fixed
|
||||
|
||||
* [#5692](https://github.com/sebastianbergmann/phpunit/issues/5692): `--log-events-text` and `--log-events-verbose-text` require the destination file to exit
|
||||
|
||||
## [10.5.9] - 2024-01-22
|
||||
|
||||
### Changed
|
||||
|
||||
* Show help for `--manifest`, `--sbom`, and `--composer-lock` when the PHAR is used
|
||||
|
||||
### Fixed
|
||||
|
||||
* [#5676](https://github.com/sebastianbergmann/phpunit/issues/5676): PHPUnit's test runner overwrites custom error handler registered using `set_error_handler()` in bootstrap script
|
||||
|
||||
## [10.5.8] - 2024-01-19
|
||||
|
||||
### Fixed
|
||||
|
||||
* [#5673](https://github.com/sebastianbergmann/phpunit/issues/5673): Confusing error message when migration of a configuration is requested that does not need to be migrated
|
||||
|
||||
## [10.5.7] - 2024-01-14
|
||||
|
||||
### Fixed
|
||||
|
||||
* [#5662](https://github.com/sebastianbergmann/phpunit/issues/5662): PHPUnit errors out on startup when the `ctype` extension is not loaded but a polyfill for it was installed
|
||||
|
||||
## [10.5.6] - 2024-01-13
|
||||
|
||||
### Added
|
||||
|
||||
* Added the `--debug` CLI option as an alias for `--no-output --log-events-text php://stdout`
|
||||
|
||||
### Fixed
|
||||
|
||||
* [#5455](https://github.com/sebastianbergmann/phpunit/issues/5455): `willReturnCallback()` does not pass unknown named variadic arguments to callback
|
||||
* [#5488](https://github.com/sebastianbergmann/phpunit/issues/5488): Details about tests that are considered risky are not displayed when the TestDox result printer is used
|
||||
* [#5516](https://github.com/sebastianbergmann/phpunit/issues/5516): Assertions that use the `LogicalNot` constraint (`assertNotEquals()`, `assertStringNotContainsString()`, ...) can generate confusing failure messages
|
||||
* [#5518](https://github.com/sebastianbergmann/phpunit/issues/5518): Details about deprecations, notices, and warnings are not displayed when the TestDox result printer is used
|
||||
* [#5574](https://github.com/sebastianbergmann/phpunit/issues/5574): Wrong backtrace line is reported
|
||||
* [#5633](https://github.com/sebastianbergmann/phpunit/pull/5633): `--log-events-text` and `--log-events-verbose-text` CLI options do not handle absolute and relative paths
|
||||
* [#5634](https://github.com/sebastianbergmann/phpunit/pull/5634): Exceptions in the destructor of a test double are ignored
|
||||
* [#5641](https://github.com/sebastianbergmann/phpunit/issues/5641): The `TestSuite` value object returned by `TestSuite\Filtered::testSuite()` contains all tests instead of only the filtered tests
|
||||
|
||||
## [10.5.5] - 2023-12-27
|
||||
|
||||
### Fixed
|
||||
|
||||
* [#5619](https://github.com/sebastianbergmann/phpunit/pull/5619): Reverted change introduced in PHPUnit 10.5.4 that broke backward compatibility
|
||||
|
||||
## [10.5.4] - 2023-12-27
|
||||
|
||||
### Fixed
|
||||
|
||||
* [#5592](https://github.com/sebastianbergmann/phpunit/issues/5592): Error Handler prevents `error_get_last()` usage in tests
|
||||
* [#5592](https://github.com/sebastianbergmann/phpunit/issues/5592): `E_USER_ERROR` does not abort test execution
|
||||
* [#5612](https://github.com/sebastianbergmann/phpunit/issues/5612): Empty `<coverage>` element in XML configuration after migrating configuration
|
||||
* [#5616](https://github.com/sebastianbergmann/phpunit/issues/5616): Values from data provider are not shown for failed test
|
||||
* [#5619](https://github.com/sebastianbergmann/phpunit/pull/5619): Check and restore error/exception global handlers
|
||||
* [#5621](https://github.com/sebastianbergmann/phpunit/issues/5621): Name of data set is missing from TeamCity output
|
||||
|
||||
## [10.5.3] - 2023-12-13
|
||||
|
||||
### Changed
|
||||
|
||||
* Make PHAR build reproducible (the only remaining differences were in the timestamps for the files in the PHAR)
|
||||
|
||||
### Deprecated
|
||||
|
||||
* `Test\AssertionFailed` and `Test\AssertionSucceeded` events
|
||||
* `PHPUnit\Runner\Extension\Facade::requireExportOfObjects()` and `PHPUnit\Runner\Extension\Facade::requiresExportOfObjects()`
|
||||
* `registerMockObjectsFromTestArgumentsRecursively` attribute on the `<phpunit>` element of the XML configuration file
|
||||
* `PHPUnit\TextUI\Configuration\Configuration::registerMockObjectsFromTestArgumentsRecursively()`
|
||||
|
||||
### Fixed
|
||||
|
||||
* [#5614](https://github.com/sebastianbergmann/phpunit/issues/5614): Infinite recursion when data provider provides recursive array
|
||||
|
||||
## [10.5.2] - 2023-12-05
|
||||
|
||||
### Fixed
|
||||
|
||||
* [#5561](https://github.com/sebastianbergmann/phpunit/issues/5561): JUnit XML logger does not handle assertion failures in before-test methods
|
||||
* [#5567](https://github.com/sebastianbergmann/phpunit/issues/5567): Infinite recursion when recursive / self-referencing arrays are checked whether they contain only scalar values
|
||||
|
||||
## [10.5.1] - 2023-12-01
|
||||
|
||||
### Fixed
|
||||
|
||||
* [#5593](https://github.com/sebastianbergmann/phpunit/issues/5593): Return Value Generator fails to correctly create test stub for method with `static` return type declaration when used recursively
|
||||
* [#5596](https://github.com/sebastianbergmann/phpunit/issues/5596): `PHPUnit\Framework\TestCase` has `@internal` annotation in PHAR
|
||||
|
||||
## [10.5.0] - 2023-12-01
|
||||
|
||||
### Added
|
||||
|
||||
* [#5532](https://github.com/sebastianbergmann/phpunit/issues/5532): `#[IgnoreDeprecations]` attribute to ignore `E_(USER_)DEPRECATED` issues on test class and test method level
|
||||
* [#5551](https://github.com/sebastianbergmann/phpunit/issues/5551): Support for omitting parameter default values for `willReturnMap()`
|
||||
* [#5577](https://github.com/sebastianbergmann/phpunit/issues/5577): `--composer-lock` CLI option for PHAR binary that displays the `composer.lock` used to build the PHAR
|
||||
|
||||
### Changed
|
||||
|
||||
* `MockBuilder::disableAutoReturnValueGeneration()` and `MockBuilder::enableAutoReturnValueGeneration()` are no longer deprecated
|
||||
|
||||
### Fixed
|
||||
|
||||
* [#5563](https://github.com/sebastianbergmann/phpunit/issues/5563): `createMockForIntersectionOfInterfaces()` does not automatically register mock object for expectation verification
|
||||
|
||||
[10.5.40]: https://github.com/sebastianbergmann/phpunit/compare/10.5.39...10.5.40
|
||||
[10.5.39]: https://github.com/sebastianbergmann/phpunit/compare/10.5.38...10.5.39
|
||||
[10.5.38]: https://github.com/sebastianbergmann/phpunit/compare/10.5.37...10.5.38
|
||||
[10.5.37]: https://github.com/sebastianbergmann/phpunit/compare/10.5.36...10.5.37
|
||||
[10.5.36]: https://github.com/sebastianbergmann/phpunit/compare/10.5.35...10.5.36
|
||||
[10.5.35]: https://github.com/sebastianbergmann/phpunit/compare/10.5.34...10.5.35
|
||||
[10.5.34]: https://github.com/sebastianbergmann/phpunit/compare/10.5.33...10.5.34
|
||||
[10.5.33]: https://github.com/sebastianbergmann/phpunit/compare/10.5.32...10.5.33
|
||||
[10.5.32]: https://github.com/sebastianbergmann/phpunit/compare/10.5.31...10.5.32
|
||||
[10.5.31]: https://github.com/sebastianbergmann/phpunit/compare/10.5.30...10.5.31
|
||||
[10.5.30]: https://github.com/sebastianbergmann/phpunit/compare/10.5.29...10.5.30
|
||||
[10.5.29]: https://github.com/sebastianbergmann/phpunit/compare/10.5.28...10.5.29
|
||||
[10.5.28]: https://github.com/sebastianbergmann/phpunit/compare/10.5.27...10.5.28
|
||||
[10.5.27]: https://github.com/sebastianbergmann/phpunit/compare/10.5.26...10.5.27
|
||||
[10.5.26]: https://github.com/sebastianbergmann/phpunit/compare/10.5.25...10.5.26
|
||||
[10.5.25]: https://github.com/sebastianbergmann/phpunit/compare/10.5.24...10.5.25
|
||||
[10.5.24]: https://github.com/sebastianbergmann/phpunit/compare/10.5.23...10.5.24
|
||||
[10.5.23]: https://github.com/sebastianbergmann/phpunit/compare/10.5.22...10.5.23
|
||||
[10.5.22]: https://github.com/sebastianbergmann/phpunit/compare/10.5.21...10.5.22
|
||||
[10.5.21]: https://github.com/sebastianbergmann/phpunit/compare/10.5.20...10.5.21
|
||||
[10.5.20]: https://github.com/sebastianbergmann/phpunit/compare/10.5.19...10.5.20
|
||||
[10.5.19]: https://github.com/sebastianbergmann/phpunit/compare/10.5.18...10.5.19
|
||||
[10.5.18]: https://github.com/sebastianbergmann/phpunit/compare/10.5.17...10.5.18
|
||||
[10.5.17]: https://github.com/sebastianbergmann/phpunit/compare/10.5.16...10.5.17
|
||||
[10.5.16]: https://github.com/sebastianbergmann/phpunit/compare/10.5.15...10.5.16
|
||||
[10.5.15]: https://github.com/sebastianbergmann/phpunit/compare/10.5.14...10.5.15
|
||||
[10.5.14]: https://github.com/sebastianbergmann/phpunit/compare/10.5.13...10.5.14
|
||||
[10.5.13]: https://github.com/sebastianbergmann/phpunit/compare/10.5.12...10.5.13
|
||||
[10.5.12]: https://github.com/sebastianbergmann/phpunit/compare/10.5.11...10.5.12
|
||||
[10.5.11]: https://github.com/sebastianbergmann/phpunit/compare/10.5.10...10.5.11
|
||||
[10.5.10]: https://github.com/sebastianbergmann/phpunit/compare/10.5.9...10.5.10
|
||||
[10.5.9]: https://github.com/sebastianbergmann/phpunit/compare/10.5.8...10.5.9
|
||||
[10.5.8]: https://github.com/sebastianbergmann/phpunit/compare/10.5.7...10.5.8
|
||||
[10.5.7]: https://github.com/sebastianbergmann/phpunit/compare/10.5.6...10.5.7
|
||||
[10.5.6]: https://github.com/sebastianbergmann/phpunit/compare/10.5.5...10.5.6
|
||||
[10.5.5]: https://github.com/sebastianbergmann/phpunit/compare/10.5.4...10.5.5
|
||||
[10.5.4]: https://github.com/sebastianbergmann/phpunit/compare/10.5.3...10.5.4
|
||||
[10.5.3]: https://github.com/sebastianbergmann/phpunit/compare/10.5.2...10.5.3
|
||||
[10.5.2]: https://github.com/sebastianbergmann/phpunit/compare/10.5.1...10.5.2
|
||||
[10.5.1]: https://github.com/sebastianbergmann/phpunit/compare/10.5.0...10.5.1
|
||||
[10.5.0]: https://github.com/sebastianbergmann/phpunit/compare/10.4.2...10.5.0
|
||||
@@ -0,0 +1,92 @@
|
||||
# Deprecations
|
||||
|
||||
## 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 |
|
||||
|-------------------------------------------------------------------|----------------------------------------------|--------|-------------|
|
||||
| [#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 | |
|
||||
|
||||
#### Test Double API
|
||||
|
||||
| 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 | |
|
||||
| [#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())` |
|
||||
|
||||
#### Miscellaneous
|
||||
|
||||
| Issue | Description | Since | Replacement |
|
||||
|-------------------------------------------------------------------|----------------------------------------------------------------|--------|--------------------------------------------------------------------|
|
||||
| [#5236](https://github.com/sebastianbergmann/phpunit/issues/5236) | `PHPUnit\Framework\Attributes\CodeCoverageIgnore()` | 10.1.0 | |
|
||||
| [#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 | |
|
||||
| [#5236](https://github.com/sebastianbergmann/phpunit/issues/5513) | `PHPUnit\Framework\Attributes\IgnoreClassForCodeCoverage()` | 10.4.0 | Use `@codeCoverageIgnore` annotation in the class' doc-comment |
|
||||
| [#5236](https://github.com/sebastianbergmann/phpunit/issues/5513) | `PHPUnit\Framework\Attributes\IgnoreMethodForCodeCoverage()` | 10.4.0 | Use `@codeCoverageIgnore` annotation in the method's doc-comment |
|
||||
| [#5236](https://github.com/sebastianbergmann/phpunit/issues/5513) | `PHPUnit\Framework\Attributes\IgnoreFunctionForCodeCoverage()` | 10.4.0 | Use `@codeCoverageIgnore` annotation in the function's doc-comment |
|
||||
|
||||
### Running Tests
|
||||
|
||||
| Issue | Description | Since | Replacement |
|
||||
|-------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------|--------|-------------|
|
||||
| [#5481](https://github.com/sebastianbergmann/phpunit/issues/5481) | `dataSet` attribute for `testCaseMethod` elements in the XML document generated by `--list-tests-xml` | 10.4.0 | |
|
||||
|
||||
### Extending PHPUnit
|
||||
|
||||
| Issue | Description | Since | Replacement |
|
||||
|-------|------------------------------------------------------------------------------------------------------------------------------|--------|--------------------------------------------------------------------------------|
|
||||
| | `PHPUnit\TextUI\Configuration\Configuration::coverageExcludeDirectories()` | 10.2.0 | `PHPUnit\TextUI\Configuration\Configuration::source()->excludeDirectories()` |
|
||||
| | `PHPUnit\TextUI\Configuration\Configuration::coverageExcludeFiles()` | 10.2.0 | `PHPUnit\TextUI\Configuration\Configuration::source()->excludeFiles()` |
|
||||
| | `PHPUnit\TextUI\Configuration\Configuration::coverageIncludeDirectories()` | 10.2.0 | `PHPUnit\TextUI\Configuration\Configuration::source()->includeDirectories()` |
|
||||
| | `PHPUnit\TextUI\Configuration\Configuration::coverageIncludeFiles()` | 10.2.0 | `PHPUnit\TextUI\Configuration\Configuration::source()->includeFiles()` |
|
||||
| | `PHPUnit\TextUI\Configuration\Configuration::loadPharExtensions()` | 10.2.0 | `PHPUnit\TextUI\Configuration\Configuration::noExtensions()` |
|
||||
| | `PHPUnit\TextUI\Configuration\Configuration::hasNonEmptyListOfFilesToBeIncludedInCodeCoverageReport()` | 10.2.0 | `PHPUnit\TextUI\Configuration\Configuration::source()->notEmpty()` |
|
||||
| | `PHPUnit\TextUI\Configuration\Configuration::restrictDeprecations()` | 10.2.0 | `PHPUnit\TextUI\Configuration\Configuration::source()->restrictDeprecations()` |
|
||||
| | `PHPUnit\TextUI\Configuration\Configuration::restrictNotices()` | 10.2.0 | `PHPUnit\TextUI\Configuration\Configuration::source()->restrictNotices()` |
|
||||
| | `PHPUnit\TextUI\Configuration\Configuration::restrictWarnings()` | 10.2.0 | `PHPUnit\TextUI\Configuration\Configuration::source()->restrictWarnings()` |
|
||||
| | `PHPUnit\TextUI\Configuration\Configuration::cliArgument()` | 10.4.0 | `PHPUnit\TextUI\Configuration\Configuration::cliArguments()[0]` |
|
||||
| | `PHPUnit\TextUI\Configuration\Configuration::hasCliArgument()` | 10.4.0 | `PHPUnit\TextUI\Configuration\Configuration::hasCliArguments()` |
|
||||
| | `PHPUnit\Framework\Constraint\Constraint::exporter()` | 10.4.0 | |
|
||||
| | `PHPUnit\TextUI\Configuration\Configuration::registerMockObjectsFromTestArgumentsRecursively()` | 10.5.3 | |
|
||||
| | `Test\AssertionFailed` and `Test\AssertionSucceeded` events | 10.5.3 | |
|
||||
| | `PHPUnit\Runner\Extension\Facade::requireExportOfObjects()` and `PHPUnit\Runner\Extension\Facade::requiresExportOfObjects()` | 10.5.3 | |
|
||||
|
||||
## Hard Deprecations
|
||||
|
||||
This functionality is currently [hard-deprecated](https://phpunit.de/backward-compatibility.html#hard-deprecation):
|
||||
|
||||
### Writing Tests
|
||||
|
||||
#### Miscellaneous
|
||||
|
||||
| Issue | Description | Since | Replacement |
|
||||
|-------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------|---------|-------------|
|
||||
| [#5100](https://github.com/sebastianbergmann/phpunit/issues/5100) | Support for non-static data provider methods, non-public data provider methods, and data provider methods that declare parameters | 10.0.0 | |
|
||||
| [#5812](https://github.com/sebastianbergmann/phpunit/pull/5812) | Support for string array keys in data sets returned by data provider methods that do not match the parameter names of the test method(s) that use(s) them | 10.5.18 | |
|
||||
@@ -0,0 +1,34 @@
|
||||
# PHPUnit
|
||||
|
||||
[](https://packagist.org/packages/phpunit/phpunit)
|
||||
[](https://github.com/sebastianbergmann/phpunit/actions)
|
||||
[](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.
|
||||
|
||||
## 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:
|
||||
|
||||
```bash
|
||||
$ wget https://phar.phpunit.de/phpunit-X.Y.phar
|
||||
|
||||
$ 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.
|
||||
|
||||
## 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
|
||||
|
||||
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:
|
||||
|
||||
* [PHPUnit](https://github.com/sebastianbergmann/phpunit/graphs/contributors)
|
||||
* [php-code-coverage](https://github.com/sebastianbergmann/php-code-coverage/graphs/contributors)
|
||||
|
||||
A very special thanks to everyone who has contributed to the [documentation](https://github.com/sebastianbergmann/phpunit-documentation-english/graphs/contributors).
|
||||
@@ -0,0 +1,94 @@
|
||||
{
|
||||
"name": "phpunit/phpunit",
|
||||
"description": "The PHP Unit Testing framework.",
|
||||
"type": "library",
|
||||
"keywords": [
|
||||
"phpunit",
|
||||
"xunit",
|
||||
"testing"
|
||||
],
|
||||
"homepage": "https://phpunit.de/",
|
||||
"license": "BSD-3-Clause",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Sebastian Bergmann",
|
||||
"email": "sebastian@phpunit.de",
|
||||
"role": "lead"
|
||||
}
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
|
||||
"security": "https://github.com/sebastianbergmann/phpunit/security/policy"
|
||||
},
|
||||
"prefer-stable": true,
|
||||
"require": {
|
||||
"php": ">=8.1",
|
||||
"ext-dom": "*",
|
||||
"ext-json": "*",
|
||||
"ext-libxml": "*",
|
||||
"ext-mbstring": "*",
|
||||
"ext-xml": "*",
|
||||
"ext-xmlwriter": "*",
|
||||
"myclabs/deep-copy": "^1.12.1",
|
||||
"phar-io/manifest": "^2.0.4",
|
||||
"phar-io/version": "^3.2.1",
|
||||
"phpunit/php-code-coverage": "^10.1.16",
|
||||
"phpunit/php-file-iterator": "^4.1.0",
|
||||
"phpunit/php-invoker": "^4.0.0",
|
||||
"phpunit/php-text-template": "^3.0.1",
|
||||
"phpunit/php-timer": "^6.0.0",
|
||||
"sebastian/cli-parser": "^2.0.1",
|
||||
"sebastian/code-unit": "^2.0.0",
|
||||
"sebastian/comparator": "^5.0.3",
|
||||
"sebastian/diff": "^5.1.1",
|
||||
"sebastian/environment": "^6.1.0",
|
||||
"sebastian/exporter": "^5.1.2",
|
||||
"sebastian/global-state": "^6.0.2",
|
||||
"sebastian/object-enumerator": "^5.0.0",
|
||||
"sebastian/recursion-context": "^5.0.0",
|
||||
"sebastian/type": "^4.0.0",
|
||||
"sebastian/version": "^4.0.1"
|
||||
},
|
||||
"config": {
|
||||
"platform": {
|
||||
"php": "8.1.0"
|
||||
},
|
||||
"optimize-autoloader": true,
|
||||
"sort-packages": true
|
||||
},
|
||||
"suggest": {
|
||||
"ext-soap": "To be able to generate mocks based on WSDL files"
|
||||
},
|
||||
"bin": [
|
||||
"phpunit"
|
||||
],
|
||||
"autoload": {
|
||||
"classmap": [
|
||||
"src/"
|
||||
],
|
||||
"files": [
|
||||
"src/Framework/Assert/Functions.php"
|
||||
]
|
||||
},
|
||||
"autoload-dev": {
|
||||
"classmap": [
|
||||
"tests/_files"
|
||||
],
|
||||
"files": [
|
||||
"tests/unit/Event/AbstractEventTestCase.php",
|
||||
"tests/unit/Framework/MockObject/TestDoubleTestCase.php",
|
||||
"tests/unit/Metadata/Parser/AnnotationParserTestCase.php",
|
||||
"tests/unit/Metadata/Parser/AttributeParserTestCase.php",
|
||||
"tests/_files/CoverageNamespacedFunctionTest.php",
|
||||
"tests/_files/CoveredFunction.php",
|
||||
"tests/_files/Generator.php",
|
||||
"tests/_files/NamespaceCoveredFunction.php",
|
||||
"tests/end-to-end/code-coverage/ignore-function-using-attribute/src/CoveredFunction.php"
|
||||
]
|
||||
},
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "10.5-dev"
|
||||
}
|
||||
}
|
||||
}
|
||||
+1553
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,323 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<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 10.5 may be structured.
|
||||
</xs:documentation>
|
||||
<xs:appinfo source="https://phpunit.de/documentation.html"/>
|
||||
</xs:annotation>
|
||||
<xs:element name="phpunit" type="phpUnitType">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Root Element</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:complexType name="sourceType">
|
||||
<xs:all>
|
||||
<xs:element name="include" minOccurs="0" maxOccurs="1">
|
||||
<xs:complexType>
|
||||
<xs:group ref="sourcePathGroup"/>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
<xs:element name="exclude" minOccurs="0" maxOccurs="1">
|
||||
<xs:complexType>
|
||||
<xs:group ref="sourcePathGroup"/>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
</xs:all>
|
||||
<xs:attribute name="baseline" type="xs:anyURI"/>
|
||||
<xs:attribute name="restrictDeprecations" type="xs:boolean" default="false"/>
|
||||
<xs:attribute name="restrictNotices" type="xs:boolean" default="false"/>
|
||||
<xs:attribute name="restrictWarnings" type="xs:boolean" default="false"/>
|
||||
<xs:attribute name="ignoreSuppressionOfDeprecations" type="xs:boolean" default="false"/>
|
||||
<xs:attribute name="ignoreSuppressionOfPhpDeprecations" type="xs:boolean" default="false"/>
|
||||
<xs:attribute name="ignoreSuppressionOfErrors" type="xs:boolean" default="false"/>
|
||||
<xs:attribute name="ignoreSuppressionOfNotices" type="xs:boolean" default="false"/>
|
||||
<xs:attribute name="ignoreSuppressionOfPhpNotices" type="xs:boolean" default="false"/>
|
||||
<xs:attribute name="ignoreSuppressionOfWarnings" type="xs:boolean" default="false"/>
|
||||
<xs:attribute name="ignoreSuppressionOfPhpWarnings" type="xs:boolean" default="false"/>
|
||||
</xs:complexType>
|
||||
<xs:group name="sourcePathGroup">
|
||||
<xs:sequence>
|
||||
<xs:choice minOccurs="0" maxOccurs="unbounded">
|
||||
<xs:element name="directory" type="sourceDirectoryType"/>
|
||||
<xs:element name="file" type="xs:anyURI"/>
|
||||
</xs:choice>
|
||||
</xs:sequence>
|
||||
</xs:group>
|
||||
<xs:complexType name="sourceDirectoryType">
|
||||
<xs:simpleContent>
|
||||
<xs:extension base="xs:string">
|
||||
<xs:attribute type="xs:string" name="prefix" default=""/>
|
||||
<xs:attribute type="xs:string" name="suffix" default=".php"/>
|
||||
</xs:extension>
|
||||
</xs:simpleContent>
|
||||
</xs:complexType>
|
||||
<xs:complexType name="coverageType">
|
||||
<xs:all>
|
||||
<xs:element name="report" minOccurs="0" maxOccurs="1">
|
||||
<xs:complexType>
|
||||
<xs:group ref="coverageReportGroup"/>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
</xs:all>
|
||||
<xs:attribute name="cacheDirectory" type="xs:anyURI"/>
|
||||
<xs:attribute name="pathCoverage" type="xs:boolean" default="false"/>
|
||||
<xs:attribute name="includeUncoveredFiles" type="xs:boolean" default="true"/>
|
||||
<xs:attribute name="ignoreDeprecatedCodeUnits" type="xs:boolean" default="false"/>
|
||||
<xs:attribute name="disableCodeCoverageIgnore" type="xs:boolean" default="false"/>
|
||||
</xs:complexType>
|
||||
<xs:complexType name="loggingType">
|
||||
<xs:group ref="loggingGroup"/>
|
||||
</xs:complexType>
|
||||
<xs:complexType name="groupsType">
|
||||
<xs:choice>
|
||||
<xs:sequence>
|
||||
<xs:element name="include" type="groupType"/>
|
||||
<xs:element name="exclude" type="groupType" minOccurs="0"/>
|
||||
</xs:sequence>
|
||||
<xs:sequence>
|
||||
<xs:element name="exclude" type="groupType"/>
|
||||
</xs:sequence>
|
||||
</xs:choice>
|
||||
</xs:complexType>
|
||||
<xs:complexType name="groupType">
|
||||
<xs:sequence>
|
||||
<xs:element name="group" type="xs:string" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
<xs:complexType name="extensionsType">
|
||||
<xs:sequence>
|
||||
<xs:element name="bootstrap" type="bootstrapType" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
<xs:complexType name="bootstrapType">
|
||||
<xs:sequence>
|
||||
<xs:element name="parameter" type="parameterType" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
<xs:attribute name="class" type="xs:string" use="required"/>
|
||||
</xs:complexType>
|
||||
<xs:complexType name="parameterType">
|
||||
<xs:attribute name="name" type="xs:string" use="required"/>
|
||||
<xs:attribute name="value" type="xs:string" use="required"/>
|
||||
</xs:complexType>
|
||||
<xs:simpleType name="columnsType">
|
||||
<xs:union>
|
||||
<xs:simpleType>
|
||||
<xs:restriction base="xs:integer"/>
|
||||
</xs:simpleType>
|
||||
<xs:simpleType>
|
||||
<xs:restriction base="xs:string">
|
||||
<xs:enumeration value="max"/>
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
</xs:union>
|
||||
</xs:simpleType>
|
||||
<xs:simpleType name="executionOrderType">
|
||||
<xs:restriction base="xs:string">
|
||||
<xs:enumeration value="default"/>
|
||||
<xs:enumeration value="defects"/>
|
||||
<xs:enumeration value="depends"/>
|
||||
<xs:enumeration value="depends,defects"/>
|
||||
<xs:enumeration value="depends,duration"/>
|
||||
<xs:enumeration value="depends,random"/>
|
||||
<xs:enumeration value="depends,reverse"/>
|
||||
<xs:enumeration value="depends,size"/>
|
||||
<xs:enumeration value="duration"/>
|
||||
<xs:enumeration value="no-depends"/>
|
||||
<xs:enumeration value="no-depends,defects"/>
|
||||
<xs:enumeration value="no-depends,duration"/>
|
||||
<xs:enumeration value="no-depends,random"/>
|
||||
<xs:enumeration value="no-depends,reverse"/>
|
||||
<xs:enumeration value="no-depends,size"/>
|
||||
<xs:enumeration value="random"/>
|
||||
<xs:enumeration value="reverse"/>
|
||||
<xs:enumeration value="size"/>
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
<xs:complexType name="phpType">
|
||||
<xs:sequence>
|
||||
<xs:choice maxOccurs="unbounded">
|
||||
<xs:element name="includePath" type="xs:anyURI" minOccurs="0" maxOccurs="unbounded"/>
|
||||
<xs:element name="ini" type="namedValueType" minOccurs="0" maxOccurs="unbounded"/>
|
||||
<xs:element name="const" type="namedValueType" minOccurs="0" maxOccurs="unbounded"/>
|
||||
<xs:element name="var" type="namedValueType" minOccurs="0" maxOccurs="unbounded"/>
|
||||
<xs:element name="env" type="namedValueType" minOccurs="0" maxOccurs="unbounded"/>
|
||||
<xs:element name="post" type="namedValueType" minOccurs="0" maxOccurs="unbounded"/>
|
||||
<xs:element name="get" type="namedValueType" minOccurs="0" maxOccurs="unbounded"/>
|
||||
<xs:element name="cookie" type="namedValueType" minOccurs="0" maxOccurs="unbounded"/>
|
||||
<xs:element name="server" type="namedValueType" minOccurs="0" maxOccurs="unbounded"/>
|
||||
<xs:element name="files" type="namedValueType" minOccurs="0" maxOccurs="unbounded"/>
|
||||
<xs:element name="request" type="namedValueType" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xs:choice>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
<xs:complexType name="namedValueType">
|
||||
<xs:attribute name="name" use="required" type="xs:string"/>
|
||||
<xs:attribute name="value" use="required" type="xs:anySimpleType"/>
|
||||
<xs:attribute name="verbatim" use="optional" type="xs:boolean"/>
|
||||
<xs:attribute name="force" use="optional" type="xs:boolean"/>
|
||||
</xs:complexType>
|
||||
<xs:complexType name="phpUnitType">
|
||||
<xs:annotation>
|
||||
<xs:documentation>The main type specifying the document structure</xs:documentation>
|
||||
</xs:annotation>
|
||||
<xs:group ref="configGroup"/>
|
||||
<xs:attributeGroup ref="configAttributeGroup"/>
|
||||
</xs:complexType>
|
||||
<xs:attributeGroup name="configAttributeGroup">
|
||||
<xs:attribute name="backupGlobals" type="xs:boolean" default="false"/>
|
||||
<xs:attribute name="backupStaticProperties" type="xs:boolean" default="false"/>
|
||||
<xs:attribute name="bootstrap" type="xs:anyURI"/>
|
||||
<xs:attribute name="cacheDirectory" type="xs:anyURI"/>
|
||||
<xs:attribute name="cacheResult" type="xs:boolean" default="true"/>
|
||||
<xs:attribute name="cacheResultFile" type="xs:anyURI"/>
|
||||
<xs:attribute name="colors" type="xs:boolean" default="false"/>
|
||||
<xs:attribute name="columns" type="columnsType" default="80"/>
|
||||
<xs:attribute name="controlGarbageCollector" type="xs:boolean" default="false"/>
|
||||
<xs:attribute name="numberOfTestsBeforeGarbageCollection" type="xs:integer" default="100"/>
|
||||
<xs:attribute name="requireCoverageMetadata" type="xs:boolean" default="false"/>
|
||||
<xs:attribute name="processIsolation" 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="failOnEmptyTestSuite" type="xs:boolean" default="false"/>
|
||||
<xs:attribute name="failOnIncomplete" type="xs:boolean" default="false"/>
|
||||
<xs:attribute name="failOnNotice" type="xs:boolean" default="false"/>
|
||||
<xs:attribute name="failOnRisky" type="xs:boolean" default="false"/>
|
||||
<xs:attribute name="failOnSkipped" type="xs:boolean" default="false"/>
|
||||
<xs:attribute name="failOnWarning" type="xs:boolean" default="false"/>
|
||||
<xs:attribute name="stopOnDefect" type="xs:boolean" default="false"/>
|
||||
<xs:attribute name="stopOnDeprecation" type="xs:boolean" default="false"/>
|
||||
<xs:attribute name="stopOnError" type="xs:boolean" default="false"/>
|
||||
<xs:attribute name="stopOnFailure" type="xs:boolean" default="false"/>
|
||||
<xs:attribute name="stopOnIncomplete" type="xs:boolean" default="false"/>
|
||||
<xs:attribute name="stopOnNotice" type="xs:boolean" default="false"/>
|
||||
<xs:attribute name="stopOnRisky" type="xs:boolean" default="false"/>
|
||||
<xs:attribute name="stopOnSkipped" type="xs:boolean" default="false"/>
|
||||
<xs:attribute name="stopOnWarning" type="xs:boolean" default="false"/>
|
||||
<xs:attribute name="beStrictAboutChangesToGlobalState" type="xs:boolean" default="false"/>
|
||||
<xs:attribute name="beStrictAboutOutputDuringTests" type="xs:boolean" default="false"/>
|
||||
<xs:attribute name="beStrictAboutTestsThatDoNotTestAnything" type="xs:boolean" default="true"/>
|
||||
<xs:attribute name="beStrictAboutCoverageMetadata" type="xs:boolean" default="false"/>
|
||||
<xs:attribute name="defaultTimeLimit" type="xs:integer" default="0"/>
|
||||
<xs:attribute name="enforceTimeLimit" type="xs:boolean" default="false"/>
|
||||
<xs:attribute name="timeoutForSmallTests" type="xs:integer" default="1"/>
|
||||
<xs:attribute name="timeoutForMediumTests" type="xs:integer" default="10"/>
|
||||
<xs:attribute name="timeoutForLargeTests" type="xs:integer" default="60"/>
|
||||
<xs:attribute name="defaultTestSuite" type="xs:string" default=""/>
|
||||
<xs:attribute name="testdox" type="xs:boolean" default="false"/>
|
||||
<xs:attribute name="stderr" type="xs:boolean" default="false"/>
|
||||
<xs:attribute name="reverseDefectList" type="xs:boolean" default="false"/>
|
||||
<xs:attribute name="registerMockObjectsFromTestArgumentsRecursively" type="xs:boolean" default="false"/>
|
||||
<xs:attribute name="extensionsDirectory" type="xs:anyURI"/>
|
||||
<xs:attribute name="executionOrder" type="executionOrderType" default="default"/>
|
||||
<xs:attribute name="resolveDependencies" type="xs:boolean" default="true"/>
|
||||
<xs:attribute name="displayDetailsOnIncompleteTests" type="xs:boolean" default="false"/>
|
||||
<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="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:attributeGroup>
|
||||
<xs:group name="configGroup">
|
||||
<xs:all>
|
||||
<xs:element ref="testSuiteFacet" minOccurs="0"/>
|
||||
<xs:element name="groups" type="groupsType" minOccurs="0"/>
|
||||
<xs:element name="source" type="sourceType" minOccurs="0"/>
|
||||
<xs:element name="coverage" type="coverageType" minOccurs="0"/>
|
||||
<xs:element name="logging" type="loggingType" minOccurs="0"/>
|
||||
<xs:element name="extensions" type="extensionsType" minOccurs="0"/>
|
||||
<xs:element name="php" type="phpType" minOccurs="0"/>
|
||||
</xs:all>
|
||||
</xs:group>
|
||||
<xs:element name="testSuiteFacet" abstract="true"/>
|
||||
<xs:element name="testsuite" type="testSuiteType" substitutionGroup="testSuiteFacet"/>
|
||||
<xs:element name="testsuites" type="testSuitesType" substitutionGroup="testSuiteFacet"/>
|
||||
<xs:complexType name="testSuitesType">
|
||||
<xs:sequence>
|
||||
<xs:element name="testsuite" type="testSuiteType" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
<xs:complexType name="testSuiteType">
|
||||
<xs:sequence>
|
||||
<xs:choice minOccurs="0" maxOccurs="unbounded">
|
||||
<xs:group ref="testSuitePathGroup"/>
|
||||
<xs:element name="exclude" type="xs:string"/>
|
||||
</xs:choice>
|
||||
</xs:sequence>
|
||||
<xs:attribute name="name" type="xs:string" use="required"/>
|
||||
</xs:complexType>
|
||||
<xs:group name="testSuitePathGroup">
|
||||
<xs:sequence>
|
||||
<xs:choice minOccurs="0" maxOccurs="unbounded">
|
||||
<xs:element name="directory" type="testSuiteDirectoryType"/>
|
||||
<xs:element name="file" type="testSuiteFileType"/>
|
||||
</xs:choice>
|
||||
</xs:sequence>
|
||||
</xs:group>
|
||||
<xs:complexType name="testSuiteDirectoryType">
|
||||
<xs:simpleContent>
|
||||
<xs:extension base="xs:string">
|
||||
<xs:attribute type="xs:string" name="prefix" default=""/>
|
||||
<xs:attribute type="xs:string" name="suffix" default="Test.php"/>
|
||||
<xs:attributeGroup ref="phpVersionGroup"/>
|
||||
</xs:extension>
|
||||
</xs:simpleContent>
|
||||
</xs:complexType>
|
||||
<xs:complexType name="testSuiteFileType">
|
||||
<xs:simpleContent>
|
||||
<xs:extension base="xs:anyURI">
|
||||
<xs:attributeGroup ref="phpVersionGroup"/>
|
||||
</xs:extension>
|
||||
</xs:simpleContent>
|
||||
</xs:complexType>
|
||||
<xs:attributeGroup name="phpVersionGroup">
|
||||
<xs:attribute name="phpVersion" type="xs:string" default="5.3.0"/>
|
||||
<xs:attribute name="phpVersionOperator" type="xs:string" default=">="/>
|
||||
</xs:attributeGroup>
|
||||
<xs:group name="coverageReportGroup">
|
||||
<xs:all>
|
||||
<xs:element name="clover" type="logToFileType" minOccurs="0"/>
|
||||
<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="php" type="logToFileType" minOccurs="0" />
|
||||
<xs:element name="text" type="coverageReportTextType" minOccurs="0" />
|
||||
<xs:element name="xml" type="logToDirectoryType" minOccurs="0" />
|
||||
</xs:all>
|
||||
</xs:group>
|
||||
<xs:group name="loggingGroup">
|
||||
<xs:all>
|
||||
<xs:element name="junit" type="logToFileType" 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" />
|
||||
</xs:all>
|
||||
</xs:group>
|
||||
<xs:complexType name="logToFileType">
|
||||
<xs:attribute name="outputFile" type="xs:anyURI" use="required"/>
|
||||
</xs:complexType>
|
||||
<xs:complexType name="logToDirectoryType">
|
||||
<xs:attribute name="outputDirectory" type="xs:anyURI" use="required"/>
|
||||
</xs:complexType>
|
||||
<xs:complexType name="coverageReportCrap4JType">
|
||||
<xs:attribute name="outputFile" type="xs:anyURI" use="required"/>
|
||||
<xs:attribute name="threshold" type="xs:integer"/>
|
||||
</xs:complexType>
|
||||
<xs:complexType name="coverageReportHtmlType">
|
||||
<xs:attribute name="outputDirectory" type="xs:anyURI" use="required"/>
|
||||
<xs:attribute name="lowUpperBound" type="xs:integer" default="50"/>
|
||||
<xs:attribute name="highLowerBound" type="xs:integer" default="90"/>
|
||||
<xs:attribute name="colorSuccessLow" type="xs:string" default="#dff0d8"/>
|
||||
<xs:attribute name="colorSuccessMedium" type="xs:string" default="#c3e3b5"/>
|
||||
<xs:attribute name="colorSuccessHigh" type="xs:string" default="#99cb84"/>
|
||||
<xs:attribute name="colorWarning" type="xs:string" default="#fcf8e3"/>
|
||||
<xs:attribute name="colorDanger" type="xs:string" default="#f2dede"/>
|
||||
<xs:attribute name="customCssFile" type="xs:string"/>
|
||||
</xs:complexType>
|
||||
<xs:complexType name="coverageReportTextType">
|
||||
<xs:attribute name="outputFile" type="xs:anyURI" use="required"/>
|
||||
<xs:attribute name="showUncoveredFiles" type="xs:boolean" default="false"/>
|
||||
<xs:attribute name="showOnlySummary" type="xs:boolean" default="false"/>
|
||||
</xs:complexType>
|
||||
</xs:schema>
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of PHPUnit.
|
||||
*
|
||||
* (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 PHPUnit\Event;
|
||||
|
||||
/**
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
|
||||
*
|
||||
* @internal This class is not covered by the backward compatibility promise for PHPUnit
|
||||
*/
|
||||
final class CollectingDispatcher implements Dispatcher
|
||||
{
|
||||
private EventCollection $events;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->events = new EventCollection;
|
||||
}
|
||||
|
||||
public function dispatch(Event $event): void
|
||||
{
|
||||
$this->events->add($event);
|
||||
}
|
||||
|
||||
public function flush(): EventCollection
|
||||
{
|
||||
$events = $this->events;
|
||||
|
||||
$this->events = new EventCollection;
|
||||
|
||||
return $events;
|
||||
}
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of PHPUnit.
|
||||
*
|
||||
* (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 PHPUnit\Event;
|
||||
|
||||
/**
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
|
||||
*
|
||||
* @internal This class is not covered by the backward compatibility promise for PHPUnit
|
||||
*/
|
||||
final class DeferringDispatcher implements SubscribableDispatcher
|
||||
{
|
||||
private readonly SubscribableDispatcher $dispatcher;
|
||||
private EventCollection $events;
|
||||
private bool $recording = true;
|
||||
|
||||
public function __construct(SubscribableDispatcher $dispatcher)
|
||||
{
|
||||
$this->dispatcher = $dispatcher;
|
||||
$this->events = new EventCollection;
|
||||
}
|
||||
|
||||
public function registerTracer(Tracer\Tracer $tracer): void
|
||||
{
|
||||
$this->dispatcher->registerTracer($tracer);
|
||||
}
|
||||
|
||||
public function registerSubscriber(Subscriber $subscriber): void
|
||||
{
|
||||
$this->dispatcher->registerSubscriber($subscriber);
|
||||
}
|
||||
|
||||
public function dispatch(Event $event): void
|
||||
{
|
||||
if ($this->recording) {
|
||||
$this->events->add($event);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->dispatcher->dispatch($event);
|
||||
}
|
||||
|
||||
public function flush(): void
|
||||
{
|
||||
$this->recording = false;
|
||||
|
||||
foreach ($this->events as $event) {
|
||||
$this->dispatcher->dispatch($event);
|
||||
}
|
||||
|
||||
$this->events = new EventCollection;
|
||||
}
|
||||
}
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of PHPUnit.
|
||||
*
|
||||
* (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 PHPUnit\Event;
|
||||
|
||||
use const PHP_EOL;
|
||||
use function array_key_exists;
|
||||
use function dirname;
|
||||
use function sprintf;
|
||||
use function str_starts_with;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
|
||||
*
|
||||
* @internal This class is not covered by the backward compatibility promise for PHPUnit
|
||||
*/
|
||||
final class DirectDispatcher implements SubscribableDispatcher
|
||||
{
|
||||
private readonly TypeMap $typeMap;
|
||||
|
||||
/**
|
||||
* @psalm-var array<class-string, list<Subscriber>>
|
||||
*/
|
||||
private array $subscribers = [];
|
||||
|
||||
/**
|
||||
* @psalm-var list<Tracer\Tracer>
|
||||
*/
|
||||
private array $tracers = [];
|
||||
|
||||
public function __construct(TypeMap $map)
|
||||
{
|
||||
$this->typeMap = $map;
|
||||
}
|
||||
|
||||
public function registerTracer(Tracer\Tracer $tracer): void
|
||||
{
|
||||
$this->tracers[] = $tracer;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws MapError
|
||||
* @throws UnknownSubscriberTypeException
|
||||
*/
|
||||
public function registerSubscriber(Subscriber $subscriber): void
|
||||
{
|
||||
if (!$this->typeMap->isKnownSubscriberType($subscriber)) {
|
||||
throw new UnknownSubscriberTypeException(
|
||||
sprintf(
|
||||
'Subscriber "%s" does not implement any known interface - did you forget to register it?',
|
||||
$subscriber::class,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
$eventClassName = $this->typeMap->map($subscriber);
|
||||
|
||||
if (!array_key_exists($eventClassName, $this->subscribers)) {
|
||||
$this->subscribers[$eventClassName] = [];
|
||||
}
|
||||
|
||||
$this->subscribers[$eventClassName][] = $subscriber;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Throwable
|
||||
* @throws UnknownEventTypeException
|
||||
*/
|
||||
public function dispatch(Event $event): void
|
||||
{
|
||||
$eventClassName = $event::class;
|
||||
|
||||
if (!$this->typeMap->isKnownEventType($event)) {
|
||||
throw new UnknownEventTypeException(
|
||||
sprintf(
|
||||
'Unknown event type "%s"',
|
||||
$eventClassName,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
foreach ($this->tracers as $tracer) {
|
||||
try {
|
||||
$tracer->trace($event);
|
||||
// @codeCoverageIgnoreStart
|
||||
} catch (Throwable $t) {
|
||||
$this->handleThrowable($t);
|
||||
}
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
|
||||
if (!array_key_exists($eventClassName, $this->subscribers)) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($this->subscribers[$eventClassName] as $subscriber) {
|
||||
try {
|
||||
$subscriber->notify($event);
|
||||
} catch (Throwable $t) {
|
||||
$this->handleThrowable($t);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function handleThrowable(Throwable $t): void
|
||||
{
|
||||
if ($this->isThrowableFromThirdPartySubscriber($t)) {
|
||||
Facade::emitter()->testRunnerTriggeredWarning(
|
||||
sprintf(
|
||||
'Exception in third-party event subscriber: %s%s%s',
|
||||
$t->getMessage(),
|
||||
PHP_EOL,
|
||||
$t->getTraceAsString(),
|
||||
),
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// @codeCoverageIgnoreStart
|
||||
throw $t;
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
|
||||
private function isThrowableFromThirdPartySubscriber(Throwable $t): bool
|
||||
{
|
||||
return !str_starts_with($t->getFile(), dirname(__DIR__, 2));
|
||||
}
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of PHPUnit.
|
||||
*
|
||||
* (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 PHPUnit\Event;
|
||||
|
||||
/**
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
|
||||
*
|
||||
* @internal This interface is not covered by the backward compatibility promise for PHPUnit
|
||||
*/
|
||||
interface Dispatcher
|
||||
{
|
||||
/**
|
||||
* @throws UnknownEventTypeException
|
||||
*/
|
||||
public function dispatch(Event $event): void;
|
||||
}
|
||||
Vendored
+25
@@ -0,0 +1,25 @@
|
||||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of PHPUnit.
|
||||
*
|
||||
* (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 PHPUnit\Event;
|
||||
|
||||
/**
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
|
||||
*
|
||||
* @internal This interface is not covered by the backward compatibility promise for PHPUnit
|
||||
*/
|
||||
interface SubscribableDispatcher extends Dispatcher
|
||||
{
|
||||
/**
|
||||
* @throws UnknownSubscriberTypeException
|
||||
*/
|
||||
public function registerSubscriber(Subscriber $subscriber): void;
|
||||
|
||||
public function registerTracer(Tracer\Tracer $tracer): void;
|
||||
}
|
||||
+1230
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,310 @@
|
||||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of PHPUnit.
|
||||
*
|
||||
* (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 PHPUnit\Event;
|
||||
|
||||
use PHPUnit\Event\Code\ClassMethod;
|
||||
use PHPUnit\Event\Code\ComparisonFailure;
|
||||
use PHPUnit\Event\Code\Throwable;
|
||||
use PHPUnit\Event\TestSuite\TestSuite;
|
||||
use PHPUnit\Framework\Constraint;
|
||||
use PHPUnit\TextUI\Configuration\Configuration;
|
||||
|
||||
/**
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
|
||||
*
|
||||
* @internal This class is not covered by the backward compatibility promise for PHPUnit
|
||||
*/
|
||||
interface Emitter
|
||||
{
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
public function exportObjects(): void;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
public function exportsObjects(): bool;
|
||||
|
||||
public function applicationStarted(): void;
|
||||
|
||||
public function testRunnerStarted(): void;
|
||||
|
||||
public function testRunnerConfigured(Configuration $configuration): void;
|
||||
|
||||
public function testRunnerBootstrapFinished(string $filename): void;
|
||||
|
||||
public function testRunnerLoadedExtensionFromPhar(string $filename, string $name, string $version): void;
|
||||
|
||||
/**
|
||||
* @psalm-param class-string $className
|
||||
* @psalm-param array<string, string> $parameters
|
||||
*/
|
||||
public function testRunnerBootstrappedExtension(string $className, array $parameters): void;
|
||||
|
||||
public function dataProviderMethodCalled(ClassMethod $testMethod, ClassMethod $dataProviderMethod): void;
|
||||
|
||||
public function dataProviderMethodFinished(ClassMethod $testMethod, ClassMethod ...$calledMethods): void;
|
||||
|
||||
public function testSuiteLoaded(TestSuite $testSuite): void;
|
||||
|
||||
public function testSuiteFiltered(TestSuite $testSuite): void;
|
||||
|
||||
public function testSuiteSorted(int $executionOrder, int $executionOrderDefects, bool $resolveDependencies): void;
|
||||
|
||||
public function testRunnerEventFacadeSealed(): void;
|
||||
|
||||
public function testRunnerExecutionStarted(TestSuite $testSuite): void;
|
||||
|
||||
public function testRunnerDisabledGarbageCollection(): void;
|
||||
|
||||
public function testRunnerTriggeredGarbageCollection(): void;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* @psalm-param class-string $testClassName
|
||||
*/
|
||||
public function testBeforeFirstTestMethodCalled(string $testClassName, ClassMethod $calledMethod): void;
|
||||
|
||||
/**
|
||||
* @psalm-param class-string $testClassName
|
||||
*/
|
||||
public function testBeforeFirstTestMethodErrored(string $testClassName, ClassMethod $calledMethod, Throwable $throwable): void;
|
||||
|
||||
/**
|
||||
* @psalm-param class-string $testClassName
|
||||
*/
|
||||
public function testBeforeFirstTestMethodFinished(string $testClassName, ClassMethod ...$calledMethods): void;
|
||||
|
||||
/**
|
||||
* @psalm-param class-string $testClassName
|
||||
*/
|
||||
public function testBeforeTestMethodCalled(string $testClassName, ClassMethod $calledMethod): void;
|
||||
|
||||
/**
|
||||
* @psalm-param class-string $testClassName
|
||||
*/
|
||||
public function testBeforeTestMethodFinished(string $testClassName, ClassMethod ...$calledMethods): void;
|
||||
|
||||
/**
|
||||
* @psalm-param class-string $testClassName
|
||||
*/
|
||||
public function testPreConditionCalled(string $testClassName, ClassMethod $calledMethod): void;
|
||||
|
||||
/**
|
||||
* @psalm-param class-string $testClassName
|
||||
*/
|
||||
public function testPreConditionFinished(string $testClassName, ClassMethod ...$calledMethods): void;
|
||||
|
||||
public function testPrepared(Code\Test $test): void;
|
||||
|
||||
/**
|
||||
* @psalm-param class-string $className
|
||||
*/
|
||||
public function testRegisteredComparator(string $className): void;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
public function testAssertionSucceeded(mixed $value, Constraint\Constraint $constraint, string $message): void;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
public function testAssertionFailed(mixed $value, Constraint\Constraint $constraint, string $message): void;
|
||||
|
||||
/**
|
||||
* @psalm-param class-string $className
|
||||
*/
|
||||
public function testCreatedMockObject(string $className): void;
|
||||
|
||||
/**
|
||||
* @psalm-param list<class-string> $interfaces
|
||||
*/
|
||||
public function testCreatedMockObjectForIntersectionOfInterfaces(array $interfaces): void;
|
||||
|
||||
/**
|
||||
* @psalm-param trait-string $traitName
|
||||
*/
|
||||
public function testCreatedMockObjectForTrait(string $traitName): void;
|
||||
|
||||
/**
|
||||
* @psalm-param class-string $className
|
||||
*/
|
||||
public function testCreatedMockObjectForAbstractClass(string $className): void;
|
||||
|
||||
/**
|
||||
* @psalm-param class-string $originalClassName
|
||||
* @psalm-param class-string $mockClassName
|
||||
*/
|
||||
public function testCreatedMockObjectFromWsdl(string $wsdlFile, string $originalClassName, string $mockClassName, array $methods, bool $callOriginalConstructor, array $options): void;
|
||||
|
||||
/**
|
||||
* @psalm-param class-string $className
|
||||
*/
|
||||
public function testCreatedPartialMockObject(string $className, string ...$methodNames): void;
|
||||
|
||||
/**
|
||||
* @psalm-param class-string $className
|
||||
*/
|
||||
public function testCreatedTestProxy(string $className, array $constructorArguments): void;
|
||||
|
||||
/**
|
||||
* @psalm-param class-string $className
|
||||
*/
|
||||
public function testCreatedStub(string $className): void;
|
||||
|
||||
/**
|
||||
* @psalm-param list<class-string> $interfaces
|
||||
*/
|
||||
public function testCreatedStubForIntersectionOfInterfaces(array $interfaces): void;
|
||||
|
||||
public function testErrored(Code\Test $test, Throwable $throwable): void;
|
||||
|
||||
public function testFailed(Code\Test $test, Throwable $throwable, ?ComparisonFailure $comparisonFailure): void;
|
||||
|
||||
public function testPassed(Code\Test $test): void;
|
||||
|
||||
/**
|
||||
* @psalm-param non-empty-string $message
|
||||
*/
|
||||
public function testConsideredRisky(Code\Test $test, string $message): void;
|
||||
|
||||
public function testMarkedAsIncomplete(Code\Test $test, Throwable $throwable): void;
|
||||
|
||||
/**
|
||||
* @psalm-param non-empty-string $message
|
||||
*/
|
||||
public function testSkipped(Code\Test $test, string $message): void;
|
||||
|
||||
/**
|
||||
* @psalm-param non-empty-string $message
|
||||
*/
|
||||
public function testTriggeredPhpunitDeprecation(Code\Test $test, string $message): void;
|
||||
|
||||
/**
|
||||
* @psalm-param non-empty-string $message
|
||||
* @psalm-param non-empty-string $file
|
||||
* @psalm-param positive-int $line
|
||||
*/
|
||||
public function testTriggeredPhpDeprecation(Code\Test $test, string $message, string $file, int $line, bool $suppressed, bool $ignoredByBaseline, bool $ignoredByTest): void;
|
||||
|
||||
/**
|
||||
* @psalm-param non-empty-string $message
|
||||
* @psalm-param non-empty-string $file
|
||||
* @psalm-param positive-int $line
|
||||
*/
|
||||
public function testTriggeredDeprecation(Code\Test $test, string $message, string $file, int $line, bool $suppressed, bool $ignoredByBaseline, bool $ignoredByTest): void;
|
||||
|
||||
/**
|
||||
* @psalm-param non-empty-string $message
|
||||
* @psalm-param non-empty-string $file
|
||||
* @psalm-param positive-int $line
|
||||
*/
|
||||
public function testTriggeredError(Code\Test $test, string $message, string $file, int $line, bool $suppressed): void;
|
||||
|
||||
/**
|
||||
* @psalm-param non-empty-string $message
|
||||
* @psalm-param non-empty-string $file
|
||||
* @psalm-param positive-int $line
|
||||
*/
|
||||
public function testTriggeredNotice(Code\Test $test, string $message, string $file, int $line, bool $suppressed, bool $ignoredByBaseline): void;
|
||||
|
||||
/**
|
||||
* @psalm-param non-empty-string $message
|
||||
* @psalm-param non-empty-string $file
|
||||
* @psalm-param positive-int $line
|
||||
*/
|
||||
public function testTriggeredPhpNotice(Code\Test $test, string $message, string $file, int $line, bool $suppressed, bool $ignoredByBaseline): void;
|
||||
|
||||
/**
|
||||
* @psalm-param non-empty-string $message
|
||||
* @psalm-param non-empty-string $file
|
||||
* @psalm-param positive-int $line
|
||||
*/
|
||||
public function testTriggeredWarning(Code\Test $test, string $message, string $file, int $line, bool $suppressed, bool $ignoredByBaseline): void;
|
||||
|
||||
/**
|
||||
* @psalm-param non-empty-string $message
|
||||
* @psalm-param non-empty-string $file
|
||||
* @psalm-param positive-int $line
|
||||
*/
|
||||
public function testTriggeredPhpWarning(Code\Test $test, string $message, string $file, int $line, bool $suppressed, bool $ignoredByBaseline): void;
|
||||
|
||||
/**
|
||||
* @psalm-param non-empty-string $message
|
||||
*/
|
||||
public function testTriggeredPhpunitError(Code\Test $test, string $message): void;
|
||||
|
||||
/**
|
||||
* @psalm-param non-empty-string $message
|
||||
*/
|
||||
public function testTriggeredPhpunitWarning(Code\Test $test, string $message): void;
|
||||
|
||||
/**
|
||||
* @psalm-param non-empty-string $output
|
||||
*/
|
||||
public function testPrintedUnexpectedOutput(string $output): void;
|
||||
|
||||
public function testFinished(Code\Test $test, int $numberOfAssertionsPerformed): void;
|
||||
|
||||
/**
|
||||
* @psalm-param class-string $testClassName
|
||||
*/
|
||||
public function testPostConditionCalled(string $testClassName, ClassMethod $calledMethod): void;
|
||||
|
||||
/**
|
||||
* @psalm-param class-string $testClassName
|
||||
*/
|
||||
public function testPostConditionFinished(string $testClassName, ClassMethod ...$calledMethods): void;
|
||||
|
||||
/**
|
||||
* @psalm-param class-string $testClassName
|
||||
*/
|
||||
public function testAfterTestMethodCalled(string $testClassName, ClassMethod $calledMethod): void;
|
||||
|
||||
/**
|
||||
* @psalm-param class-string $testClassName
|
||||
*/
|
||||
public function testAfterTestMethodFinished(string $testClassName, ClassMethod ...$calledMethods): void;
|
||||
|
||||
/**
|
||||
* @psalm-param class-string $testClassName
|
||||
*/
|
||||
public function testAfterLastTestMethodCalled(string $testClassName, ClassMethod $calledMethod): void;
|
||||
|
||||
/**
|
||||
* @psalm-param class-string $testClassName
|
||||
*/
|
||||
public function testAfterLastTestMethodFinished(string $testClassName, ClassMethod ...$calledMethods): void;
|
||||
|
||||
public function testSuiteFinished(TestSuite $testSuite): void;
|
||||
|
||||
public function testRunnerTriggeredDeprecation(string $message): void;
|
||||
|
||||
public function testRunnerTriggeredWarning(string $message): void;
|
||||
|
||||
public function testRunnerEnabledGarbageCollection(): void;
|
||||
|
||||
public function testRunnerExecutionAborted(): void;
|
||||
|
||||
public function testRunnerExecutionFinished(): void;
|
||||
|
||||
public function testRunnerFinished(): void;
|
||||
|
||||
public function applicationFinished(int $shellExitCode): void;
|
||||
}
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of PHPUnit.
|
||||
*
|
||||
* (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 PHPUnit\Event\Test;
|
||||
|
||||
use const PHP_EOL;
|
||||
use function sprintf;
|
||||
use PHPUnit\Event\Code;
|
||||
use PHPUnit\Event\Code\Throwable;
|
||||
use PHPUnit\Event\Event;
|
||||
use PHPUnit\Event\Telemetry;
|
||||
|
||||
/**
|
||||
* @psalm-immutable
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
|
||||
*/
|
||||
final class BeforeFirstTestMethodErrored implements Event
|
||||
{
|
||||
private readonly Telemetry\Info $telemetryInfo;
|
||||
|
||||
/**
|
||||
* @psalm-var class-string
|
||||
*/
|
||||
private readonly string $testClassName;
|
||||
private readonly Code\ClassMethod $calledMethod;
|
||||
private readonly Throwable $throwable;
|
||||
|
||||
/**
|
||||
* @psalm-param class-string $testClassName
|
||||
*/
|
||||
public function __construct(Telemetry\Info $telemetryInfo, string $testClassName, Code\ClassMethod $calledMethod, Throwable $throwable)
|
||||
{
|
||||
$this->telemetryInfo = $telemetryInfo;
|
||||
$this->testClassName = $testClassName;
|
||||
$this->calledMethod = $calledMethod;
|
||||
$this->throwable = $throwable;
|
||||
}
|
||||
|
||||
public function telemetryInfo(): Telemetry\Info
|
||||
{
|
||||
return $this->telemetryInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-return class-string
|
||||
*/
|
||||
public function testClassName(): string
|
||||
{
|
||||
return $this->testClassName;
|
||||
}
|
||||
|
||||
public function calledMethod(): Code\ClassMethod
|
||||
{
|
||||
return $this->calledMethod;
|
||||
}
|
||||
|
||||
public function throwable(): Throwable
|
||||
{
|
||||
return $this->throwable;
|
||||
}
|
||||
|
||||
public function asString(): string
|
||||
{
|
||||
$message = $this->throwable->message();
|
||||
|
||||
if (!empty($message)) {
|
||||
$message = PHP_EOL . $message;
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
'Before First Test Method Errored (%s::%s)%s',
|
||||
$this->calledMethod->className(),
|
||||
$this->calledMethod->methodName(),
|
||||
$message,
|
||||
);
|
||||
}
|
||||
}
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of PHPUnit.
|
||||
*
|
||||
* (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 PHPUnit\Event\Test;
|
||||
|
||||
use const PHP_EOL;
|
||||
use function sprintf;
|
||||
use PHPUnit\Event\Code\ClassMethod;
|
||||
use PHPUnit\Event\Event;
|
||||
use PHPUnit\Event\Telemetry;
|
||||
|
||||
/**
|
||||
* @psalm-immutable
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
|
||||
*/
|
||||
final class DataProviderMethodFinished implements Event
|
||||
{
|
||||
private readonly Telemetry\Info $telemetryInfo;
|
||||
private readonly ClassMethod $testMethod;
|
||||
|
||||
/**
|
||||
* @psalm-var list<ClassMethod>
|
||||
*/
|
||||
private readonly array $calledMethods;
|
||||
|
||||
public function __construct(Telemetry\Info $telemetryInfo, ClassMethod $testMethod, ClassMethod ...$calledMethods)
|
||||
{
|
||||
$this->telemetryInfo = $telemetryInfo;
|
||||
$this->testMethod = $testMethod;
|
||||
$this->calledMethods = $calledMethods;
|
||||
}
|
||||
|
||||
public function telemetryInfo(): Telemetry\Info
|
||||
{
|
||||
return $this->telemetryInfo;
|
||||
}
|
||||
|
||||
public function testMethod(): ClassMethod
|
||||
{
|
||||
return $this->testMethod;
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-return list<ClassMethod>
|
||||
*/
|
||||
public function calledMethods(): array
|
||||
{
|
||||
return $this->calledMethods;
|
||||
}
|
||||
|
||||
public function asString(): string
|
||||
{
|
||||
$buffer = sprintf(
|
||||
'Data Provider Method Finished for %s::%s:',
|
||||
$this->testMethod->className(),
|
||||
$this->testMethod->methodName(),
|
||||
);
|
||||
|
||||
foreach ($this->calledMethods as $calledMethod) {
|
||||
$buffer .= sprintf(
|
||||
PHP_EOL . '- %s::%s',
|
||||
$calledMethod->className(),
|
||||
$calledMethod->methodName(),
|
||||
);
|
||||
}
|
||||
|
||||
return $buffer;
|
||||
}
|
||||
}
|
||||
Vendored
+61
@@ -0,0 +1,61 @@
|
||||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of PHPUnit.
|
||||
*
|
||||
* (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 PHPUnit\Event\Test;
|
||||
|
||||
use const PHP_EOL;
|
||||
use function sprintf;
|
||||
use PHPUnit\Event\Event;
|
||||
use PHPUnit\Event\Telemetry;
|
||||
|
||||
/**
|
||||
* @psalm-immutable
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
|
||||
*/
|
||||
final class PrintedUnexpectedOutput implements Event
|
||||
{
|
||||
private readonly Telemetry\Info $telemetryInfo;
|
||||
|
||||
/**
|
||||
* @psalm-var non-empty-string
|
||||
*/
|
||||
private readonly string $output;
|
||||
|
||||
/**
|
||||
* @psalm-param non-empty-string $output
|
||||
*/
|
||||
public function __construct(Telemetry\Info $telemetryInfo, string $output)
|
||||
{
|
||||
$this->telemetryInfo = $telemetryInfo;
|
||||
$this->output = $output;
|
||||
}
|
||||
|
||||
public function telemetryInfo(): Telemetry\Info
|
||||
{
|
||||
return $this->telemetryInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-return non-empty-string
|
||||
*/
|
||||
public function output(): string
|
||||
{
|
||||
return $this->output;
|
||||
}
|
||||
|
||||
public function asString(): string
|
||||
{
|
||||
return sprintf(
|
||||
'Test Printed Unexpected Output%s%s',
|
||||
PHP_EOL,
|
||||
$this->output,
|
||||
);
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of PHPUnit.
|
||||
*
|
||||
* (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 PHPUnit\Event\Code;
|
||||
|
||||
use PHPUnit\Event\Exception;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
|
||||
*
|
||||
* @internal This class is not covered by the backward compatibility promise for PHPUnit
|
||||
*/
|
||||
final class NoTestCaseObjectOnCallStackException extends RuntimeException implements Exception
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('Cannot find TestCase object on call stack');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of PHPUnit.
|
||||
*
|
||||
* (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 PHPUnit\Event;
|
||||
|
||||
use function gc_status;
|
||||
use PHPUnit\Event\Telemetry\HRTime;
|
||||
use PHPUnit\Event\Telemetry\Php81GarbageCollectorStatusProvider;
|
||||
use PHPUnit\Event\Telemetry\Php83GarbageCollectorStatusProvider;
|
||||
|
||||
/**
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
|
||||
*
|
||||
* @internal This class is not covered by the backward compatibility promise for PHPUnit
|
||||
*/
|
||||
final class Facade
|
||||
{
|
||||
private static ?self $instance = null;
|
||||
private Emitter $emitter;
|
||||
private ?TypeMap $typeMap = null;
|
||||
private ?DeferringDispatcher $deferringDispatcher = null;
|
||||
private bool $sealed = false;
|
||||
|
||||
public static function instance(): self
|
||||
{
|
||||
if (self::$instance === null) {
|
||||
self::$instance = new self;
|
||||
}
|
||||
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public static function emitter(): Emitter
|
||||
{
|
||||
return self::instance()->emitter;
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->emitter = $this->createDispatchingEmitter();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws EventFacadeIsSealedException
|
||||
* @throws UnknownSubscriberTypeException
|
||||
*/
|
||||
public function registerSubscribers(Subscriber ...$subscribers): void
|
||||
{
|
||||
foreach ($subscribers as $subscriber) {
|
||||
$this->registerSubscriber($subscriber);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws EventFacadeIsSealedException
|
||||
* @throws UnknownSubscriberTypeException
|
||||
*/
|
||||
public function registerSubscriber(Subscriber $subscriber): void
|
||||
{
|
||||
if ($this->sealed) {
|
||||
throw new EventFacadeIsSealedException;
|
||||
}
|
||||
|
||||
$this->deferredDispatcher()->registerSubscriber($subscriber);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws EventFacadeIsSealedException
|
||||
*/
|
||||
public function registerTracer(Tracer\Tracer $tracer): void
|
||||
{
|
||||
if ($this->sealed) {
|
||||
throw new EventFacadeIsSealedException;
|
||||
}
|
||||
|
||||
$this->deferredDispatcher()->registerTracer($tracer);
|
||||
}
|
||||
|
||||
/**
|
||||
* @codeCoverageIgnore
|
||||
*
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
public function initForIsolation(HRTime $offset, bool $exportObjects): CollectingDispatcher
|
||||
{
|
||||
$dispatcher = new CollectingDispatcher;
|
||||
|
||||
$this->emitter = new DispatchingEmitter(
|
||||
$dispatcher,
|
||||
new Telemetry\System(
|
||||
new Telemetry\SystemStopWatchWithOffset($offset),
|
||||
new Telemetry\SystemMemoryMeter,
|
||||
$this->garbageCollectorStatusProvider(),
|
||||
),
|
||||
);
|
||||
|
||||
if ($exportObjects) {
|
||||
$this->emitter->exportObjects();
|
||||
}
|
||||
|
||||
$this->sealed = true;
|
||||
|
||||
return $dispatcher;
|
||||
}
|
||||
|
||||
public function forward(EventCollection $events): void
|
||||
{
|
||||
$dispatcher = $this->deferredDispatcher();
|
||||
|
||||
foreach ($events as $event) {
|
||||
$dispatcher->dispatch($event);
|
||||
}
|
||||
}
|
||||
|
||||
public function seal(): void
|
||||
{
|
||||
$this->deferredDispatcher()->flush();
|
||||
|
||||
$this->sealed = true;
|
||||
|
||||
$this->emitter->testRunnerEventFacadeSealed();
|
||||
}
|
||||
|
||||
private function createDispatchingEmitter(): DispatchingEmitter
|
||||
{
|
||||
return new DispatchingEmitter(
|
||||
$this->deferredDispatcher(),
|
||||
$this->createTelemetrySystem(),
|
||||
);
|
||||
}
|
||||
|
||||
private function createTelemetrySystem(): Telemetry\System
|
||||
{
|
||||
return new Telemetry\System(
|
||||
new Telemetry\SystemStopWatch,
|
||||
new Telemetry\SystemMemoryMeter,
|
||||
$this->garbageCollectorStatusProvider(),
|
||||
);
|
||||
}
|
||||
|
||||
private function deferredDispatcher(): DeferringDispatcher
|
||||
{
|
||||
if ($this->deferringDispatcher === null) {
|
||||
$this->deferringDispatcher = new DeferringDispatcher(
|
||||
new DirectDispatcher($this->typeMap()),
|
||||
);
|
||||
}
|
||||
|
||||
return $this->deferringDispatcher;
|
||||
}
|
||||
|
||||
private function typeMap(): TypeMap
|
||||
{
|
||||
if ($this->typeMap === null) {
|
||||
$typeMap = new TypeMap;
|
||||
|
||||
$this->registerDefaultTypes($typeMap);
|
||||
|
||||
$this->typeMap = $typeMap;
|
||||
}
|
||||
|
||||
return $this->typeMap;
|
||||
}
|
||||
|
||||
private function registerDefaultTypes(TypeMap $typeMap): void
|
||||
{
|
||||
$defaultEvents = [
|
||||
Application\Started::class,
|
||||
Application\Finished::class,
|
||||
|
||||
Test\DataProviderMethodCalled::class,
|
||||
Test\DataProviderMethodFinished::class,
|
||||
Test\MarkedIncomplete::class,
|
||||
Test\AfterLastTestMethodCalled::class,
|
||||
Test\AfterLastTestMethodFinished::class,
|
||||
Test\AfterTestMethodCalled::class,
|
||||
Test\AfterTestMethodFinished::class,
|
||||
Test\AssertionSucceeded::class,
|
||||
Test\AssertionFailed::class,
|
||||
Test\BeforeFirstTestMethodCalled::class,
|
||||
Test\BeforeFirstTestMethodErrored::class,
|
||||
Test\BeforeFirstTestMethodFinished::class,
|
||||
Test\BeforeTestMethodCalled::class,
|
||||
Test\BeforeTestMethodFinished::class,
|
||||
Test\ComparatorRegistered::class,
|
||||
Test\ConsideredRisky::class,
|
||||
Test\DeprecationTriggered::class,
|
||||
Test\Errored::class,
|
||||
Test\ErrorTriggered::class,
|
||||
Test\Failed::class,
|
||||
Test\Finished::class,
|
||||
Test\NoticeTriggered::class,
|
||||
Test\Passed::class,
|
||||
Test\PhpDeprecationTriggered::class,
|
||||
Test\PhpNoticeTriggered::class,
|
||||
Test\PhpunitDeprecationTriggered::class,
|
||||
Test\PhpunitErrorTriggered::class,
|
||||
Test\PhpunitWarningTriggered::class,
|
||||
Test\PhpWarningTriggered::class,
|
||||
Test\PostConditionCalled::class,
|
||||
Test\PostConditionFinished::class,
|
||||
Test\PreConditionCalled::class,
|
||||
Test\PreConditionFinished::class,
|
||||
Test\PreparationStarted::class,
|
||||
Test\Prepared::class,
|
||||
Test\PreparationFailed::class,
|
||||
Test\PrintedUnexpectedOutput::class,
|
||||
Test\Skipped::class,
|
||||
Test\WarningTriggered::class,
|
||||
|
||||
Test\MockObjectCreated::class,
|
||||
Test\MockObjectForAbstractClassCreated::class,
|
||||
Test\MockObjectForIntersectionOfInterfacesCreated::class,
|
||||
Test\MockObjectForTraitCreated::class,
|
||||
Test\MockObjectFromWsdlCreated::class,
|
||||
Test\PartialMockObjectCreated::class,
|
||||
Test\TestProxyCreated::class,
|
||||
Test\TestStubCreated::class,
|
||||
Test\TestStubForIntersectionOfInterfacesCreated::class,
|
||||
|
||||
TestRunner\BootstrapFinished::class,
|
||||
TestRunner\Configured::class,
|
||||
TestRunner\EventFacadeSealed::class,
|
||||
TestRunner\ExecutionAborted::class,
|
||||
TestRunner\ExecutionFinished::class,
|
||||
TestRunner\ExecutionStarted::class,
|
||||
TestRunner\ExtensionLoadedFromPhar::class,
|
||||
TestRunner\ExtensionBootstrapped::class,
|
||||
TestRunner\Finished::class,
|
||||
TestRunner\Started::class,
|
||||
TestRunner\DeprecationTriggered::class,
|
||||
TestRunner\WarningTriggered::class,
|
||||
TestRunner\GarbageCollectionDisabled::class,
|
||||
TestRunner\GarbageCollectionTriggered::class,
|
||||
TestRunner\GarbageCollectionEnabled::class,
|
||||
|
||||
TestSuite\Filtered::class,
|
||||
TestSuite\Finished::class,
|
||||
TestSuite\Loaded::class,
|
||||
TestSuite\Skipped::class,
|
||||
TestSuite\Sorted::class,
|
||||
TestSuite\Started::class,
|
||||
];
|
||||
|
||||
foreach ($defaultEvents as $eventClass) {
|
||||
$typeMap->addMapping(
|
||||
$eventClass . 'Subscriber',
|
||||
$eventClass,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private function garbageCollectorStatusProvider(): Telemetry\GarbageCollectorStatusProvider
|
||||
{
|
||||
if (!isset(gc_status()['running'])) {
|
||||
// @codeCoverageIgnoreStart
|
||||
return new Php81GarbageCollectorStatusProvider;
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
|
||||
return new Php83GarbageCollectorStatusProvider;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of PHPUnit.
|
||||
*
|
||||
* (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 PHPUnit\Event;
|
||||
|
||||
use function array_key_exists;
|
||||
use function class_exists;
|
||||
use function class_implements;
|
||||
use function in_array;
|
||||
use function interface_exists;
|
||||
use function sprintf;
|
||||
|
||||
/**
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
|
||||
*
|
||||
* @internal This class is not covered by the backward compatibility promise for PHPUnit
|
||||
*/
|
||||
final class TypeMap
|
||||
{
|
||||
/**
|
||||
* @psalm-var array<class-string, class-string>
|
||||
*/
|
||||
private array $mapping = [];
|
||||
|
||||
/**
|
||||
* @psalm-param class-string $subscriberInterface
|
||||
* @psalm-param class-string $eventClass
|
||||
*
|
||||
* @throws EventAlreadyAssignedException
|
||||
* @throws InvalidEventException
|
||||
* @throws InvalidSubscriberException
|
||||
* @throws SubscriberTypeAlreadyRegisteredException
|
||||
* @throws UnknownEventException
|
||||
* @throws UnknownSubscriberException
|
||||
*/
|
||||
public function addMapping(string $subscriberInterface, string $eventClass): void
|
||||
{
|
||||
$this->ensureSubscriberInterfaceExists($subscriberInterface);
|
||||
$this->ensureSubscriberInterfaceExtendsInterface($subscriberInterface);
|
||||
$this->ensureEventClassExists($eventClass);
|
||||
$this->ensureEventClassImplementsEventInterface($eventClass);
|
||||
$this->ensureSubscriberWasNotAlreadyRegistered($subscriberInterface);
|
||||
$this->ensureEventWasNotAlreadyAssigned($eventClass);
|
||||
|
||||
$this->mapping[$subscriberInterface] = $eventClass;
|
||||
}
|
||||
|
||||
public function isKnownSubscriberType(Subscriber $subscriber): bool
|
||||
{
|
||||
foreach (class_implements($subscriber) as $interface) {
|
||||
if (array_key_exists($interface, $this->mapping)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function isKnownEventType(Event $event): bool
|
||||
{
|
||||
return in_array($event::class, $this->mapping, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-return class-string
|
||||
*
|
||||
* @throws MapError
|
||||
*/
|
||||
public function map(Subscriber $subscriber): string
|
||||
{
|
||||
foreach (class_implements($subscriber) as $interface) {
|
||||
if (array_key_exists($interface, $this->mapping)) {
|
||||
return $this->mapping[$interface];
|
||||
}
|
||||
}
|
||||
|
||||
throw new MapError(
|
||||
sprintf(
|
||||
'Subscriber "%s" does not implement a known interface',
|
||||
$subscriber::class,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-param class-string $subscriberInterface
|
||||
*
|
||||
* @throws UnknownSubscriberException
|
||||
*/
|
||||
private function ensureSubscriberInterfaceExists(string $subscriberInterface): void
|
||||
{
|
||||
if (!interface_exists($subscriberInterface)) {
|
||||
throw new UnknownSubscriberException(
|
||||
sprintf(
|
||||
'Subscriber "%s" does not exist or is not an interface',
|
||||
$subscriberInterface,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-param class-string $eventClass
|
||||
*
|
||||
* @throws UnknownEventException
|
||||
*/
|
||||
private function ensureEventClassExists(string $eventClass): void
|
||||
{
|
||||
if (!class_exists($eventClass)) {
|
||||
throw new UnknownEventException(
|
||||
sprintf(
|
||||
'Event class "%s" does not exist',
|
||||
$eventClass,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-param class-string $subscriberInterface
|
||||
*
|
||||
* @throws InvalidSubscriberException
|
||||
*/
|
||||
private function ensureSubscriberInterfaceExtendsInterface(string $subscriberInterface): void
|
||||
{
|
||||
if (!in_array(Subscriber::class, class_implements($subscriberInterface), true)) {
|
||||
throw new InvalidSubscriberException(
|
||||
sprintf(
|
||||
'Subscriber "%s" does not extend Subscriber interface',
|
||||
$subscriberInterface,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-param class-string $eventClass
|
||||
*
|
||||
* @throws InvalidEventException
|
||||
*/
|
||||
private function ensureEventClassImplementsEventInterface(string $eventClass): void
|
||||
{
|
||||
if (!in_array(Event::class, class_implements($eventClass), true)) {
|
||||
throw new InvalidEventException(
|
||||
sprintf(
|
||||
'Event "%s" does not implement Event interface',
|
||||
$eventClass,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-param class-string $subscriberInterface
|
||||
*
|
||||
* @throws SubscriberTypeAlreadyRegisteredException
|
||||
*/
|
||||
private function ensureSubscriberWasNotAlreadyRegistered(string $subscriberInterface): void
|
||||
{
|
||||
if (array_key_exists($subscriberInterface, $this->mapping)) {
|
||||
throw new SubscriberTypeAlreadyRegisteredException(
|
||||
sprintf(
|
||||
'Subscriber type "%s" already registered',
|
||||
$subscriberInterface,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-param class-string $eventClass
|
||||
*
|
||||
* @throws EventAlreadyAssignedException
|
||||
*/
|
||||
private function ensureEventWasNotAlreadyAssigned(string $eventClass): void
|
||||
{
|
||||
if (in_array($eventClass, $this->mapping, true)) {
|
||||
throw new EventAlreadyAssignedException(
|
||||
sprintf(
|
||||
'Event "%s" already assigned',
|
||||
$eventClass,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of PHPUnit.
|
||||
*
|
||||
* (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 PHPUnit\Event\Code;
|
||||
|
||||
use function is_bool;
|
||||
use function is_scalar;
|
||||
use function print_r;
|
||||
use PHPUnit\Framework\ExpectationFailedException;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
|
||||
*
|
||||
* @internal This class is not covered by the backward compatibility promise for PHPUnit
|
||||
*/
|
||||
final class ComparisonFailureBuilder
|
||||
{
|
||||
public static function from(Throwable $t): ?ComparisonFailure
|
||||
{
|
||||
if (!$t instanceof ExpectationFailedException) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!$t->getComparisonFailure()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$expectedAsString = $t->getComparisonFailure()->getExpectedAsString();
|
||||
|
||||
if (empty($expectedAsString)) {
|
||||
$expectedAsString = self::mapScalarValueToString($t->getComparisonFailure()->getExpected());
|
||||
}
|
||||
|
||||
$actualAsString = $t->getComparisonFailure()->getActualAsString();
|
||||
|
||||
if (empty($actualAsString)) {
|
||||
$actualAsString = self::mapScalarValueToString($t->getComparisonFailure()->getActual());
|
||||
}
|
||||
|
||||
return new ComparisonFailure(
|
||||
$expectedAsString,
|
||||
$actualAsString,
|
||||
$t->getComparisonFailure()->getDiff(),
|
||||
);
|
||||
}
|
||||
|
||||
private static function mapScalarValueToString(mixed $value): string
|
||||
{
|
||||
if ($value === null) {
|
||||
return 'null';
|
||||
}
|
||||
|
||||
if (is_bool($value)) {
|
||||
return $value ? 'true' : 'false';
|
||||
}
|
||||
|
||||
if (is_scalar($value)) {
|
||||
return print_r($value, true);
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of PHPUnit.
|
||||
*
|
||||
* (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 PHPUnit\Event\Telemetry;
|
||||
|
||||
/**
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
|
||||
*
|
||||
* @internal This interface is not covered by the backward compatibility promise for PHPUnit
|
||||
*/
|
||||
interface GarbageCollectorStatusProvider
|
||||
{
|
||||
public function status(): GarbageCollectorStatus;
|
||||
}
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of PHPUnit.
|
||||
*
|
||||
* (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 PHPUnit\Event\Telemetry;
|
||||
|
||||
use function sprintf;
|
||||
use PHPUnit\Event\InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* @psalm-immutable
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
|
||||
*/
|
||||
final class HRTime
|
||||
{
|
||||
private readonly int $seconds;
|
||||
private readonly int $nanoseconds;
|
||||
|
||||
/**
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public static function fromSecondsAndNanoseconds(int $seconds, int $nanoseconds): self
|
||||
{
|
||||
return new self(
|
||||
$seconds,
|
||||
$nanoseconds,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
private function __construct(int $seconds, int $nanoseconds)
|
||||
{
|
||||
$this->ensureNotNegative($seconds, 'seconds');
|
||||
$this->ensureNotNegative($nanoseconds, 'nanoseconds');
|
||||
$this->ensureNanoSecondsInRange($nanoseconds);
|
||||
|
||||
$this->seconds = $seconds;
|
||||
$this->nanoseconds = $nanoseconds;
|
||||
}
|
||||
|
||||
public function seconds(): int
|
||||
{
|
||||
return $this->seconds;
|
||||
}
|
||||
|
||||
public function nanoseconds(): int
|
||||
{
|
||||
return $this->nanoseconds;
|
||||
}
|
||||
|
||||
public function duration(self $start): Duration
|
||||
{
|
||||
$seconds = $this->seconds - $start->seconds();
|
||||
$nanoseconds = $this->nanoseconds - $start->nanoseconds();
|
||||
|
||||
if ($nanoseconds < 0) {
|
||||
$seconds--;
|
||||
|
||||
$nanoseconds += 1000000000;
|
||||
}
|
||||
|
||||
if ($seconds < 0) {
|
||||
return Duration::fromSecondsAndNanoseconds(0, 0);
|
||||
}
|
||||
|
||||
return Duration::fromSecondsAndNanoseconds(
|
||||
$seconds,
|
||||
$nanoseconds,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
private function ensureNotNegative(int $value, string $type): void
|
||||
{
|
||||
if ($value < 0) {
|
||||
throw new InvalidArgumentException(
|
||||
sprintf(
|
||||
'Value for %s must not be negative.',
|
||||
$type,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
private function ensureNanoSecondsInRange(int $nanoseconds): void
|
||||
{
|
||||
if ($nanoseconds > 999999999) {
|
||||
throw new InvalidArgumentException(
|
||||
'Value for nanoseconds must not be greater than 999999999.',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of PHPUnit.
|
||||
*
|
||||
* (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 PHPUnit\Event\Telemetry;
|
||||
|
||||
/**
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
|
||||
*
|
||||
* @internal This interface is not covered by the backward compatibility promise for PHPUnit
|
||||
*/
|
||||
interface MemoryMeter
|
||||
{
|
||||
public function memoryUsage(): MemoryUsage;
|
||||
|
||||
public function peakMemoryUsage(): MemoryUsage;
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of PHPUnit.
|
||||
*
|
||||
* (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 PHPUnit\Event\Telemetry;
|
||||
|
||||
use function gc_status;
|
||||
|
||||
/**
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
|
||||
*
|
||||
* @internal This class is not covered by the backward compatibility promise for PHPUnit
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
final class Php81GarbageCollectorStatusProvider implements GarbageCollectorStatusProvider
|
||||
{
|
||||
public function status(): GarbageCollectorStatus
|
||||
{
|
||||
$status = gc_status();
|
||||
|
||||
return new GarbageCollectorStatus(
|
||||
$status['runs'],
|
||||
$status['collected'],
|
||||
$status['threshold'],
|
||||
$status['roots'],
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
);
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of PHPUnit.
|
||||
*
|
||||
* (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 PHPUnit\Event\Telemetry;
|
||||
|
||||
use function gc_status;
|
||||
|
||||
/**
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
|
||||
*
|
||||
* @internal This class is not covered by the backward compatibility promise for PHPUnit
|
||||
*/
|
||||
final class Php83GarbageCollectorStatusProvider implements GarbageCollectorStatusProvider
|
||||
{
|
||||
public function status(): GarbageCollectorStatus
|
||||
{
|
||||
$status = gc_status();
|
||||
|
||||
return new GarbageCollectorStatus(
|
||||
$status['runs'],
|
||||
$status['collected'],
|
||||
$status['threshold'],
|
||||
$status['roots'],
|
||||
$status['application_time'],
|
||||
$status['collector_time'],
|
||||
$status['destructor_time'],
|
||||
$status['free_time'],
|
||||
$status['running'],
|
||||
$status['protected'],
|
||||
$status['full'],
|
||||
$status['buffer_size'],
|
||||
);
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of PHPUnit.
|
||||
*
|
||||
* (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 PHPUnit\Event\Telemetry;
|
||||
|
||||
/**
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
|
||||
*
|
||||
* @internal This interface is not covered by the backward compatibility promise for PHPUnit
|
||||
*/
|
||||
interface StopWatch
|
||||
{
|
||||
public function current(): HRTime;
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of PHPUnit.
|
||||
*
|
||||
* (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 PHPUnit\Event\Telemetry;
|
||||
|
||||
/**
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
|
||||
*
|
||||
* @internal This class is not covered by the backward compatibility promise for PHPUnit
|
||||
*/
|
||||
final class System
|
||||
{
|
||||
private readonly StopWatch $stopWatch;
|
||||
private readonly MemoryMeter $memoryMeter;
|
||||
private readonly GarbageCollectorStatusProvider $garbageCollectorStatusProvider;
|
||||
|
||||
public function __construct(StopWatch $stopWatch, MemoryMeter $memoryMeter, GarbageCollectorStatusProvider $garbageCollectorStatusProvider)
|
||||
{
|
||||
$this->stopWatch = $stopWatch;
|
||||
$this->memoryMeter = $memoryMeter;
|
||||
$this->garbageCollectorStatusProvider = $garbageCollectorStatusProvider;
|
||||
}
|
||||
|
||||
public function snapshot(): Snapshot
|
||||
{
|
||||
return new Snapshot(
|
||||
$this->stopWatch->current(),
|
||||
$this->memoryMeter->memoryUsage(),
|
||||
$this->memoryMeter->peakMemoryUsage(),
|
||||
$this->garbageCollectorStatusProvider->status(),
|
||||
);
|
||||
}
|
||||
}
|
||||
Vendored
+31
@@ -0,0 +1,31 @@
|
||||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of PHPUnit.
|
||||
*
|
||||
* (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 PHPUnit\Event\Telemetry;
|
||||
|
||||
use function memory_get_peak_usage;
|
||||
use function memory_get_usage;
|
||||
|
||||
/**
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
|
||||
*
|
||||
* @internal This class is not covered by the backward compatibility promise for PHPUnit
|
||||
*/
|
||||
final class SystemMemoryMeter implements MemoryMeter
|
||||
{
|
||||
public function memoryUsage(): MemoryUsage
|
||||
{
|
||||
return MemoryUsage::fromBytes(memory_get_usage(true));
|
||||
}
|
||||
|
||||
public function peakMemoryUsage(): MemoryUsage
|
||||
{
|
||||
return MemoryUsage::fromBytes(memory_get_peak_usage(true));
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of PHPUnit.
|
||||
*
|
||||
* (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 PHPUnit\Event\Telemetry;
|
||||
|
||||
use function hrtime;
|
||||
use PHPUnit\Event\InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
|
||||
*
|
||||
* @internal This class is not covered by the backward compatibility promise for PHPUnit
|
||||
*/
|
||||
final class SystemStopWatch implements StopWatch
|
||||
{
|
||||
/**
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function current(): HRTime
|
||||
{
|
||||
return HRTime::fromSecondsAndNanoseconds(...hrtime());
|
||||
}
|
||||
}
|
||||
docker/streamline-src/vendor/phpunit/phpunit/src/Event/Value/Telemetry/SystemStopWatchWithOffset.php
Vendored
+46
@@ -0,0 +1,46 @@
|
||||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of PHPUnit.
|
||||
*
|
||||
* (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 PHPUnit\Event\Telemetry;
|
||||
|
||||
use function hrtime;
|
||||
use PHPUnit\Event\InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
|
||||
*
|
||||
* @internal This class is not covered by the backward compatibility promise for PHPUnit
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
final class SystemStopWatchWithOffset implements StopWatch
|
||||
{
|
||||
private ?HRTime $offset;
|
||||
|
||||
public function __construct(HRTime $offset)
|
||||
{
|
||||
$this->offset = $offset;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function current(): HRTime
|
||||
{
|
||||
if ($this->offset !== null) {
|
||||
$offset = $this->offset;
|
||||
|
||||
$this->offset = null;
|
||||
|
||||
return $offset;
|
||||
}
|
||||
|
||||
return HRTime::fromSecondsAndNanoseconds(...hrtime());
|
||||
}
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of PHPUnit.
|
||||
*
|
||||
* (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 PHPUnit\Event\Code;
|
||||
|
||||
use PHPUnit\Event\TestData\MoreThanOneDataSetFromDataProviderException;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use PHPUnit\Logging\TestDox\NamePrettifier;
|
||||
|
||||
/**
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
|
||||
*
|
||||
* @internal This class is not covered by the backward compatibility promise for PHPUnit
|
||||
*/
|
||||
final class TestDoxBuilder
|
||||
{
|
||||
/**
|
||||
* @throws MoreThanOneDataSetFromDataProviderException
|
||||
*/
|
||||
public static function fromTestCase(TestCase $testCase): TestDox
|
||||
{
|
||||
$prettifier = new NamePrettifier;
|
||||
|
||||
return new TestDox(
|
||||
$prettifier->prettifyTestClassName($testCase::class),
|
||||
$prettifier->prettifyTestCase($testCase, false),
|
||||
$prettifier->prettifyTestCase($testCase, true),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-param class-string $className
|
||||
* @psalm-param non-empty-string $methodName
|
||||
*/
|
||||
public static function fromClassNameAndMethodName(string $className, string $methodName): TestDox
|
||||
{
|
||||
$prettifier = new NamePrettifier;
|
||||
|
||||
$prettifiedMethodName = $prettifier->prettifyTestMethodName($methodName);
|
||||
|
||||
return new TestDox(
|
||||
$prettifier->prettifyTestClassName($className),
|
||||
$prettifiedMethodName,
|
||||
$prettifiedMethodName,
|
||||
);
|
||||
}
|
||||
}
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of PHPUnit.
|
||||
*
|
||||
* (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 PHPUnit\Event\Code;
|
||||
|
||||
use const DEBUG_BACKTRACE_IGNORE_ARGS;
|
||||
use const DEBUG_BACKTRACE_PROVIDE_OBJECT;
|
||||
use function assert;
|
||||
use function debug_backtrace;
|
||||
use function is_numeric;
|
||||
use PHPUnit\Event\Facade as EventFacade;
|
||||
use PHPUnit\Event\TestData\DataFromDataProvider;
|
||||
use PHPUnit\Event\TestData\DataFromTestDependency;
|
||||
use PHPUnit\Event\TestData\MoreThanOneDataSetFromDataProviderException;
|
||||
use PHPUnit\Event\TestData\TestDataCollection;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use PHPUnit\Metadata\Parser\Registry as MetadataRegistry;
|
||||
use PHPUnit\Util\Exporter;
|
||||
use PHPUnit\Util\Reflection;
|
||||
|
||||
/**
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
|
||||
*
|
||||
* @internal This class is not covered by the backward compatibility promise for PHPUnit
|
||||
*/
|
||||
final class TestMethodBuilder
|
||||
{
|
||||
/**
|
||||
* @throws MoreThanOneDataSetFromDataProviderException
|
||||
*/
|
||||
public static function fromTestCase(TestCase $testCase): TestMethod
|
||||
{
|
||||
$methodName = $testCase->name();
|
||||
|
||||
assert(!empty($methodName));
|
||||
|
||||
$location = Reflection::sourceLocationFor($testCase::class, $methodName);
|
||||
|
||||
return new TestMethod(
|
||||
$testCase::class,
|
||||
$methodName,
|
||||
$location['file'],
|
||||
$location['line'],
|
||||
TestDoxBuilder::fromTestCase($testCase),
|
||||
MetadataRegistry::parser()->forClassAndMethod($testCase::class, $methodName),
|
||||
self::dataFor($testCase),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws NoTestCaseObjectOnCallStackException
|
||||
*/
|
||||
public static function fromCallStack(): TestMethod
|
||||
{
|
||||
foreach (debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT | DEBUG_BACKTRACE_IGNORE_ARGS) as $frame) {
|
||||
if (isset($frame['object']) && $frame['object'] instanceof TestCase) {
|
||||
return $frame['object']->valueObjectForEvents();
|
||||
}
|
||||
}
|
||||
|
||||
throw new NoTestCaseObjectOnCallStackException;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws MoreThanOneDataSetFromDataProviderException
|
||||
*/
|
||||
private static function dataFor(TestCase $testCase): TestDataCollection
|
||||
{
|
||||
$testData = [];
|
||||
|
||||
if ($testCase->usesDataProvider()) {
|
||||
$dataSetName = $testCase->dataName();
|
||||
|
||||
if (is_numeric($dataSetName)) {
|
||||
$dataSetName = (int) $dataSetName;
|
||||
}
|
||||
|
||||
$testData[] = DataFromDataProvider::from(
|
||||
$dataSetName,
|
||||
Exporter::export($testCase->providedData(), EventFacade::emitter()->exportsObjects()),
|
||||
$testCase->dataSetAsStringWithData(),
|
||||
);
|
||||
}
|
||||
|
||||
if ($testCase->hasDependencyInput()) {
|
||||
$testData[] = DataFromTestDependency::from(
|
||||
Exporter::export($testCase->dependencyInput(), EventFacade::emitter()->exportsObjects()),
|
||||
);
|
||||
}
|
||||
|
||||
return TestDataCollection::fromArray($testData);
|
||||
}
|
||||
}
|
||||
Vendored
+112
@@ -0,0 +1,112 @@
|
||||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of PHPUnit.
|
||||
*
|
||||
* (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 PHPUnit\Event\TestSuite;
|
||||
|
||||
use function explode;
|
||||
use PHPUnit\Event\Code\Test;
|
||||
use PHPUnit\Event\Code\TestCollection;
|
||||
use PHPUnit\Event\RuntimeException;
|
||||
use PHPUnit\Framework\DataProviderTestSuite;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use PHPUnit\Framework\TestSuite as FrameworkTestSuite;
|
||||
use PHPUnit\Runner\PhptTestCase;
|
||||
use ReflectionClass;
|
||||
use ReflectionException;
|
||||
use ReflectionMethod;
|
||||
|
||||
/**
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
|
||||
*
|
||||
* @internal This class is not covered by the backward compatibility promise for PHPUnit
|
||||
*/
|
||||
final class TestSuiteBuilder
|
||||
{
|
||||
/**
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public static function from(FrameworkTestSuite $testSuite): TestSuite
|
||||
{
|
||||
$tests = [];
|
||||
|
||||
self::process($testSuite, $tests);
|
||||
|
||||
if ($testSuite instanceof DataProviderTestSuite) {
|
||||
[$className, $methodName] = explode('::', $testSuite->name());
|
||||
|
||||
try {
|
||||
$reflector = new ReflectionMethod($className, $methodName);
|
||||
|
||||
return new TestSuiteForTestMethodWithDataProvider(
|
||||
$testSuite->name(),
|
||||
$testSuite->count(),
|
||||
TestCollection::fromArray($tests),
|
||||
$className,
|
||||
$methodName,
|
||||
$reflector->getFileName(),
|
||||
$reflector->getStartLine(),
|
||||
);
|
||||
// @codeCoverageIgnoreStart
|
||||
} catch (ReflectionException $e) {
|
||||
throw new RuntimeException(
|
||||
$e->getMessage(),
|
||||
$e->getCode(),
|
||||
$e,
|
||||
);
|
||||
}
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
|
||||
if ($testSuite->isForTestClass()) {
|
||||
try {
|
||||
$reflector = new ReflectionClass($testSuite->name());
|
||||
|
||||
return new TestSuiteForTestClass(
|
||||
$testSuite->name(),
|
||||
$testSuite->count(),
|
||||
TestCollection::fromArray($tests),
|
||||
$reflector->getFileName(),
|
||||
$reflector->getStartLine(),
|
||||
);
|
||||
// @codeCoverageIgnoreStart
|
||||
} catch (ReflectionException $e) {
|
||||
throw new RuntimeException(
|
||||
$e->getMessage(),
|
||||
$e->getCode(),
|
||||
$e,
|
||||
);
|
||||
}
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
|
||||
return new TestSuiteWithName(
|
||||
$testSuite->name(),
|
||||
$testSuite->count(),
|
||||
TestCollection::fromArray($tests),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-param list<Test> $tests
|
||||
*/
|
||||
private static function process(FrameworkTestSuite $testSuite, array &$tests): void
|
||||
{
|
||||
foreach ($testSuite->getIterator() as $test) {
|
||||
if ($test instanceof FrameworkTestSuite) {
|
||||
self::process($test, $tests);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($test instanceof TestCase || $test instanceof PhptTestCase) {
|
||||
$tests[] = $test->valueObjectForEvents();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of PHPUnit.
|
||||
*
|
||||
* (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 PHPUnit\Event\Code;
|
||||
|
||||
use PHPUnit\Event\NoPreviousThrowableException;
|
||||
use PHPUnit\Framework\Exception;
|
||||
use PHPUnit\Util\Filter;
|
||||
use PHPUnit\Util\ThrowableToStringMapper;
|
||||
|
||||
/**
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
|
||||
*
|
||||
* @internal This class is not covered by the backward compatibility promise for PHPUnit
|
||||
*/
|
||||
final class ThrowableBuilder
|
||||
{
|
||||
/**
|
||||
* @throws Exception
|
||||
* @throws NoPreviousThrowableException
|
||||
*/
|
||||
public static function from(\Throwable $t): Throwable
|
||||
{
|
||||
$previous = $t->getPrevious();
|
||||
|
||||
if ($previous !== null) {
|
||||
$previous = self::from($previous);
|
||||
}
|
||||
|
||||
return new Throwable(
|
||||
$t::class,
|
||||
$t->getMessage(),
|
||||
ThrowableToStringMapper::map($t),
|
||||
Filter::getFilteredStacktrace($t, false),
|
||||
$previous,
|
||||
);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
+2707
File diff suppressed because it is too large
Load Diff
+73
@@ -0,0 +1,73 @@
|
||||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of PHPUnit.
|
||||
*
|
||||
* (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 PHPUnit\Framework\Constraint;
|
||||
|
||||
use Closure;
|
||||
use ReflectionFunction;
|
||||
|
||||
/**
|
||||
* @psalm-template CallbackInput of mixed
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
|
||||
*/
|
||||
final class Callback extends Constraint
|
||||
{
|
||||
/**
|
||||
* @psalm-var callable(CallbackInput $input): bool
|
||||
*/
|
||||
private readonly mixed $callback;
|
||||
|
||||
/**
|
||||
* @psalm-param callable(CallbackInput $input): bool $callback
|
||||
*/
|
||||
public function __construct(callable $callback)
|
||||
{
|
||||
$this->callback = $callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string representation of the constraint.
|
||||
*/
|
||||
public function toString(): string
|
||||
{
|
||||
return 'is accepted by specified callback';
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-suppress ArgumentTypeCoercion
|
||||
*/
|
||||
public function isVariadic(): bool
|
||||
{
|
||||
foreach ((new ReflectionFunction(Closure::fromCallable($this->callback)))->getParameters() as $parameter) {
|
||||
if ($parameter->isVariadic()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates the constraint for parameter $value. Returns true if the
|
||||
* constraint is met, false otherwise.
|
||||
*
|
||||
* @psalm-param CallbackInput $other
|
||||
*
|
||||
* @psalm-suppress InvalidArgument
|
||||
*/
|
||||
protected function matches(mixed $other): bool
|
||||
{
|
||||
if ($this->isVariadic()) {
|
||||
return ($this->callback)(...$other);
|
||||
}
|
||||
|
||||
return ($this->callback)($other);
|
||||
}
|
||||
}
|
||||
+264
@@ -0,0 +1,264 @@
|
||||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of PHPUnit.
|
||||
*
|
||||
* (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 PHPUnit\Framework\Constraint;
|
||||
|
||||
use function gettype;
|
||||
use function sprintf;
|
||||
use function strtolower;
|
||||
use Countable;
|
||||
use PHPUnit\Framework\ExpectationFailedException;
|
||||
use PHPUnit\Framework\SelfDescribing;
|
||||
use PHPUnit\Util\Exporter;
|
||||
use SebastianBergmann\Comparator\ComparisonFailure;
|
||||
|
||||
/**
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
|
||||
*/
|
||||
abstract class Constraint implements Countable, SelfDescribing
|
||||
{
|
||||
/**
|
||||
* Evaluates the constraint for parameter $other.
|
||||
*
|
||||
* If $returnResult is set to false (the default), an exception is thrown
|
||||
* in case of a failure. null is returned otherwise.
|
||||
*
|
||||
* If $returnResult is true, the result of the evaluation is returned as
|
||||
* a boolean value instead: true in case of success, false in case of a
|
||||
* failure.
|
||||
*
|
||||
* @throws ExpectationFailedException
|
||||
*/
|
||||
public function evaluate(mixed $other, string $description = '', bool $returnResult = false): ?bool
|
||||
{
|
||||
$success = false;
|
||||
|
||||
if ($this->matches($other)) {
|
||||
$success = true;
|
||||
}
|
||||
|
||||
if ($returnResult) {
|
||||
return $success;
|
||||
}
|
||||
|
||||
if (!$success) {
|
||||
$this->fail($other, $description);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts the number of constraint elements.
|
||||
*/
|
||||
public function count(): int
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
protected function exporter(): \SebastianBergmann\Exporter\Exporter
|
||||
{
|
||||
return new \SebastianBergmann\Exporter\Exporter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates the constraint for parameter $other. Returns true if the
|
||||
* constraint is met, false otherwise.
|
||||
*
|
||||
* This method can be overridden to implement the evaluation algorithm.
|
||||
*/
|
||||
protected function matches(mixed $other): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws an exception for the given compared value and test description.
|
||||
*
|
||||
* @throws ExpectationFailedException
|
||||
*/
|
||||
protected function fail(mixed $other, string $description, ?ComparisonFailure $comparisonFailure = null): never
|
||||
{
|
||||
$failureDescription = sprintf(
|
||||
'Failed asserting that %s.',
|
||||
$this->failureDescription($other),
|
||||
);
|
||||
|
||||
$additionalFailureDescription = $this->additionalFailureDescription($other);
|
||||
|
||||
if ($additionalFailureDescription) {
|
||||
$failureDescription .= "\n" . $additionalFailureDescription;
|
||||
}
|
||||
|
||||
if (!empty($description)) {
|
||||
$failureDescription = $description . "\n" . $failureDescription;
|
||||
}
|
||||
|
||||
throw new ExpectationFailedException(
|
||||
$failureDescription,
|
||||
$comparisonFailure,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return additional failure description where needed.
|
||||
*
|
||||
* The function can be overridden to provide additional failure
|
||||
* information like a diff
|
||||
*/
|
||||
protected function additionalFailureDescription(mixed $other): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the description of the failure.
|
||||
*
|
||||
* The beginning of failure messages is "Failed asserting that" in most
|
||||
* cases. This method should return the second part of that sentence.
|
||||
*
|
||||
* To provide additional failure information additionalFailureDescription
|
||||
* can be used.
|
||||
*/
|
||||
protected function failureDescription(mixed $other): string
|
||||
{
|
||||
return Exporter::export($other, true) . ' ' . $this->toString(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a custom string representation of the constraint object when it
|
||||
* appears in context of an $operator expression.
|
||||
*
|
||||
* The purpose of this method is to provide meaningful descriptive string
|
||||
* in context of operators such as LogicalNot. Native PHPUnit constraints
|
||||
* are supported out of the box by LogicalNot, but externally developed
|
||||
* ones had no way to provide correct strings in this context.
|
||||
*
|
||||
* The method shall return empty string, when it does not handle
|
||||
* customization by itself.
|
||||
*/
|
||||
protected function toStringInContext(Operator $operator, mixed $role): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the description of the failure when this constraint appears in
|
||||
* context of an $operator expression.
|
||||
*
|
||||
* The purpose of this method is to provide meaningful failure description
|
||||
* in context of operators such as LogicalNot. Native PHPUnit constraints
|
||||
* are supported out of the box by LogicalNot, but externally developed
|
||||
* ones had no way to provide correct messages in this context.
|
||||
*
|
||||
* The method shall return empty string, when it does not handle
|
||||
* customization by itself.
|
||||
*/
|
||||
protected function failureDescriptionInContext(Operator $operator, mixed $role, mixed $other): string
|
||||
{
|
||||
$string = $this->toStringInContext($operator, $role);
|
||||
|
||||
if ($string === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
return Exporter::export($other, true) . ' ' . $string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reduces the sub-expression starting at $this by skipping degenerate
|
||||
* sub-expression and returns first descendant constraint that starts
|
||||
* a non-reducible sub-expression.
|
||||
*
|
||||
* Returns $this for terminal constraints and for operators that start
|
||||
* non-reducible sub-expression, or the nearest descendant of $this that
|
||||
* starts a non-reducible sub-expression.
|
||||
*
|
||||
* A constraint expression may be modelled as a tree with non-terminal
|
||||
* nodes (operators) and terminal nodes. For example:
|
||||
*
|
||||
* LogicalOr (operator, non-terminal)
|
||||
* + LogicalAnd (operator, non-terminal)
|
||||
* | + IsType('int') (terminal)
|
||||
* | + GreaterThan(10) (terminal)
|
||||
* + LogicalNot (operator, non-terminal)
|
||||
* + IsType('array') (terminal)
|
||||
*
|
||||
* A degenerate sub-expression is a part of the tree, that effectively does
|
||||
* not contribute to the evaluation of the expression it appears in. An example
|
||||
* of degenerate sub-expression is a BinaryOperator constructed with single
|
||||
* operand or nested BinaryOperators, each with single operand. An
|
||||
* expression involving a degenerate sub-expression is equivalent to a
|
||||
* reduced expression with the degenerate sub-expression removed, for example
|
||||
*
|
||||
* LogicalAnd (operator)
|
||||
* + LogicalOr (degenerate operator)
|
||||
* | + LogicalAnd (degenerate operator)
|
||||
* | + IsType('int') (terminal)
|
||||
* + GreaterThan(10) (terminal)
|
||||
*
|
||||
* is equivalent to
|
||||
*
|
||||
* LogicalAnd (operator)
|
||||
* + IsType('int') (terminal)
|
||||
* + GreaterThan(10) (terminal)
|
||||
*
|
||||
* because the subexpression
|
||||
*
|
||||
* + LogicalOr
|
||||
* + LogicalAnd
|
||||
* + -
|
||||
*
|
||||
* is degenerate. Calling reduce() on the LogicalOr object above, as well
|
||||
* as on LogicalAnd, shall return the IsType('int') instance.
|
||||
*
|
||||
* Other specific reductions can be implemented, for example cascade of
|
||||
* LogicalNot operators
|
||||
*
|
||||
* + LogicalNot
|
||||
* + LogicalNot
|
||||
* +LogicalNot
|
||||
* + IsTrue
|
||||
*
|
||||
* can be reduced to
|
||||
*
|
||||
* LogicalNot
|
||||
* + IsTrue
|
||||
*/
|
||||
protected function reduce(): self
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-return non-empty-string
|
||||
*/
|
||||
protected function valueToTypeStringFragment(mixed $value): string
|
||||
{
|
||||
$type = strtolower(gettype($value));
|
||||
|
||||
if ($type === 'double') {
|
||||
$type = 'float';
|
||||
}
|
||||
|
||||
if ($type === 'resource (closed)') {
|
||||
$type = 'closed resource';
|
||||
}
|
||||
|
||||
return match ($type) {
|
||||
'array', 'integer', 'object' => 'an ' . $type . ' ',
|
||||
'boolean', 'closed resource', 'float', 'resource', 'string' => 'a ' . $type . ' ',
|
||||
'null' => 'null ',
|
||||
default => 'a value of ' . $type . ' ',
|
||||
};
|
||||
}
|
||||
}
|
||||
Vendored
+81
@@ -0,0 +1,81 @@
|
||||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of PHPUnit.
|
||||
*
|
||||
* (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 PHPUnit\Framework\Constraint;
|
||||
|
||||
use function sprintf;
|
||||
use PHPUnit\Util\Filter;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
|
||||
*
|
||||
* @internal This class is not covered by the backward compatibility promise for PHPUnit
|
||||
*/
|
||||
final class Exception extends Constraint
|
||||
{
|
||||
private readonly string $className;
|
||||
|
||||
public function __construct(string $className)
|
||||
{
|
||||
$this->className = $className;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string representation of the constraint.
|
||||
*/
|
||||
public function toString(): string
|
||||
{
|
||||
return sprintf(
|
||||
'exception of type "%s"',
|
||||
$this->className,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates the constraint for parameter $other. Returns true if the
|
||||
* constraint is met, false otherwise.
|
||||
*/
|
||||
protected function matches(mixed $other): bool
|
||||
{
|
||||
return $other instanceof $this->className;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the description of the failure.
|
||||
*
|
||||
* The beginning of failure messages is "Failed asserting that" in most
|
||||
* cases. This method should return the second part of that sentence.
|
||||
*
|
||||
* @throws \PHPUnit\Framework\Exception
|
||||
*/
|
||||
protected function failureDescription(mixed $other): string
|
||||
{
|
||||
if ($other === null) {
|
||||
return sprintf(
|
||||
'exception of type "%s" is thrown',
|
||||
$this->className,
|
||||
);
|
||||
}
|
||||
|
||||
$message = '';
|
||||
|
||||
if ($other instanceof Throwable) {
|
||||
$message = '. Message was: "' . $other->getMessage() . '" at'
|
||||
. "\n" . Filter::getFilteredStacktrace($other);
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
'exception of type "%s" matches expected exception "%s"%s',
|
||||
$other::class,
|
||||
$this->className,
|
||||
$message,
|
||||
);
|
||||
}
|
||||
}
|
||||
Vendored
+57
@@ -0,0 +1,57 @@
|
||||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of PHPUnit.
|
||||
*
|
||||
* (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 PHPUnit\Framework\Constraint;
|
||||
|
||||
use function sprintf;
|
||||
use PHPUnit\Util\Exporter;
|
||||
|
||||
/**
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
|
||||
*
|
||||
* @internal This class is not covered by the backward compatibility promise for PHPUnit
|
||||
*/
|
||||
final class ExceptionCode extends Constraint
|
||||
{
|
||||
private readonly int|string $expectedCode;
|
||||
|
||||
public function __construct(int|string $expected)
|
||||
{
|
||||
$this->expectedCode = $expected;
|
||||
}
|
||||
|
||||
public function toString(): string
|
||||
{
|
||||
return 'exception code is ' . $this->expectedCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates the constraint for parameter $other. Returns true if the
|
||||
* constraint is met, false otherwise.
|
||||
*/
|
||||
protected function matches(mixed $other): bool
|
||||
{
|
||||
return (string) $other === (string) $this->expectedCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the description of the failure.
|
||||
*
|
||||
* The beginning of failure messages is "Failed asserting that" in most
|
||||
* cases. This method should return the second part of that sentence.
|
||||
*/
|
||||
protected function failureDescription(mixed $other): string
|
||||
{
|
||||
return sprintf(
|
||||
'%s is equal to expected exception code %s',
|
||||
Exporter::export($other, true),
|
||||
Exporter::export($this->expectedCode, true),
|
||||
);
|
||||
}
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of PHPUnit.
|
||||
*
|
||||
* (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 PHPUnit\Framework\Constraint;
|
||||
|
||||
use function sprintf;
|
||||
use function str_contains;
|
||||
use PHPUnit\Util\Exporter;
|
||||
|
||||
/**
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
|
||||
*
|
||||
* @internal This class is not covered by the backward compatibility promise for PHPUnit
|
||||
*/
|
||||
final class ExceptionMessageIsOrContains extends Constraint
|
||||
{
|
||||
private readonly string $expectedMessage;
|
||||
|
||||
public function __construct(string $expectedMessage)
|
||||
{
|
||||
$this->expectedMessage = $expectedMessage;
|
||||
}
|
||||
|
||||
public function toString(): string
|
||||
{
|
||||
if ($this->expectedMessage === '') {
|
||||
return 'exception message is empty';
|
||||
}
|
||||
|
||||
return 'exception message contains ' . Exporter::export($this->expectedMessage);
|
||||
}
|
||||
|
||||
protected function matches(mixed $other): bool
|
||||
{
|
||||
if ($this->expectedMessage === '') {
|
||||
return $other === '';
|
||||
}
|
||||
|
||||
return str_contains((string) $other, $this->expectedMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the description of the failure.
|
||||
*
|
||||
* The beginning of failure messages is "Failed asserting that" in most
|
||||
* cases. This method should return the second part of that sentence.
|
||||
*/
|
||||
protected function failureDescription(mixed $other): string
|
||||
{
|
||||
if ($this->expectedMessage === '') {
|
||||
return sprintf(
|
||||
"exception message is empty but is '%s'",
|
||||
$other,
|
||||
);
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
"exception message '%s' contains '%s'",
|
||||
$other,
|
||||
$this->expectedMessage,
|
||||
);
|
||||
}
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of PHPUnit.
|
||||
*
|
||||
* (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 PHPUnit\Framework\Constraint;
|
||||
|
||||
use function preg_match;
|
||||
use function sprintf;
|
||||
use Exception;
|
||||
use PHPUnit\Util\Exporter;
|
||||
|
||||
/**
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
|
||||
*
|
||||
* @internal This class is not covered by the backward compatibility promise for PHPUnit
|
||||
*/
|
||||
final class ExceptionMessageMatchesRegularExpression extends Constraint
|
||||
{
|
||||
private readonly string $regularExpression;
|
||||
|
||||
public function __construct(string $regularExpression)
|
||||
{
|
||||
$this->regularExpression = $regularExpression;
|
||||
}
|
||||
|
||||
public function toString(): string
|
||||
{
|
||||
return 'exception message matches ' . Exporter::export($this->regularExpression);
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates the constraint for parameter $other. Returns true if the
|
||||
* constraint is met, false otherwise.
|
||||
*
|
||||
* @throws \PHPUnit\Framework\Exception
|
||||
* @throws Exception
|
||||
*/
|
||||
protected function matches(mixed $other): bool
|
||||
{
|
||||
$match = @preg_match($this->regularExpression, (string) $other);
|
||||
|
||||
if ($match === false) {
|
||||
throw new \PHPUnit\Framework\Exception(
|
||||
sprintf(
|
||||
'Invalid expected exception message regular expression given: %s',
|
||||
$this->regularExpression,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return $match === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the description of the failure.
|
||||
*
|
||||
* The beginning of failure messages is "Failed asserting that" in most
|
||||
* cases. This method should return the second part of that sentence.
|
||||
*/
|
||||
protected function failureDescription(mixed $other): string
|
||||
{
|
||||
return sprintf(
|
||||
"exception message '%s' matches '%s'",
|
||||
$other,
|
||||
$this->regularExpression,
|
||||
);
|
||||
}
|
||||
}
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of PHPUnit.
|
||||
*
|
||||
* (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 PHPUnit\Framework\Constraint;
|
||||
|
||||
use function json_decode;
|
||||
use function sprintf;
|
||||
use PHPUnit\Framework\ExpectationFailedException;
|
||||
use PHPUnit\Util\InvalidJsonException;
|
||||
use PHPUnit\Util\Json;
|
||||
use SebastianBergmann\Comparator\ComparisonFailure;
|
||||
|
||||
/**
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
|
||||
*/
|
||||
final class JsonMatches extends Constraint
|
||||
{
|
||||
private readonly string $value;
|
||||
|
||||
public function __construct(string $value)
|
||||
{
|
||||
$this->value = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string representation of the object.
|
||||
*/
|
||||
public function toString(): string
|
||||
{
|
||||
return sprintf(
|
||||
'matches JSON string "%s"',
|
||||
$this->value,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates the constraint for parameter $other. Returns true if the
|
||||
* constraint is met, false otherwise.
|
||||
*
|
||||
* This method can be overridden to implement the evaluation algorithm.
|
||||
*/
|
||||
protected function matches(mixed $other): bool
|
||||
{
|
||||
[$error, $recodedOther] = Json::canonicalize($other);
|
||||
|
||||
if ($error) {
|
||||
return false;
|
||||
}
|
||||
|
||||
[$error, $recodedValue] = Json::canonicalize($this->value);
|
||||
|
||||
if ($error) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $recodedOther == $recodedValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws an exception for the given compared value and test description.
|
||||
*
|
||||
* @throws ExpectationFailedException
|
||||
* @throws InvalidJsonException
|
||||
*/
|
||||
protected function fail(mixed $other, string $description, ?ComparisonFailure $comparisonFailure = null): never
|
||||
{
|
||||
if ($comparisonFailure === null) {
|
||||
[$error, $recodedOther] = Json::canonicalize($other);
|
||||
|
||||
if ($error) {
|
||||
parent::fail($other, $description);
|
||||
}
|
||||
|
||||
[$error, $recodedValue] = Json::canonicalize($this->value);
|
||||
|
||||
if ($error) {
|
||||
parent::fail($other, $description);
|
||||
}
|
||||
|
||||
$comparisonFailure = new ComparisonFailure(
|
||||
json_decode($this->value),
|
||||
json_decode($other),
|
||||
Json::prettify($recodedValue),
|
||||
Json::prettify($recodedOther),
|
||||
'Failed asserting that two json values are equal.',
|
||||
);
|
||||
}
|
||||
|
||||
parent::fail($other, $description, $comparisonFailure);
|
||||
}
|
||||
}
|
||||
Vendored
+128
@@ -0,0 +1,128 @@
|
||||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of PHPUnit.
|
||||
*
|
||||
* (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 PHPUnit\Framework\Constraint;
|
||||
|
||||
use function count;
|
||||
|
||||
/**
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
|
||||
*/
|
||||
abstract class UnaryOperator extends Operator
|
||||
{
|
||||
private readonly Constraint $constraint;
|
||||
|
||||
public function __construct(mixed $constraint)
|
||||
{
|
||||
$this->constraint = $this->checkConstraint($constraint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of operands (constraints).
|
||||
*/
|
||||
public function arity(): int
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string representation of the constraint.
|
||||
*/
|
||||
public function toString(): string
|
||||
{
|
||||
$reduced = $this->reduce();
|
||||
|
||||
if ($reduced !== $this) {
|
||||
return $reduced->toString();
|
||||
}
|
||||
|
||||
$constraint = $this->constraint->reduce();
|
||||
|
||||
if ($this->constraintNeedsParentheses($constraint)) {
|
||||
return $this->operator() . '( ' . $constraint->toString() . ' )';
|
||||
}
|
||||
|
||||
$string = $constraint->toStringInContext($this, 0);
|
||||
|
||||
if ($string === '') {
|
||||
return $this->transformString($constraint->toString());
|
||||
}
|
||||
|
||||
return $string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts the number of constraint elements.
|
||||
*/
|
||||
public function count(): int
|
||||
{
|
||||
return count($this->constraint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the description of the failure.
|
||||
*
|
||||
* The beginning of failure messages is "Failed asserting that" in most
|
||||
* cases. This method should return the second part of that sentence.
|
||||
*/
|
||||
protected function failureDescription(mixed $other): string
|
||||
{
|
||||
$reduced = $this->reduce();
|
||||
|
||||
if ($reduced !== $this) {
|
||||
return $reduced->failureDescription($other);
|
||||
}
|
||||
|
||||
$constraint = $this->constraint->reduce();
|
||||
|
||||
if ($this->constraintNeedsParentheses($constraint)) {
|
||||
return $this->operator() . '( ' . $constraint->failureDescription($other) . ' )';
|
||||
}
|
||||
|
||||
$string = $constraint->failureDescriptionInContext($this, 0, $other);
|
||||
|
||||
if ($string === '') {
|
||||
return $this->transformString($constraint->failureDescription($other));
|
||||
}
|
||||
|
||||
return $string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms string returned by the member constraint's toString() or
|
||||
* failureDescription() such that it reflects constraint's participation in
|
||||
* this expression.
|
||||
*
|
||||
* The method may be overwritten in a subclass to apply default
|
||||
* transformation in case the operand constraint does not provide its own
|
||||
* custom strings via toStringInContext() or failureDescriptionInContext().
|
||||
*/
|
||||
protected function transformString(string $string): string
|
||||
{
|
||||
return $string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides access to $this->constraint for subclasses.
|
||||
*/
|
||||
final protected function constraint(): Constraint
|
||||
{
|
||||
return $this->constraint;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the $constraint needs to be wrapped with parentheses.
|
||||
*/
|
||||
protected function constraintNeedsParentheses(Constraint $constraint): bool
|
||||
{
|
||||
$constraint = $constraint->reduce();
|
||||
|
||||
return $constraint instanceof self || parent::constraintNeedsParentheses($constraint);
|
||||
}
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of PHPUnit.
|
||||
*
|
||||
* (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 PHPUnit\Framework\Constraint;
|
||||
|
||||
use const JSON_ERROR_CTRL_CHAR;
|
||||
use const JSON_ERROR_DEPTH;
|
||||
use const JSON_ERROR_NONE;
|
||||
use const JSON_ERROR_STATE_MISMATCH;
|
||||
use const JSON_ERROR_SYNTAX;
|
||||
use const JSON_ERROR_UTF8;
|
||||
use function is_string;
|
||||
use function json_decode;
|
||||
use function json_last_error;
|
||||
use function sprintf;
|
||||
|
||||
/**
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
|
||||
*/
|
||||
final class IsJson extends Constraint
|
||||
{
|
||||
/**
|
||||
* Returns a string representation of the constraint.
|
||||
*/
|
||||
public function toString(): string
|
||||
{
|
||||
return 'is valid JSON';
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates the constraint for parameter $other. Returns true if the
|
||||
* constraint is met, false otherwise.
|
||||
*/
|
||||
protected function matches(mixed $other): bool
|
||||
{
|
||||
if (!is_string($other) || $other === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
json_decode($other);
|
||||
|
||||
if (json_last_error()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the description of the failure.
|
||||
*
|
||||
* The beginning of failure messages is "Failed asserting that" in most
|
||||
* cases. This method should return the second part of that sentence.
|
||||
*/
|
||||
protected function failureDescription(mixed $other): string
|
||||
{
|
||||
if (!is_string($other)) {
|
||||
return $this->valueToTypeStringFragment($other) . 'is valid JSON';
|
||||
}
|
||||
|
||||
if ($other === '') {
|
||||
return 'an empty string is valid JSON';
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
'a string is valid JSON (%s)',
|
||||
$this->determineJsonError($other),
|
||||
);
|
||||
}
|
||||
|
||||
private function determineJsonError(string $json): string
|
||||
{
|
||||
json_decode($json);
|
||||
|
||||
return match (json_last_error()) {
|
||||
JSON_ERROR_NONE => '',
|
||||
JSON_ERROR_DEPTH => 'Maximum stack depth exceeded',
|
||||
JSON_ERROR_STATE_MISMATCH => 'Underflow or the modes mismatch',
|
||||
JSON_ERROR_CTRL_CHAR => 'Unexpected control character found',
|
||||
JSON_ERROR_SYNTAX => 'Syntax error, malformed JSON',
|
||||
JSON_ERROR_UTF8 => 'Malformed UTF-8 characters, possibly incorrectly encoded',
|
||||
default => 'Unknown error',
|
||||
};
|
||||
}
|
||||
}
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of PHPUnit.
|
||||
*
|
||||
* (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 PHPUnit\Framework\Constraint;
|
||||
|
||||
use const DIRECTORY_SEPARATOR;
|
||||
use const PHP_EOL;
|
||||
use function explode;
|
||||
use function implode;
|
||||
use function preg_match;
|
||||
use function preg_quote;
|
||||
use function preg_replace;
|
||||
use function strtr;
|
||||
use SebastianBergmann\Diff\Differ;
|
||||
use SebastianBergmann\Diff\Output\UnifiedDiffOutputBuilder;
|
||||
|
||||
/**
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
|
||||
*/
|
||||
final class StringMatchesFormatDescription extends Constraint
|
||||
{
|
||||
private readonly string $formatDescription;
|
||||
|
||||
public function __construct(string $formatDescription)
|
||||
{
|
||||
$this->formatDescription = $formatDescription;
|
||||
}
|
||||
|
||||
public function toString(): string
|
||||
{
|
||||
return 'matches format description:' . PHP_EOL . $this->formatDescription;
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates the constraint for parameter $other. Returns true if the
|
||||
* constraint is met, false otherwise.
|
||||
*/
|
||||
protected function matches(mixed $other): bool
|
||||
{
|
||||
$other = $this->convertNewlines($other);
|
||||
|
||||
$matches = preg_match(
|
||||
$this->regularExpressionForFormatDescription(
|
||||
$this->convertNewlines($this->formatDescription),
|
||||
),
|
||||
$other,
|
||||
);
|
||||
|
||||
return $matches > 0;
|
||||
}
|
||||
|
||||
protected function failureDescription(mixed $other): string
|
||||
{
|
||||
return 'string matches format description';
|
||||
}
|
||||
|
||||
protected function additionalFailureDescription(mixed $other): string
|
||||
{
|
||||
$from = explode("\n", $this->formatDescription);
|
||||
$to = explode("\n", $this->convertNewlines($other));
|
||||
|
||||
foreach ($from as $index => $line) {
|
||||
if (isset($to[$index]) && $line !== $to[$index]) {
|
||||
$line = $this->regularExpressionForFormatDescription($line);
|
||||
|
||||
if (preg_match($line, $to[$index]) > 0) {
|
||||
$from[$index] = $to[$index];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$from = implode("\n", $from);
|
||||
$to = implode("\n", $to);
|
||||
|
||||
return $this->differ()->diff($from, $to);
|
||||
}
|
||||
|
||||
private function regularExpressionForFormatDescription(string $string): string
|
||||
{
|
||||
$string = strtr(
|
||||
preg_quote($string, '/'),
|
||||
[
|
||||
'%%' => '%',
|
||||
'%e' => preg_quote(DIRECTORY_SEPARATOR, '/'),
|
||||
'%s' => '[^\r\n]+',
|
||||
'%S' => '[^\r\n]*',
|
||||
'%a' => '.+?',
|
||||
'%A' => '.*?',
|
||||
'%w' => '\s*',
|
||||
'%i' => '[+-]?\d+',
|
||||
'%d' => '\d+',
|
||||
'%x' => '[0-9a-fA-F]+',
|
||||
'%f' => '[+-]?(?:\d+|(?=\.\d))(?:\.\d+)?(?:[Ee][+-]?\d+)?',
|
||||
'%c' => '.',
|
||||
'%0' => '\x00',
|
||||
],
|
||||
);
|
||||
|
||||
return '/^' . $string . '$/s';
|
||||
}
|
||||
|
||||
private function convertNewlines(string $text): string
|
||||
{
|
||||
return preg_replace('/\r\n/', "\n", $text);
|
||||
}
|
||||
|
||||
private function differ(): Differ
|
||||
{
|
||||
return new Differ(new UnifiedDiffOutputBuilder("--- Expected\n+++ Actual\n"));
|
||||
}
|
||||
}
|
||||
Vendored
+64
@@ -0,0 +1,64 @@
|
||||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of PHPUnit.
|
||||
*
|
||||
* (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 PHPUnit\Framework\Constraint;
|
||||
|
||||
use function array_key_exists;
|
||||
use function is_array;
|
||||
use ArrayAccess;
|
||||
use PHPUnit\Util\Exporter;
|
||||
|
||||
/**
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
|
||||
*/
|
||||
final class ArrayHasKey extends Constraint
|
||||
{
|
||||
private readonly mixed $key;
|
||||
|
||||
public function __construct(mixed $key)
|
||||
{
|
||||
$this->key = $key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string representation of the constraint.
|
||||
*/
|
||||
public function toString(): string
|
||||
{
|
||||
return 'has the key ' . Exporter::export($this->key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates the constraint for parameter $other. Returns true if the
|
||||
* constraint is met, false otherwise.
|
||||
*/
|
||||
protected function matches(mixed $other): bool
|
||||
{
|
||||
if (is_array($other)) {
|
||||
return array_key_exists($this->key, $other);
|
||||
}
|
||||
|
||||
if ($other instanceof ArrayAccess) {
|
||||
return $other->offsetExists($this->key);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the description of the failure.
|
||||
*
|
||||
* The beginning of failure messages is "Failed asserting that" in most
|
||||
* cases. This method should return the second part of that sentence.
|
||||
*/
|
||||
protected function failureDescription(mixed $other): string
|
||||
{
|
||||
return 'an array ' . $this->toString(true);
|
||||
}
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of PHPUnit.
|
||||
*
|
||||
* (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 PHPUnit\Framework\Constraint;
|
||||
|
||||
use PHPUnit\Framework\Exception;
|
||||
use PHPUnit\Framework\ExpectationFailedException;
|
||||
|
||||
/**
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
|
||||
*/
|
||||
final class TraversableContainsOnly extends Constraint
|
||||
{
|
||||
private Constraint $constraint;
|
||||
private readonly string $type;
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function __construct(string $type, bool $isNativeType = true)
|
||||
{
|
||||
if ($isNativeType) {
|
||||
$this->constraint = new IsType($type);
|
||||
} else {
|
||||
$this->constraint = new IsInstanceOf($type);
|
||||
}
|
||||
|
||||
$this->type = $type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates the constraint for parameter $other.
|
||||
*
|
||||
* If $returnResult is set to false (the default), an exception is thrown
|
||||
* in case of a failure. null is returned otherwise.
|
||||
*
|
||||
* If $returnResult is true, the result of the evaluation is returned as
|
||||
* a boolean value instead: true in case of success, false in case of a
|
||||
* failure.
|
||||
*
|
||||
* @throws ExpectationFailedException
|
||||
*/
|
||||
public function evaluate(mixed $other, string $description = '', bool $returnResult = false): bool
|
||||
{
|
||||
$success = true;
|
||||
|
||||
foreach ($other as $item) {
|
||||
if (!$this->constraint->evaluate($item, '', true)) {
|
||||
$success = false;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$success && !$returnResult) {
|
||||
$this->fail($other, $description);
|
||||
}
|
||||
|
||||
return $success;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string representation of the constraint.
|
||||
*/
|
||||
public function toString(): string
|
||||
{
|
||||
return 'contains only values of type "' . $this->type . '"';
|
||||
}
|
||||
}
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of PHPUnit.
|
||||
*
|
||||
* (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 PHPUnit\Framework;
|
||||
|
||||
use function explode;
|
||||
use PHPUnit\Framework\TestSize\TestSize;
|
||||
use PHPUnit\Metadata\Api\Groups;
|
||||
|
||||
/**
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
|
||||
*
|
||||
* @internal This class is not covered by the backward compatibility promise for PHPUnit
|
||||
*/
|
||||
final class DataProviderTestSuite extends TestSuite
|
||||
{
|
||||
/**
|
||||
* @psalm-var list<ExecutionOrderDependency>
|
||||
*/
|
||||
private array $dependencies = [];
|
||||
private ?array $providedTests = null;
|
||||
|
||||
/**
|
||||
* @psalm-param list<ExecutionOrderDependency> $dependencies
|
||||
*/
|
||||
public function setDependencies(array $dependencies): void
|
||||
{
|
||||
$this->dependencies = $dependencies;
|
||||
|
||||
foreach ($this->tests() as $test) {
|
||||
if (!$test instanceof TestCase) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$test->setDependencies($dependencies);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-return list<ExecutionOrderDependency>
|
||||
*/
|
||||
public function provides(): array
|
||||
{
|
||||
if ($this->providedTests === null) {
|
||||
$this->providedTests = [new ExecutionOrderDependency($this->name())];
|
||||
}
|
||||
|
||||
return $this->providedTests;
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-return list<ExecutionOrderDependency>
|
||||
*/
|
||||
public function requires(): array
|
||||
{
|
||||
// A DataProviderTestSuite does not have to traverse its child tests
|
||||
// as these are inherited and cannot reference dataProvider rows directly
|
||||
return $this->dependencies;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the size of each test created using the data provider(s).
|
||||
*/
|
||||
public function size(): TestSize
|
||||
{
|
||||
[$className, $methodName] = explode('::', $this->name());
|
||||
|
||||
return (new Groups)->size($className, $methodName);
|
||||
}
|
||||
}
|
||||
Vendored
+26
@@ -0,0 +1,26 @@
|
||||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of PHPUnit.
|
||||
*
|
||||
* (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 PHPUnit\Framework;
|
||||
|
||||
/**
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
|
||||
*
|
||||
* @internal This class is not covered by the backward compatibility promise for PHPUnit
|
||||
*/
|
||||
class AssertionFailedError extends Exception implements SelfDescribing
|
||||
{
|
||||
/**
|
||||
* Wrapper for getMessage() which is declared as final.
|
||||
*/
|
||||
public function toString(): string
|
||||
{
|
||||
return $this->getMessage();
|
||||
}
|
||||
}
|
||||
Vendored
+19
@@ -0,0 +1,19 @@
|
||||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of PHPUnit.
|
||||
*
|
||||
* (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 PHPUnit\Framework;
|
||||
|
||||
/**
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
|
||||
*
|
||||
* @internal This class is not covered by the backward compatibility promise for PHPUnit
|
||||
*/
|
||||
class CodeCoverageException extends Exception
|
||||
{
|
||||
}
|
||||
Vendored
+19
@@ -0,0 +1,19 @@
|
||||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of PHPUnit.
|
||||
*
|
||||
* (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 PHPUnit\Framework;
|
||||
|
||||
/**
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
|
||||
*
|
||||
* @internal This class is not covered by the backward compatibility promise for PHPUnit
|
||||
*/
|
||||
final class EmptyStringException extends InvalidArgumentException
|
||||
{
|
||||
}
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of PHPUnit.
|
||||
*
|
||||
* (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 PHPUnit\Framework;
|
||||
|
||||
use function array_keys;
|
||||
use function get_object_vars;
|
||||
use function is_int;
|
||||
use function sprintf;
|
||||
use RuntimeException;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Base class for all PHPUnit Framework exceptions.
|
||||
*
|
||||
* Ensures that exceptions thrown during a test run do not leave stray
|
||||
* references behind.
|
||||
*
|
||||
* Every Exception contains a stack trace. Each stack frame contains the 'args'
|
||||
* of the called function. The function arguments can contain references to
|
||||
* instantiated objects. The references prevent the objects from being
|
||||
* destructed (until test results are eventually printed), so memory cannot be
|
||||
* freed up.
|
||||
*
|
||||
* With enabled process isolation, test results are serialized in the child
|
||||
* process and unserialized in the parent process. The stack trace of Exceptions
|
||||
* may contain objects that cannot be serialized or unserialized (e.g., PDO
|
||||
* connections). Unserializing user-space objects from the child process into
|
||||
* the parent would break the intended encapsulation of process isolation.
|
||||
*
|
||||
* @see http://fabien.potencier.org/article/9/php-serialization-stack-traces-and-exceptions
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
|
||||
*
|
||||
* @internal This class is not covered by the backward compatibility promise for PHPUnit
|
||||
*/
|
||||
class Exception extends RuntimeException implements \PHPUnit\Exception
|
||||
{
|
||||
protected array $serializableTrace;
|
||||
|
||||
public function __construct(string $message = '', int|string $code = 0, ?Throwable $previous = null)
|
||||
{
|
||||
/**
|
||||
* @see https://github.com/sebastianbergmann/phpunit/issues/5965
|
||||
*/
|
||||
if (!is_int($code)) {
|
||||
$message .= sprintf(
|
||||
' (exception code: %s)',
|
||||
$code,
|
||||
);
|
||||
|
||||
$code = 0;
|
||||
}
|
||||
|
||||
parent::__construct($message, $code, $previous);
|
||||
|
||||
$this->serializableTrace = $this->getTrace();
|
||||
|
||||
foreach (array_keys($this->serializableTrace) as $key) {
|
||||
unset($this->serializableTrace[$key]['args']);
|
||||
}
|
||||
}
|
||||
|
||||
public function __sleep(): array
|
||||
{
|
||||
return array_keys(get_object_vars($this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the serializable trace (without 'args').
|
||||
*/
|
||||
public function getSerializableTrace(): array
|
||||
{
|
||||
return $this->serializableTrace;
|
||||
}
|
||||
}
|
||||
Vendored
+41
@@ -0,0 +1,41 @@
|
||||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of PHPUnit.
|
||||
*
|
||||
* (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 PHPUnit\Framework;
|
||||
|
||||
use Exception;
|
||||
use SebastianBergmann\Comparator\ComparisonFailure;
|
||||
|
||||
/**
|
||||
* Exception for expectations which failed their check.
|
||||
*
|
||||
* The exception contains the error message and optionally a
|
||||
* SebastianBergmann\Comparator\ComparisonFailure which is used to
|
||||
* generate diff output of the failed expectations.
|
||||
*
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
|
||||
*
|
||||
* @internal This class is not covered by the backward compatibility promise for PHPUnit
|
||||
*/
|
||||
final class ExpectationFailedException extends AssertionFailedError
|
||||
{
|
||||
protected ?ComparisonFailure $comparisonFailure = null;
|
||||
|
||||
public function __construct(string $message, ?ComparisonFailure $comparisonFailure = null, ?Exception $previous = null)
|
||||
{
|
||||
$this->comparisonFailure = $comparisonFailure;
|
||||
|
||||
parent::__construct($message, 0, $previous);
|
||||
}
|
||||
|
||||
public function getComparisonFailure(): ?ComparisonFailure
|
||||
{
|
||||
return $this->comparisonFailure;
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of PHPUnit.
|
||||
*
|
||||
* (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 PHPUnit\Framework;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
/**
|
||||
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
|
||||
*
|
||||
* @internal This class is not covered by the backward compatibility promise for PHPUnit
|
||||
*/
|
||||
final class GeneratorNotSupportedException extends InvalidArgumentException
|
||||
{
|
||||
public static function fromParameterName(string $parameterName): self
|
||||
{
|
||||
return new self(
|
||||
sprintf(
|
||||
'Passing an argument of type Generator for the %s parameter is not supported',
|
||||
$parameterName,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user