diff --git a/composer.json b/composer.json index 4eab89c0..d88d9d48 100644 --- a/composer.json +++ b/composer.json @@ -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", diff --git a/overrides/Runner/Filter/NameFilterIterator.php b/overrides/Runner/Filter/NameFilterIterator.php index 16641329..537f5820 100644 --- a/overrides/Runner/Filter/NameFilterIterator.php +++ b/overrides/Runner/Filter/NameFilterIterator.php @@ -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> + * * @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 $iterator - * @psalm-param non-empty-string $filter + * @param RecursiveIterator $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, - ]; - } } diff --git a/overrides/Runner/ResultCache/DefaultResultCache.php b/overrides/Runner/ResultCache/DefaultResultCache.php deleted file mode 100644 index 025134b2..00000000 --- a/overrides/Runner/ResultCache/DefaultResultCache.php +++ /dev/null @@ -1,171 +0,0 @@ - - * - * 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 - */ - private array $defects = []; - - /** - * @var array - */ - 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(); - } -} diff --git a/overrides/Runner/TestRunHistory/DefaultTestRunHistory.php b/overrides/Runner/TestRunHistory/DefaultTestRunHistory.php new file mode 100644 index 00000000..d364378d --- /dev/null +++ b/overrides/Runner/TestRunHistory/DefaultTestRunHistory.php @@ -0,0 +1,302 @@ + + * + * 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 + */ + private array $defects = []; + + /** + * @var array + */ + private array $times = []; + + /** + * @var array + */ + private array $changedDefects = []; + + /** + * @var array + */ + 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, 1: array} + */ + 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(); + } +} diff --git a/overrides/Runner/TestSuiteSorter.php b/overrides/Runner/TestSuiteSorter.php index 53255526..d91569c0 100644 --- a/overrides/Runner/TestSuiteSorter.php +++ b/overrides/Runner/TestSuiteSorter.php @@ -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 @@ -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 $tests + * @return list + */ + private function sortByDurationDescending(array $tests): array + { + usort( + $tests, + fn (Test $left, Test $right) => $this->cmpDuration($right, $left), + ); + + return $tests; + } + /** * @param list $tests * @return list @@ -224,6 +256,20 @@ final class TestSuiteSorter return $tests; } + /** + * @param list $tests + * @return list + */ + 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; + return $priorityB <=> $priorityA; } private function cmpDuration(Test $a, Test $b): int { - if (! ($a instanceof Reorderable && $b instanceof Reorderable)) { - return 0; + return $this->durationWeight($a) <=> $this->durationWeight($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; } - return $this->cache->time(ResultCacheId::fromReorderable($a)) <=> $this->cache->time(ResultCacheId::fromReorderable($b)); + 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 $tests - * @return array + * @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 $tests + * @return list */ 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 $tests */ private function anyTestHasDependencies(iterable $tests): bool diff --git a/src/Bootstrappers/BootOverrides.php b/src/Bootstrappers/BootOverrides.php index a53bf330..e2671d05 100644 --- a/src/Bootstrappers/BootOverrides.php +++ b/src/Bootstrappers/BootOverrides.php @@ -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', diff --git a/src/Plugins/Cache.php b/src/Plugins/Cache.php index 1d4ca21f..6c22ae59 100644 --- a/src/Plugins/Cache.php +++ b/src/Plugins/Cache.php @@ -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 $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)); + } } diff --git a/src/Plugins/Parallel/Handlers/Parallel.php b/src/Plugins/Parallel/Handlers/Parallel.php index 99f2a140..023d54c0 100644 --- a/src/Plugins/Parallel/Handlers/Parallel.php +++ b/src/Plugins/Parallel/Handlers/Parallel.php @@ -20,6 +20,7 @@ final class Parallel implements HandlesArguments '-p', '--no-output', '--cache-result', + '--record-test-run-history', ]; public function handleArguments(array $arguments): array diff --git a/src/Plugins/Parallel/Paratest/WrapperRunner.php b/src/Plugins/Parallel/Paratest/WrapperRunner.php index 3bc9b67d..8ea7881a 100644 --- a/src/Plugins/Parallel/Paratest/WrapperRunner.php +++ b/src/Plugins/Parallel/Paratest/WrapperRunner.php @@ -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 === []) { diff --git a/src/Plugins/Tia/CoverageMerger.php b/src/Plugins/Tia/CoverageMerger.php index bed832f9..680a71fb 100644 --- a/src/Plugins/Tia/CoverageMerger.php +++ b/src/Plugins/Tia/CoverageMerger.php @@ -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 = []; diff --git a/tests/.pest/snapshots/Visual/Help/visual_snapshot_of_help_command_output.snap b/tests/.pest/snapshots/Visual/Help/visual_snapshot_of_help_command_output.snap index d9517985..f39892d8 100644 --- a/tests/.pest/snapshots/Visual/Help/visual_snapshot_of_help_command_output.snap +++ b/tests/.pest/snapshots/Visual/Help/visual_snapshot_of_help_command_output.snap @@ -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 diff --git a/tests/.snapshots/success.txt b/tests/.snapshots/success.txt index 527b14cd..d4366261 100644 --- a/tests/.snapshots/success.txt +++ b/tests/.snapshots/success.txt @@ -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) \ No newline at end of file + Tests: 2 deprecated, 4 warnings, 5 incomplete, 2 notices, 40 todos, 34 skipped, 1561 passed (3405 assertions) \ No newline at end of file diff --git a/tests/Visual/Parallel.php b/tests/Visual/Parallel.php index 34ccd6d9..84f3e3a4 100644 --- a/tests/Visual/Parallel.php +++ b/tests/Visual/Parallel.php @@ -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, 26 skipped, 1543 passed (3350 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, 26 skipped, 1543 passed (3350 assertions)'; expect($output) ->toContain("Tests: {$expected}") diff --git a/tests/Visual/Success.php b/tests/Visual/Success.php index 7ca9b0bb..75df24bd 100644 --- a/tests/Visual/Success.php +++ b/tests/Visual/Success.php @@ -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);