Compare commits

...

5 Commits

Author SHA1 Message Date
nuno maduro 375b92e93b chore: updates sponsors 2026-08-10 11:25:58 +01:00
nuno maduro e0e00ca748 chore: style 2026-08-10 11:12:08 +01:00
nuno maduro d70ecb873e chore: skip --tia on --mutate 2026-08-10 11:07:46 +01:00
nuno maduro 638932ddb5 chore: updates snapshots 2026-08-10 10:22:36 +01:00
nuno maduro ac48185736 chore: upgrades to phpunit:13.3 2026-08-10 10:16:30 +01:00
17 changed files with 574 additions and 550 deletions
+8 -3
View File
@@ -29,13 +29,18 @@
We cannot thank our sponsors enough for their incredible support in funding Pest's development. Their contributions have been instrumental in making Pest the best it can be. For those who are interested in becoming a sponsor, please visit Nuno Maduro's Sponsor page at **[github.com/sponsors/nunomaduro](https://github.com/sponsors/nunomaduro)**.
- **[CMS Max](https://cmsmax.com/?ref=pestphp)**
- **[PhpStorm](https://jb.gg/nuno)**
- **[CodeRabbit](https://coderabbit.link/nunomaduro)**
- **[SerpApi](https://serpapi.com/?ref=nunomaduro)**
- **[Typesense](https://typesense.org/?ref=nunomaduro)**
- **[Bento](https://bentonow.com/?ref=nunomaduro)**
- **[CMS Max](https://cmsmax.com/?ref=nunomaduro)**
- **[Redberry](https://redberry.international/laravel-development/?utm_source=pest&utm_medium=banner&utm_campaign=pest_sponsorship)**
- **[Typesense](https://typesense.org/?ref=nunomaduro)**
- **[Pixel](https://wearepixel.com.au/?ref=nunomaduro)**
- **[Redberry](https://redberry.international/laravel-development/?ref=nunomaduro)**
- [TestMu AI](https://www.testmuai.com/?utm_medium=sponsor&utm_source=pest)
- [Zapiet](https://zapiet.com/?ref=nunomaduro)
- [LoadForge](https://loadforge.com/?ref=nunomaduro)
- [Route4Me](https://route4me.com/?ref=nunomaduro)
- [Nerdify](https://getnerdify.com/?ref=nunomaduro)
Pest is an open-sourced software licensed under the **[MIT license](https://opensource.org/licenses/MIT)**.
+5 -5
View File
@@ -18,19 +18,19 @@
],
"require": {
"php": "^8.4",
"brianium/paratest": "^7.23.1",
"brianium/paratest": "^7.24.0",
"nunomaduro/collision": "^8.9.5",
"nunomaduro/termwind": "^2.4.0",
"pestphp/pest-plugin": "^5.0.0",
"pestphp/pest-plugin-arch": "^5.0.0",
"pestphp/pest-plugin-mutate": "^5.0.1",
"pestphp/pest-plugin-profanity": "^5.0.0",
"phpunit/phpunit": "^13.2.6",
"phpunit/phpunit": "^13.3.0",
"symfony/process": "^8.1.0"
},
"conflict": {
"filp/whoops": "<2.18.3",
"phpunit/phpunit": ">13.2.6",
"phpunit/phpunit": ">13.3.0",
"sebastian/exporter": "<7.0.0",
"webmozart/assert": "<1.11.0"
},
@@ -61,7 +61,7 @@
"require-dev": {
"pestphp/pest-dev-tools": "^5.0.0",
"pestphp/pest-plugin-browser": "^5.0.0",
"pestphp/pest-plugin-phpstan": "^5.0.0",
"pestphp/pest-plugin-phpstan": "^5.0.2",
"pestphp/pest-plugin-rector": "^5.0.3",
"pestphp/pest-plugin-type-coverage": "^5.0.2",
"psy/psysh": "^0.12.24"
@@ -96,7 +96,7 @@
"test:parallel": "php bin/pest --exclude-group=integration --parallel --processes=3",
"test:integration": "php bin/pest --group=integration -v",
"test:tia": "php bin/pest --group=tia -v",
"update:snapshots": "REBUILD_SNAPSHOTS=true php bin/pest --update-snapshots",
"update:snapshots": "REBUILD_SNAPSHOTS=true php bin/pest --update-snapshots --exclude-group=tia",
"test": [
"@test:lint",
"@test:type:check",
+11 -76
View File
@@ -16,42 +16,32 @@ namespace PHPUnit\Runner\Filter;
use Pest\Contracts\HasPrintableTestCaseName;
use PHPUnit\Framework\Test;
use PHPUnit\Framework\TestSuite;
use PHPUnit\Runner\PhptTestCase;
use PHPUnit\Runner\Phpt\TestCase as PhptTestCase;
use RecursiveFilterIterator;
use RecursiveIterator;
use function end;
use function preg_match;
use function sprintf;
use function str_replace;
use function trim;
/**
* @extends RecursiveFilterIterator<int, Test, RecursiveIterator<int, Test>>
*
* @internal This class is not covered by the backward compatibility promise for PHPUnit
*/
abstract class NameFilterIterator extends RecursiveFilterIterator
{
/**
* @psalm-var non-empty-string
*/
private readonly string $regularExpression;
private readonly ?int $dataSetMinimum;
private readonly ?int $dataSetMaximum;
private readonly CompiledNameFilter $filter;
/**
* @psalm-param RecursiveIterator<int, Test> $iterator
* @psalm-param non-empty-string $filter
* @param RecursiveIterator<int, Test> $iterator
* @param non-empty-string $filter
*/
public function __construct(RecursiveIterator $iterator, string $filter)
{
parent::__construct($iterator);
$preparedFilter = $this->prepareFilter($filter);
$this->regularExpression = $preparedFilter['regularExpression'];
$this->dataSetMinimum = $preparedFilter['dataSetMinimum'];
$this->dataSetMaximum = $preparedFilter['dataSetMaximum'];
$this->filter = CompiledNameFilter::from($filter);
}
public function accept(): bool
@@ -74,70 +64,15 @@ abstract class NameFilterIterator extends RecursiveFilterIterator
$name = $test::class.'::'.$test->nameWithDataSet();
}
$accepted = @preg_match($this->regularExpression, $name, $matches) === 1;
$accepted = @preg_match($this->filter->regularExpression(), $name, $matches) === 1;
if ($accepted && isset($this->dataSetMaximum)) {
if ($accepted && $this->filter->hasDataSetRange()) {
$set = end($matches);
$accepted = $set >= $this->dataSetMinimum && $set <= $this->dataSetMaximum;
$accepted = $set >= $this->filter->dataSetMinimum() && $set <= $this->filter->dataSetMaximum();
}
return $this->doAccept($accepted);
}
abstract protected function doAccept(bool $result): bool;
/**
* @psalm-param non-empty-string $filter
*
* @psalm-return array{regularExpression: non-empty-string, dataSetMinimum: ?int, dataSetMaximum: ?int}
*/
private function prepareFilter(string $filter): array
{
$dataSetMinimum = null;
$dataSetMaximum = null;
if (@preg_match($filter, '') === false) {
if (preg_match('/^(.*?)#(\d+)(?:-(\d+))?$/', $filter, $matches)) {
if (isset($matches[3]) && $matches[2] < $matches[3]) {
$filter = sprintf(
'%s.*with data set #(\d+)$',
$matches[1],
);
$dataSetMinimum = (int) $matches[2];
$dataSetMaximum = (int) $matches[3];
} else {
$filter = sprintf(
'%s.*with data set #%s$',
$matches[1],
$matches[2],
);
}
}
// * testDetermineJsonError@JSON_ERROR_NONE
// * testDetermineJsonError@JSON.*
elseif (preg_match('/^(.*?)@(.+)$/', $filter, $matches)) {
$filter = sprintf(
'%s.*with data set "%s"$',
$matches[1],
$matches[2],
);
}
$filter = sprintf(
'/%s/i',
str_replace(
'/',
'\\/',
$filter,
),
);
}
return [
'regularExpression' => $filter,
'dataSetMinimum' => $dataSetMinimum,
'dataSetMaximum' => $dataSetMaximum,
];
}
}
@@ -1,171 +0,0 @@
<?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\Runner\ResultCache;
use const DIRECTORY_SEPARATOR;
use const LOCK_EX;
use PHPUnit\Framework\TestStatus\TestStatus;
use PHPUnit\Runner\DirectoryDoesNotExistException;
use PHPUnit\Runner\Exception;
use PHPUnit\Util\Filesystem;
use function array_keys;
use function assert;
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 json_decode;
use function json_encode;
use function Pest\version;
/**
* @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 DefaultResultCache implements ResultCache
{
private const string DEFAULT_RESULT_CACHE_FILENAME = '.phpunit.result.cache';
private readonly string $cacheFilename;
/**
* @var array<string, TestStatus>
*/
private array $defects = [];
/**
* @var array<string, float>
*/
private array $times = [];
public function __construct(?string $filepath = null)
{
if ($filepath !== null && is_dir($filepath)) {
$filepath .= DIRECTORY_SEPARATOR.self::DEFAULT_RESULT_CACHE_FILENAME;
}
$this->cacheFilename = $filepath ?? $_ENV['PHPUNIT_RESULT_CACHE'] ?? self::DEFAULT_RESULT_CACHE_FILENAME;
}
public function setStatus(ResultCacheId $id, TestStatus $status): void
{
if ($status->isSuccess()) {
return;
}
$this->defects[$id->asString()] = $status;
}
public function status(ResultCacheId $id): TestStatus
{
return $this->defects[$id->asString()] ?? TestStatus::unknown();
}
public function setTime(ResultCacheId $id, float $time): void
{
$this->times[$id->asString()] = $time;
}
public function time(ResultCacheId $id): float
{
return $this->times[$id->asString()] ?? 0.0;
}
public function mergeWith(self $other): void
{
foreach ($other->defects as $id => $defect) {
$this->defects[$id] = $defect;
}
foreach ($other->times as $id => $time) {
$this->times[$id] = $time;
}
}
public function load(): void
{
if (! is_file($this->cacheFilename)) {
return;
}
$contents = file_get_contents($this->cacheFilename);
if ($contents === false) {
return;
}
$data = json_decode(
$contents,
true,
);
if ($data === null) {
return;
}
if (! isset($data['version'])) {
return;
}
if ($data['version'] !== $this->cacheVersion()) {
return;
}
assert(isset($data['defects']) && is_array($data['defects']));
assert(isset($data['times']) && is_array($data['times']));
foreach (array_keys($data['defects']) as $test) {
$data['defects'][$test] = TestStatus::from($data['defects'][$test]);
}
$this->defects = $data['defects'];
$this->times = $data['times'];
}
/**
* @throws Exception
*/
public function persist(): void
{
if (! Filesystem::createDirectory(dirname($this->cacheFilename))) {
throw new DirectoryDoesNotExistException(dirname($this->cacheFilename));
}
$data = [
'version' => $this->cacheVersion(),
'defects' => [],
'times' => $this->times,
];
foreach ($this->defects as $test => $status) {
$data['defects'][$test] = $status->asInt();
}
file_put_contents(
$this->cacheFilename,
json_encode($data),
LOCK_EX,
);
}
private function cacheVersion(): string
{
return 'pest_'.version();
}
}
@@ -0,0 +1,302 @@
<?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\Runner\TestRunHistory;
use const DIRECTORY_SEPARATOR;
use const LOCK_EX;
use const LOCK_UN;
use PHPUnit\Framework\TestStatus\TestStatus;
use PHPUnit\Runner\DirectoryDoesNotExistException;
use PHPUnit\Runner\Exception;
use PHPUnit\Util\Filesystem;
use function array_keys;
use function dirname;
use function fclose;
use function file_get_contents;
use function flock;
use function fopen;
use function ftruncate;
use function fwrite;
use function is_array;
use function is_dir;
use function is_file;
use function is_float;
use function is_int;
use function is_string;
use function json_decode;
use function json_encode;
use function Pest\version;
use function rewind;
use function stream_get_contents;
/**
* @internal This class is not covered by the backward compatibility promise for PHPUnit
*/
final class DefaultTestRunHistory implements TestRunHistory
{
private const string DEFAULT_FILENAME = '.phpunit.result.cache';
private readonly string $filename;
/**
* @var array<string, TestStatus>
*/
private array $defects = [];
/**
* @var array<string, float>
*/
private array $times = [];
/**
* @var array<string, true>
*/
private array $changedDefects = [];
/**
* @var array<string, true>
*/
private array $changedTimes = [];
public function __construct(?string $filepath = null)
{
if ($filepath !== null && is_dir($filepath)) {
$filepath .= DIRECTORY_SEPARATOR.self::DEFAULT_FILENAME;
}
$this->filename = $filepath ?? $_ENV['PHPUNIT_RESULT_CACHE'] ?? self::DEFAULT_FILENAME;
}
public function setStatus(TestRunHistoryId $id, TestStatus $status): void
{
if ($status->isSuccess()) {
return;
}
$this->defects[$id->asString()] = $status;
$this->changedDefects[$id->asString()] = true;
}
public function remove(TestRunHistoryId $id): void
{
unset($this->defects[$id->asString()]);
$this->changedDefects[$id->asString()] = true;
}
public function status(TestRunHistoryId $id): TestStatus
{
return $this->defects[$id->asString()] ?? TestStatus::unknown();
}
public function setTime(TestRunHistoryId $id, float $time): void
{
$this->times[$id->asString()] = $time;
$this->changedTimes[$id->asString()] = true;
}
public function time(TestRunHistoryId $id): float
{
return $this->times[$id->asString()] ?? 0.0;
}
public function mergeWith(self $other): void
{
foreach ($other->defects as $id => $defect) {
$this->defects[$id] = $defect;
$this->changedDefects[$id] = true;
}
foreach ($other->times as $id => $time) {
$this->times[$id] = $time;
$this->changedTimes[$id] = true;
}
}
public function load(): void
{
if (! is_file($this->filename)) {
return;
}
$contents = file_get_contents($this->filename);
if ($contents === false) {
// @codeCoverageIgnoreStart
return;
// @codeCoverageIgnoreEnd
}
$parsed = $this->parse($contents);
if ($parsed === null) {
return;
}
[$this->defects, $this->times] = $parsed;
$this->changedDefects = [];
$this->changedTimes = [];
}
/**
* @throws Exception
*/
public function persist(): void
{
$this->writeToFile(false);
}
/**
* @throws Exception
*/
public function persistAndPrune(): void
{
$this->writeToFile(true);
}
/**
* @throws Exception
*/
private function writeToFile(bool $prune): void
{
if (! Filesystem::createDirectory(dirname($this->filename))) {
throw new DirectoryDoesNotExistException(dirname($this->filename));
}
$handle = fopen($this->filename, 'c+');
if ($handle === false) {
// @codeCoverageIgnoreStart
return;
// @codeCoverageIgnoreEnd
}
flock($handle, LOCK_EX);
if ($prune) {
$defects = [];
foreach ($this->defects as $id => $status) {
if (isset($this->changedDefects[$id])) {
$defects[$id] = $status;
}
}
$times = [];
foreach ($this->times as $id => $time) {
if (isset($this->changedTimes[$id]) || isset($this->changedDefects[$id])) {
$times[$id] = $time;
}
}
} else {
$parsed = $this->parse((string) stream_get_contents($handle));
if ($parsed !== null) {
[$defects, $times] = $parsed;
foreach (array_keys($this->changedDefects) as $id) {
if (isset($this->defects[$id])) {
$defects[$id] = $this->defects[$id];
} else {
unset($defects[$id]);
}
}
foreach ($this->times as $id => $time) {
if (isset($this->changedTimes[$id])) {
$times[$id] = $time;
}
}
} else {
$defects = $this->defects;
$times = $this->times;
}
}
$data = [
'version' => $this->version(),
'defects' => [],
'times' => $times,
];
foreach ($defects as $test => $status) {
$data['defects'][$test] = $status->asInt();
}
$json = json_encode($data);
if ($json !== false) {
ftruncate($handle, 0);
rewind($handle);
fwrite($handle, $json);
}
flock($handle, LOCK_UN);
fclose($handle);
}
/**
* @return ?array{0: array<string, TestStatus>, 1: array<string, float>}
*/
private function parse(string $contents): ?array
{
$data = json_decode(
$contents,
true,
);
if (! is_array($data)) {
return null;
}
if (! isset($data['version']) || $data['version'] !== $this->version()) {
return null;
}
if (! isset($data['defects'], $data['times']) || ! is_array($data['defects']) || ! is_array($data['times'])) {
return null;
}
$defects = [];
foreach ($data['defects'] as $test => $status) {
if (! is_string($test) || ! is_int($status)) {
continue;
}
$defects[$test] = TestStatus::from($status);
}
$times = [];
foreach ($data['times'] as $test => $time) {
if (! is_string($test) || (! is_float($time) && ! is_int($time))) {
continue;
}
$times[$test] = (float) $time;
}
return [$defects, $times];
}
private function version(): string
{
return 'pest_'.version();
}
}
+120 -49
View File
@@ -14,13 +14,14 @@ declare(strict_types=1);
namespace PHPUnit\Runner;
use PHPUnit\Framework\DataProviderTestSuite;
use PHPUnit\Framework\IterativeTestSuite;
use PHPUnit\Framework\Reorderable;
use PHPUnit\Framework\Test;
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\TestSuite;
use PHPUnit\Runner\ResultCache\NullResultCache;
use PHPUnit\Runner\ResultCache\ResultCache;
use PHPUnit\Runner\ResultCache\ResultCacheId;
use PHPUnit\Runner\TestRunHistory\NullTestRunHistory;
use PHPUnit\Runner\TestRunHistory\TestRunHistory;
use PHPUnit\Runner\TestRunHistory\TestRunHistoryId;
use function array_diff;
use function array_merge;
@@ -46,9 +47,13 @@ final class TestSuiteSorter
public const int ORDER_DEFECTS_FIRST = 3;
public const int ORDER_DURATION = 4;
public const int ORDER_DURATION_ASCENDING = 4;
public const int ORDER_SIZE = 5;
public const int ORDER_SIZE_ASCENDING = 5;
public const int ORDER_DURATION_DESCENDING = 6;
public const int ORDER_SIZE_DESCENDING = 7;
/**
* @var non-empty-array<non-empty-string, positive-int>
@@ -65,11 +70,11 @@ final class TestSuiteSorter
*/
private array $defectSortOrder = [];
private readonly ResultCache $cache;
private readonly TestRunHistory $testRunHistory;
public function __construct(?ResultCache $cache = null)
public function __construct(?TestRunHistory $testRunHistory = null)
{
$this->cache = $cache ?? new NullResultCache;
$this->testRunHistory = $testRunHistory ?? new NullTestRunHistory;
}
/**
@@ -81,8 +86,10 @@ final class TestSuiteSorter
self::ORDER_DEFAULT,
self::ORDER_REVERSED,
self::ORDER_RANDOMIZED,
self::ORDER_DURATION,
self::ORDER_SIZE,
self::ORDER_DURATION_ASCENDING,
self::ORDER_SIZE_ASCENDING,
self::ORDER_DURATION_DESCENDING,
self::ORDER_SIZE_DESCENDING,
];
if (! in_array($order, $allowedOrders, true)) {
@@ -102,6 +109,10 @@ final class TestSuiteSorter
// @codeCoverageIgnoreEnd
}
if ($suite instanceof IterativeTestSuite) {
return;
}
if ($suite instanceof TestSuite) {
foreach ($suite as $_suite) {
$this->reorderTestsInSuite($_suite, $order, $resolveDependencies, $orderDefects);
@@ -117,31 +128,37 @@ final class TestSuiteSorter
private function sort(TestSuite $suite, int $order, bool $resolveDependencies, int $orderDefects): void
{
if ($suite->tests() === []) {
$tests = $suite->tests();
if ($tests === []) {
return;
}
if ($order === self::ORDER_REVERSED) {
$suite->setTests($this->reverse($suite->tests()));
$tests = $this->reverse($tests);
} elseif ($order === self::ORDER_RANDOMIZED) {
$suite->setTests($this->randomize($suite->tests()));
} elseif ($order === self::ORDER_DURATION) {
$suite->setTests($this->sortByDuration($suite->tests()));
} elseif ($order === self::ORDER_SIZE) {
$suite->setTests($this->sortBySize($suite->tests()));
$tests = $this->randomize($tests);
} elseif ($order === self::ORDER_DURATION_ASCENDING) {
$tests = $this->sortByDuration($tests);
} elseif ($order === self::ORDER_DURATION_DESCENDING) {
$tests = $this->sortByDurationDescending($tests);
} elseif ($order === self::ORDER_SIZE_ASCENDING) {
$tests = $this->sortBySize($tests);
} elseif ($order === self::ORDER_SIZE_DESCENDING) {
$tests = $this->sortBySizeDescending($tests);
}
if ($orderDefects === self::ORDER_DEFECTS_FIRST) {
$suite->setTests($this->sortDefectsFirst($suite->tests()));
$tests = $this->sortDefectsFirst($tests);
}
if ($resolveDependencies && ! ($suite instanceof DataProviderTestSuite)) {
$tests = $suite->tests();
/** @noinspection PhpParamsInspection */
/** @phpstan-ignore argument.type */
$suite->setTests($this->resolveDependencies($tests));
$tests = $this->resolveDependencies($tests);
}
$suite->setTests($tests);
}
private function addSuiteToDefectSortOrder(TestSuite $suite): void
@@ -154,9 +171,10 @@ final class TestSuiteSorter
$sortId = $test->sortId();
if (! isset($this->defectSortOrder[$sortId])) {
$this->defectSortOrder[$sortId] = $this->cache->status(ResultCacheId::fromReorderable($test))->asInt();
$max = max($max, $this->defectSortOrder[$sortId]);
$this->defectSortOrder[$sortId] = $this->testRunHistory->status(TestRunHistoryId::fromReorderable($test))->sortWeight();
}
$max = max($max, $this->defectSortOrder[$sortId]);
}
$this->defectSortOrder[$suite->sortId()] = $max;
@@ -210,6 +228,20 @@ final class TestSuiteSorter
return $tests;
}
/**
* @param list<Test> $tests
* @return list<Test>
*/
private function sortByDurationDescending(array $tests): array
{
usort(
$tests,
fn (Test $left, Test $right) => $this->cmpDuration($right, $left),
);
return $tests;
}
/**
* @param list<Test> $tests
* @return list<Test>
@@ -224,6 +256,20 @@ final class TestSuiteSorter
return $tests;
}
/**
* @param list<Test> $tests
* @return list<Test>
*/
private function sortBySizeDescending(array $tests): array
{
usort(
$tests,
fn (Test $left, Test $right) => $this->cmpSize($right, $left),
);
return $tests;
}
private function cmpDefectPriorityAndTime(Test $a, Test $b): int
{
assert($a instanceof Reorderable);
@@ -232,45 +278,72 @@ final class TestSuiteSorter
$priorityA = $this->defectSortOrder[$a->sortId()] ?? 0;
$priorityB = $this->defectSortOrder[$b->sortId()] ?? 0;
if ($priorityA !== $priorityB) {
return $priorityB <=> $priorityA;
}
if ($priorityA > 0 || $priorityB > 0) {
return $this->cmpDuration($a, $b);
}
return 0;
}
private function cmpDuration(Test $a, Test $b): int
{
if (! ($a instanceof Reorderable && $b instanceof Reorderable)) {
return 0;
return $this->durationWeight($a) <=> $this->durationWeight($b);
}
return $this->cache->time(ResultCacheId::fromReorderable($a)) <=> $this->cache->time(ResultCacheId::fromReorderable($b));
private function durationWeight(Test $test): float
{
if ($test instanceof TestSuite) {
$sum = 0.0;
foreach ($test->tests() as $inner) {
$sum += $this->durationWeight($inner);
}
return $sum;
}
if ($test instanceof Reorderable) {
return $this->testRunHistory->time(TestRunHistoryId::fromReorderable($test));
}
return 0.0;
}
private function cmpSize(Test $a, Test $b): int
{
$sizeA = ($a instanceof TestCase || $a instanceof DataProviderTestSuite)
? $a->size()->asString()
: 'unknown';
$sizeB = ($b instanceof TestCase || $b instanceof DataProviderTestSuite)
? $b->size()->asString()
: 'unknown';
return self::SIZE_SORT_WEIGHT[$sizeA] <=> self::SIZE_SORT_WEIGHT[$sizeB];
return $this->sizeWeight($a) <=> $this->sizeWeight($b);
}
/**
* @param array<TestCase> $tests
* @return array<TestCase>
* @return positive-int
*/
private function sizeWeight(Test $test): int
{
if ($test instanceof TestCase || $test instanceof DataProviderTestSuite) {
return self::SIZE_SORT_WEIGHT[$test->size()->asString()];
}
if ($test instanceof TestSuite) {
$max = 0;
foreach ($test->tests() as $inner) {
$weight = $this->sizeWeight($inner);
if ($weight > $max) {
$max = $weight;
}
}
if ($max > 0) {
return $max;
}
}
return self::SIZE_SORT_WEIGHT['unknown'];
}
/**
* @param list<TestCase> $tests
* @return list<TestCase>
*/
private function resolveDependencies(array $tests): array
{
// when no test uses `->depends()` / PHPUnit `@depends`.
if (! $this->anyTestHasDependencies($tests)) {
return $tests;
}
@@ -279,7 +352,7 @@ final class TestSuiteSorter
$i = 0;
$provided = [];
do {
while ($tests !== [] && $i < count($tests)) {
if (array_diff($tests[$i]->requires(), $provided) === []) {
$provided = array_merge($provided, $tests[$i]->provides());
$newTestOrder = array_merge($newTestOrder, array_splice($tests, $i, 1));
@@ -287,14 +360,12 @@ final class TestSuiteSorter
} else {
$i++;
}
} while ($tests !== [] && ($i < count($tests)));
}
return array_merge($newTestOrder, $tests);
}
/**
* Cheaply determines whether any test in the tree declares @depends.
*
* @param iterable<Test> $tests
*/
private function anyTestHasDependencies(iterable $tests): bool
+1 -1
View File
@@ -18,7 +18,7 @@ final class BootOverrides implements Bootstrapper
public const array FILES = [
'ParaTest/WrapperRunner/ProgressPrinterOutput.php',
'Runner/Filter/NameFilterIterator.php',
'Runner/ResultCache/DefaultResultCache.php',
'Runner/TestRunHistory/DefaultTestRunHistory.php',
'Runner/TestSuiteLoader.php',
'Runner/TestSuiteSorter.php',
'TextUI/Command/Commands/WarmCodeCoverageCacheCommand.php',
+10 -2
View File
@@ -44,10 +44,18 @@ final class Cache implements HandlesArguments
}
}
if (! $this->hasArgument('--parallel', $arguments) && ! $this->hasArgument('--do-not-cache-result', $arguments) && ! $this->hasArgument('--cache-result', $arguments)) {
return $this->pushArgument('--cache-result', $arguments);
if (! $this->hasArgument('--parallel', $arguments) && ! $this->hasTestRunHistoryArgument($arguments)) {
return $this->pushArgument('--record-test-run-history', $arguments);
}
return $arguments;
}
/**
* @param array<int, string> $arguments
*/
private function hasTestRunHistoryArgument(array $arguments): bool
{
return array_any(['--record-test-run-history', '--do-not-record-test-run-history', '--cache-result', '--do-not-cache-result'], fn (string $argument): bool => $this->hasArgument($argument, $arguments));
}
}
@@ -20,6 +20,7 @@ final class Parallel implements HandlesArguments
'-p',
'--no-output',
'--cache-result',
'--record-test-run-history',
];
public function handleArguments(array $arguments): array
@@ -21,7 +21,7 @@ use PHPUnit\Event\Facade as EventFacade;
use PHPUnit\Event\Test\AfterLastTestMethodFailed;
use PHPUnit\Event\TestRunner\WarningTriggered;
use PHPUnit\Runner\CodeCoverage;
use PHPUnit\Runner\ResultCache\DefaultResultCache;
use PHPUnit\Runner\TestRunHistory\DefaultTestRunHistory;
use PHPUnit\TestRunner\TestResult\Facade as TestResultFacade;
use PHPUnit\TestRunner\TestResult\TestResult;
use PHPUnit\TextUI\Configuration\CodeCoverageFilterRegistry;
@@ -417,6 +417,9 @@ final class WrapperRunner implements RunnerInterface
// @phpstan-ignore-next-line
array_merge_recursive($testResultSum->phpWarnings(), $testResult->phpWarnings()),
$testResultSum->numberOfIssuesIgnoredByBaseline() + $testResult->numberOfIssuesIgnoredByBaseline(),
self::numberOfDeprecationsByTrigger($testResultSum, $testResult),
// @phpstan-ignore-next-line
array_merge_recursive($testResultSum->retriedTests(), $testResult->retriedTests()),
);
}
@@ -455,14 +458,16 @@ final class WrapperRunner implements RunnerInterface
$testResultSum->phpNotices(),
$testResultSum->phpWarnings(),
$testResultSum->numberOfIssuesIgnoredByBaseline(),
self::numberOfDeprecationsByTrigger($testResultSum),
$testResultSum->retriedTests(),
);
self::$result = $testResultSum;
if ($this->options->configuration->cacheResult()) {
$resultCacheSum = new DefaultResultCache($this->options->configuration->testResultCacheFile());
if ($this->options->configuration->recordTestRunHistory()) {
$resultCacheSum = new DefaultTestRunHistory($this->options->configuration->testRunHistoryFile());
foreach ($this->resultCacheFiles as $resultCacheFile) {
$resultCache = new DefaultResultCache($resultCacheFile->getPathname());
$resultCache = new DefaultTestRunHistory($resultCacheFile->getPathname());
$resultCache->load();
$resultCacheSum->mergeWith($resultCache);
@@ -495,6 +500,28 @@ final class WrapperRunner implements RunnerInterface
return $exitcode;
}
/**
* @return array{self: non-negative-int, direct: non-negative-int, indirect: non-negative-int, unknown: non-negative-int}
*/
private static function numberOfDeprecationsByTrigger(TestResult ...$results): array
{
$self = $direct = $indirect = $unknown = 0;
foreach ($results as $result) {
$self += max(0, $result->numberOfSelfDeprecations());
$direct += max(0, $result->numberOfDirectDeprecations());
$indirect += max(0, $result->numberOfIndirectDeprecations());
$unknown += max(0, $result->numberOfDeprecationsWithUnknownTrigger());
}
return [
'self' => $self,
'direct' => $direct,
'indirect' => $indirect,
'unknown' => $unknown,
];
}
private function generateCodeCoverageReports(): void
{
if ($this->coverageFiles === []) {
+34
View File
@@ -64,6 +64,8 @@ final class Tia implements AddsOutput, HandlesArguments, HandlesOriginalArgument
private const string BASELINE_PATH_OPTION = '--baseline';
private const string MUTATE_OPTION = '--mutate';
private const string ENV_MUTATION_TESTING = 'PEST_MUTATION_TESTING';
private const string ENV_TIA = 'PEST_TIA';
@@ -329,6 +331,18 @@ final class Tia implements AddsOutput, HandlesArguments, HandlesOriginalArgument
return ! self::argumentPresent('--ci', $arguments);
}
/**
* @param array<int, string> $arguments
*/
public static function isMutationRun(array $arguments): bool
{
if (self::argumentPresent(self::MUTATE_OPTION, $arguments)) {
return true;
}
return getenv(self::ENV_MUTATION_TESTING) !== false;
}
public static function recordsEdgesInWorkers(): bool
{
return (string) Parallel::getGlobal(self::RECORDING_GLOBAL) === '1'
@@ -472,6 +486,8 @@ final class Tia implements AddsOutput, HandlesArguments, HandlesOriginalArgument
$partial = ! $isWorker && ($hasExplicitPath || $this->hasPartialSelection($arguments));
$disabled = $disabled || $partial;
$mutating = self::isMutationRun($this->originalArguments);
if (getenv(self::ENV_MUTATION_TESTING) !== false) {
$this->writesSuppressed = true;
}
@@ -508,6 +524,16 @@ final class Tia implements AddsOutput, HandlesArguments, HandlesOriginalArgument
return $arguments;
}
if (! $isWorker && $mutating && ($enabled || $this->forceRefetch)) {
$this->requestWorkerResults();
$this->emitMutationScopedRecordSkipped();
$this->forceRefetch = false;
$this->filteredMode = false;
return $arguments;
}
if ($isWorker && (string) Parallel::getGlobal(self::WORKER_RESULTS_GLOBAL) === '1') {
$this->flushesWorkerResults = true;
$this->resultsOnlyWrites = true;
@@ -1286,6 +1312,14 @@ final class Tia implements AddsOutput, HandlesArguments, HandlesOriginalArgument
$this->renderChild('Record the baseline with a plain --tia run first; coverage runs then reuse it.');
}
private function emitMutationScopedRecordSkipped(): void
{
$this->output->writeln('');
$this->renderChild('Running in TIA mode, however TIA is skipped as mutation testing needs the coverage of a complete run.');
$this->renderChild('Drop --mutate to narrow and replay with TIA.');
}
/**
* @param array<string, array<int, string>> $perTestFiles
* @param array<string, array<int, string>> $perTestTables
+24 -2
View File
@@ -57,7 +57,7 @@ final class CoverageMerger
}
self::primeUncoveredFiles($cached);
self::primeUncoveredFiles($current);
self::discardUnexercisedFiles($current);
self::stripCurrentTestsFromCached($cached, $current);
@@ -77,6 +77,28 @@ final class CoverageMerger
$coverage->getData(false);
}
private static function discardUnexercisedFiles(CodeCoverage $coverage): void
{
$data = $coverage->getData(true);
$lineCoverage = $data->lineCoverage();
$discarded = false;
foreach ($lineCoverage as $file => $lines) {
foreach ($lines as $hits) {
if (is_array($hits) && $hits !== []) {
continue 2;
}
}
unset($lineCoverage[$file]);
$discarded = true;
}
if ($discarded) {
$data->setLineCoverage($lineCoverage);
}
}
private static function compress(string $bytes): string
{
$compressed = @gzencode($bytes);
@@ -139,7 +161,7 @@ final class CoverageMerger
*/
private static function collectTestIds(CodeCoverage $coverage): array
{
$data = $coverage->getData();
$data = $coverage->getData(true);
$idByIndex = $data->testIds();
$ids = [];
+4
View File
@@ -78,6 +78,10 @@ final class XdebugRestarter implements Restarter
}
}
if (Tia::isMutationRun($arguments)) {
return false;
}
if (! Tia::isEnabledForRun($arguments)) {
return false;
}
@@ -77,6 +77,9 @@
--fail-on-warning Signal failure using shell exit code when a warning was triggered
--fail-on-risky Signal failure using shell exit code when a test was considered risky
--fail-on-deprecation Signal failure using shell exit code when a deprecation was triggered
--fail-on-self-deprecation Signal failure using shell exit code when a deprecation was triggered in first-party code
--fail-on-direct-deprecation Signal failure using shell exit code when a deprecation was triggered in third-party code called from first-party code
--fail-on-indirect-deprecation Signal failure using shell exit code when a deprecation was triggered in third-party code called from third-party code
--fail-on-phpunit-deprecation Signal failure using shell exit code when a PHPUnit deprecation was triggered
--fail-on-phpunit-notice Signal failure using shell exit code when a PHPUnit notice was triggered
--fail-on-phpunit-warning Signal failure using shell exit code when a PHPUnit warning was triggered
@@ -88,19 +91,26 @@
--do-not-fail-on-warning Do not signal failure using shell exit code when a warning was triggered
--do-not-fail-on-risky Do not signal failure using shell exit code when a test was considered risky
--do-not-fail-on-deprecation Do not signal failure using shell exit code when a deprecation was triggered
--do-not-fail-on-self-deprecation Do not signal failure using shell exit code when a deprecation was triggered in first-party code
--do-not-fail-on-direct-deprecation Do not signal failure using shell exit code when a deprecation was triggered in third-party code called from first-party code
--do-not-fail-on-indirect-deprecation Do not signal failure using shell exit code when a deprecation was triggered in third-party code called from third-party code
--do-not-fail-on-phpunit-deprecation Do not signal failure using shell exit code when a PHPUnit deprecation was triggered
--do-not-fail-on-phpunit-notice Do not signal failure using shell exit code when a PHPUnit notice was triggered
--do-not-fail-on-phpunit-warning Do not signal failure using shell exit code when a PHPUnit warning was triggered
--do-not-fail-on-notice Do not signal failure using shell exit code when a notice was triggered
--do-not-fail-on-skipped Do not signal failure using shell exit code when a test was skipped
--do-not-fail-on-incomplete Do not signal failure using shell exit code when a test was marked incomplete
--cache-result ............................ Write test results to cache file
--do-not-cache-result .............. Do not write test results to cache file
--record-test-run-history .......................... Record test run history
--do-not-record-test-run-history ............ Do not record test run history
--warn-when-php-is-not-configured-for-development Trigger a test runner warning when PHP is not configured for development
--do-not-warn-when-php-is-not-configured-for-development Do not trigger a test runner warning when PHP is not configured for development
--order-by [order] Run tests in order: default|defects|depends|duration-ascending|duration-descending|no-depends|random|reverse|size-ascending|size-descending
--resolve-dependencies ...................... Alias for "--order-by depends"
--ignore-dependencies .................... Alias for "--order-by no-depends"
--random-order ............................... Alias for "--order-by random"
--random-order-seed [N] Use the specified random seed when running tests in random order
--repeat [N] .......... Run each test N times, stopping at the first failure
--retry [N] . Attempt each test up to N times, stopping at the first success
--reverse-order ............................. Alias for "--order-by reverse"
REPORTING OPTIONS:
@@ -151,6 +161,8 @@
--coverage-cobertura [file] Write code coverage report in Cobertura XML format to file
--coverage-crap4j [file] Write code coverage report in Crap4J XML format to file
--coverage-html [dir] Write code coverage report in HTML format to directory
--without-class-view Render code coverage report in HTML format without class view
--without-file-view Render code coverage report in HTML format without file view
--coverage-php [file] .......... Write serialized code coverage data to file
--coverage-text=[file] Write code coverage report in text format to file [default: standard output]
--only-summary-for-coverage-text Option for code coverage report in text format: only show summary
+4 -230
View File
@@ -73,9 +73,9 @@
↓ is marked as todo 3
↓ shouldBeMarkedAsTodo
PASS Tests\Features\Coverage
WARN Tests\Features\Coverage
✓ it has plugin
it adds coverage if --coverage exist
- it adds coverage if --coverage exist → Coverage is not available
✓ it adds coverage if --min exist
✓ it generates coverage based on file input
@@ -1226,7 +1226,7 @@
✓ it does not leak mock objects between retries
✓ it does not stop retrying when snapshot changes are absent
✓ it does not leak dynamic properties between retries
it clears output buffer between retries when expectOutputString is used
! it clears output buffer between retries when expectOutputString is used → Creation of dynamic property PHPUnit\Framework\TestCase\OutputBuffer::$expectedString is deprecated
✓ it preserves output between retries when no output expectation is set
WARN Tests\Features\Helpers
@@ -1568,232 +1568,6 @@
PASS Tests\Features\Tia
✓ it does not run user hooks when replaying cached skipped and incomplete results
PASS Tests\Features\Tia\BranchShapes
✓ a branch name git allows is a branch key TIA can hold with dataset "slashes"
✓ a branch name git allows is a branch key TIA can hold with dataset "dots"
✓ a branch name git allows is a branch key TIA can hold with dataset "unicode"
✓ a branch name git allows is a branch key TIA can hold with dataset "digits"
✓ a branch name git allows is a branch key TIA can hold with dataset "underscores"
✓ a branch name git allows is a branch key TIA can hold with dataset "very long"
✓ a branch differing from the default only in case gets its own key
✓ a branch that only lives on the remote keeps its baseline
✓ a branch checked out in a worktree keeps its baseline
✓ deleting many branches reclaims every one of their baselines with dataset "sequential"
✓ deleting many branches reclaims every one of their baselines with dataset "parallel"
✓ a narrowed run does not reclaim anything
✓ a detached HEAD does not reclaim anything either
✓ the default branch baseline survives every branch that comes and goes
✓ a project below the git repository root refuses to run and writes nothing with dataset "sequential"
✓ a project below the git repository root refuses to run and writes nothing with dataset "parallel"
✓ a repository with no commits says so, and leaves plain runs alone
✓ a directory with no repository at all still asks for git
PASS Tests\Features\Tia\CompleteRunWriteTier
✓ a complete run prunes a deleted test with dataset "sequential"
✓ a complete run prunes a deleted test with dataset "parallel"
✓ a complete run records nothing for a test file the graph does not know
✓ a partial run records nothing for a test file the graph does not know
✓ a truncated run does not prune with dataset "sequential"
✓ a truncated run does not prune with dataset "parallel"
✓ a green bail run is complete
✓ --no-tia refreshes results without enabling tia with dataset "sequential"
✓ --no-tia refreshes results without enabling tia with dataset "parallel"
✓ a plain run refreshes the results it executed with dataset "sequential"
✓ a plain run refreshes the results it executed with dataset "parallel"
✓ a parallel replay keeps the recorded time of tests that did not run
✓ a run that never enables tia creates no graph with dataset "plain"
✓ a run that never enables tia creates no graph with dataset "filtered"
✓ a run that never enables tia creates no graph with dataset "parallel filtered"
✓ a test edit narrows to the affected file and replays the rest
✓ a parallel run merges worker results into the parent baseline
PASS Tests\Features\Tia\CoveragePiggyback
✓ a coverage report does not found a dependency graph with dataset "pest coverage"
✓ a coverage report does not found a dependency graph with dataset "phpunit coverage report"
✓ a coverage report does not found a dependency graph with dataset "parallel"
✓ a plain run after a coverage run records the whole project scope
✓ a coverage report leaves the edges of an existing graph alone
PASS Tests\Features\Tia\DefaultBranchReplay
✓ replays the default branch baseline on a new branch
✓ replays whatever the default branch is called with ('main')
✓ replays whatever the default branch is called with ('master')
✓ replays whatever the default branch is called with ('trunk')
✓ replays whatever the default branch is called with ('develop')
✓ replays on a second new branch too
✓ writes nothing on a second run on the same branch
✓ replays on a branch whose name contains slashes
✓ replays again once back on the default branch
✓ replays on a new branch when tia is enabled by configuration
✓ a narrowed run on a new branch does not cost the fallback with dataset "sequential"
✓ a narrowed run on a new branch does not cost the fallback with dataset "parallel"
✓ replays inside a worktree on a new branch
PASS Tests\Features\Tia\DefaultBranchResolution
✓ a declared default branch beats autodetection
✓ a declared default branch that does not exist degrades to a full run
✓ a renamed default branch replays and writes under its new name
✓ the CI provider names the default branch where the checkout cannot
✓ GitLab names the default branch through its own variable
✓ a lone recorded baseline names the default branch
✓ a default branch nothing can name is refused rather than guessed
✓ an init.defaultBranch naming a branch that exists is still trusted
✓ a repository with no remote is refused rather than silently re-run
✓ a remote-less repository holding one baseline is not refused
✓ a declared default branch stands in for a missing remote
✓ tia still requires git
✓ a plain run outside a repository creates no baseline
✓ the default branch is resolved once per run, not once per test
PASS Tests\Features\Tia\DefaultBranchWriteTier
✓ narrows to the affected tests on a new branch
✓ filtered mode reads the fallback too
✓ filtered mode finds nothing to do on a clean green feature branch
✓ filtered mode falls back to a full replay when a cached failure cannot be located
✓ filtered mode finds nothing to do on the default branch itself
✓ a detached HEAD replays without minting a branch key
✓ a detached HEAD does not write into the default branch baseline with dataset "sequential"
✓ a detached HEAD does not write into the default branch baseline with dataset "parallel"
✓ the branch that ran gets its own key and the default branch keeps its baseline
✓ the fallback reaches parallel workers
PASS Tests\Features\Tia\FilteredMode
✓ re-runs a cached failure on a clean tree
✓ an explicit path turns filtered mode off
✓ filtered mode runs the whole suite when there is no baseline
✓ filtered mode finds nothing to do in parallel either
✓ a corrupt graph is reported and does not crash the run
✓ --parallel --retry is refused and leaves the graph alone
PASS Tests\Features\Tia\HostileState
✓ a graph mangled beyond use still lets the suite run with dataset "empty"
✓ a graph mangled beyond use still lets the suite run with dataset "truncated"
✓ a graph mangled beyond use still lets the suite run with dataset "not json"
✓ a graph mangled beyond use still lets the suite run with dataset "json scalar"
✓ a graph mangled beyond use still lets the suite run with dataset "json list"
✓ a graph mangled beyond use still lets the suite run with dataset "json null"
✓ a graph mangled beyond use still lets the suite run with dataset "empty object"
✓ a graph mangled beyond use still lets the suite run with dataset "nul bytes"
✓ a graph whose shape is wrong everywhere is repaired rather than trusted with dataset "sequential"
✓ a graph whose shape is wrong everywhere is repaired rather than trusted with dataset "parallel"
✓ a cached status this build cannot interpret is re-run, not replayed with dataset "below the range"
✓ a cached status this build cannot interpret is re-run, not replayed with dataset "one past the range"
✓ a cached status this build cannot interpret is re-run, not replayed with dataset "far past the range"
✓ a cached status this build cannot interpret is re-run, not replayed with dataset "huge"
✓ a cached status with no replay of its own does not fail the run with dataset "notice"
✓ a cached status with no replay of its own does not fail the run with dataset "deprecation"
✓ a cached status with no replay of its own does not fail the run with dataset "warning"
✓ a cached skip or todo replays with its message intact with dataset "skipped"
✓ a cached skip or todo replays with its message intact with dataset "incomplete"
✓ a cached failure with a multi-line message re-runs rather than replaying the text
✓ a result pointing outside the project is not addressable and widens the run
✓ an edge pointing at a file id that does not exist is ignored
✓ a graph from a schema this build does not know is rebuilt, not read
✓ graph.json being a directory does not stop the run
✓ a state dir it cannot write to still replays
PASS Tests\Features\Tia\IssueStatuses
✓ a triggered issue is recorded as itself, not as a pass with dataset "sequential" / dataset "deprecation"
✓ a triggered issue is recorded as itself, not as a pass with dataset "sequential" / dataset "notice"
✓ a triggered issue is recorded as itself, not as a pass with dataset "sequential" / dataset "warning"
✓ a triggered issue is recorded as itself, not as a pass with dataset "parallel" / dataset "deprecation"
✓ a triggered issue is recorded as itself, not as a pass with dataset "parallel" / dataset "notice"
✓ a triggered issue is recorded as itself, not as a pass with dataset "parallel" / dataset "warning"
✓ a cached deprecation still fails the run that asked to fail on one with dataset "sequential"
✓ a cached deprecation still fails the run that asked to fail on one with dataset "parallel"
✓ replaying a cached issue does not downgrade it to a pass with dataset "sequential"
✓ replaying a cached issue does not downgrade it to a pass with dataset "parallel"
✓ a failure outranks an issue triggered on the way to it
✓ a skip outranks an issue triggered on the way to it
✓ a suppressed issue is not recorded
PASS Tests\Features\Tia\LivewireComponents
✓ a changed single-file component selects only the tests that rendered it, across workers
✓ a changed multi-file component asset selects the tests that rendered the component
✓ a deleted single-file component selects the tests that rendered it
✓ a Blade file with no generated view still falls back to the watch pattern
PASS Tests\Features\Tia\PartialRunWriteTier
✓ a filtered run rewrites only the test that ran
✓ a filtered run under --tia announces that tia does not apply
✓ a test suffix narrows the tier even though every test runs
✓ a dirty run narrows to the uncommitted test edit
✓ filtered mode yields to an explicit filter
✓ an env flag narrows exactly like the option it mirrors with ('PEST_TIA')
✓ an env flag narrows exactly like the option it mirrors with ('PEST_TIA_FILTERED')
✓ a partial run does not purge the graph even with --fresh
✓ --no-tia does not stop a partial run from refreshing its own entry
✓ two partial runs each keep the other entry
✓ a shard is a partial run
✓ a parallel partial run records the test that ran, like a sequential one
PASS Tests\Features\Tia\RemoteBaseline
✓ a published baseline is fetched instead of recorded locally
✓ a fetched baseline that will not decode is discarded rather than trusted
✓ a fetched baseline recorded against another tree is not used
✓ an artifact without a graph in it fails loudly
✓ a baseline that cannot be authenticated for fails loudly
✓ a workflow or artifact that is not there fails loudly
✓ a network failure warns and lets the suite run with dataset "querying the runs"
✓ a network failure warns and lets the suite run with dataset "downloading the artifact"
✓ no published baseline yet starts a cooldown, and a corrupt cooldown does not break the run
PASS Tests\Features\Tia\SelectionPaths
✓ a committed rename selects the tests that depended on the old path with dataset "sequential"
✓ a committed rename selects the tests that depended on the old path with dataset "parallel"
✓ an affected test file that is gone does not strand a filtered run with dataset "sequential"
✓ an affected test file that is gone does not strand a filtered run with dataset "parallel"
✓ a plain run reclaims the edge of a test file that is gone
✓ a changed view selects the test that rendered it
✓ a changed partial selects the test that rendered its ancestor with dataset "direct @include"
✓ a changed partial selects the test that rendered its ancestor with dataset "transitive @include"
✓ a changed partial selects the test that rendered its ancestor with dataset "x- component"
✓ a changed partial selects the test that rendered its ancestor with dataset "include cycle"
✓ a changed Inertia page selects the test that rendered its component
✓ a changed shared JS module selects the tests of the pages that import it
✓ a changed frontend runtime file selects every Inertia test
PASS Tests\Features\Tia\StateReclamation
✓ a detached HEAD does not purge the graph on structural drift with dataset "sequential"
✓ a detached HEAD does not purge the graph on structural drift with dataset "parallel"
✓ a detached HEAD does not purge the graph with --fresh either with dataset "sequential"
✓ a detached HEAD does not purge the graph with --fresh either with dataset "parallel"
✓ a detached HEAD leaves an unreadable graph for a checkout that can rebuild it
✓ a cached failure whose test file was deleted stops widening later runs with dataset "sequential"
✓ a cached failure whose test file was deleted stops widening later runs with dataset "parallel"
✓ a complete run reclaims the entry and the edge of a deleted test file with dataset "sequential"
✓ a complete run reclaims the entry and the edge of a deleted test file with dataset "parallel"
✓ a pruned result does not come back from the fallback with dataset "sequential"
✓ a pruned result does not come back from the fallback with dataset "parallel"
✓ the fallback still reaches a branch that has never run a test file
✓ a branch that git no longer knows loses its baseline
✓ an unknown cached status is re-run rather than replayed as a failure with dataset "unknown"
✓ an unknown cached status is re-run rather than replayed as a failure with dataset "future"
✓ an unknown cached status is re-run rather than replayed as a failure with dataset "garbage"
✓ a cached notice, deprecation or warning does not replay as a failure with dataset "notice"
✓ a cached notice, deprecation or warning does not replay as a failure with dataset "deprecation"
✓ a cached notice, deprecation or warning does not replay as a failure with dataset "warning"
✓ a malformed baseline entry cannot break the run with dataset "sequential"
✓ a malformed baseline entry cannot break the run with dataset "parallel"
✓ a run torn down mid-file does not prune the tests it never reached with dataset "sequential"
✓ a run torn down mid-file does not prune the tests it never reached with dataset "parallel"
✓ a fatal error mid-file is a test error, not a truncation with dataset "sequential"
✓ a fatal error mid-file is a test error, not a truncation with dataset "parallel"
✓ a green complete run leaves the graph exactly as it found it with dataset "bail"
✓ a green complete run leaves the graph exactly as it found it with dataset "stop-on-failure"
✓ a green complete run leaves the graph exactly as it found it with dataset "compact"
✓ a green complete run leaves the graph exactly as it found it with dataset "parallel bail"
✓ a green complete run leaves the graph exactly as it found it with dataset "parallel one process"
✓ a green complete run leaves the graph exactly as it found it with dataset "parallel more processes than files"
✓ --tia --no-tia is a plain run that still refreshes what it executed with dataset "sequential"
✓ --tia --no-tia is a plain run that still refreshes what it executed with dataset "parallel"
✓ --fresh on a partial run neither purges nor prunes with dataset "sequential"
✓ --fresh on a partial run neither purges nor prunes with dataset "parallel"
✓ a second green run on a feature branch writes nothing at all with dataset "sequential"
✓ a second green run on a feature branch writes nothing at all with dataset "parallel"
✓ a graph whose recorded commit is gone is re-anchored, not warned about forever with dataset "sequential"
✓ a graph whose recorded commit is gone is re-anchored, not warned about forever with dataset "parallel"
PASS Tests\Features\Ticket
✓ it may be associated with an ticket #1, #2
✓ nested → it may be associated with an ticket #1, #4, #5, #6, #3
@@ -2435,4 +2209,4 @@
✓ pass with dataset with ('my-datas-set-value')
✓ within describe → pass with dataset with ('my-datas-set-value')
Tests: 1 deprecated, 4 warnings, 5 incomplete, 2 notices, 40 todos, 34 skipped, 1760 passed (3983 assertions)
Tests: 2 deprecated, 4 warnings, 5 incomplete, 2 notices, 40 todos, 35 skipped, 1560 passed (3400 assertions)
+2 -2
View File
@@ -26,13 +26,13 @@ test('parallel', function () use ($run): void {
$file = file_get_contents(__FILE__);
$file = preg_replace(
'/\$expected = \'.*?\';/',
"\$expected = '1 deprecated, 4 warnings, 5 incomplete, 3 notices, 40 todos, 27 skipped, 1543 passed (3345 assertions)';",
"\$expected = '2 deprecated, 4 warnings, 5 incomplete, 3 notices, 40 todos, 27 skipped, 1542 passed (3345 assertions)';",
$file,
);
file_put_contents(__FILE__, $file);
}
$expected = '1 deprecated, 4 warnings, 5 incomplete, 3 notices, 40 todos, 27 skipped, 1543 passed (3345 assertions)';
$expected = '2 deprecated, 4 warnings, 5 incomplete, 3 notices, 40 todos, 27 skipped, 1542 passed (3345 assertions)';
expect($output)
->toContain("Tests: {$expected}")
+2 -2
View File
@@ -12,9 +12,9 @@ test('visual snapshot of test suite on success', function (): void {
$output = function () use ($testsPath): ?string {
$process = (new Process(
['php', '-d', 'memory_limit=-1', 'bin/pest'],
['php', '-d', 'memory_limit=-1', 'bin/pest', '--exclude-group=tia'],
dirname($testsPath),
['EXCLUDE' => 'integration', '--exclude-group' => 'integration', 'REBUILD_SNAPSHOTS' => false, 'PARATEST' => 0, 'COLLISION_PRINTER' => 'DefaultPrinter', 'COLLISION_IGNORE_DURATION' => 'true', 'PAO_DISABLE' => '1'],
['EXCLUDE' => 'integration', 'REBUILD_SNAPSHOTS' => false, 'PARATEST' => 0, 'COLLISION_PRINTER' => 'DefaultPrinter', 'COLLISION_IGNORE_DURATION' => 'true', 'PAO_DISABLE' => '1'],
));
$process->setTimeout(300.0);