mirror of
https://github.com/pestphp/pest.git
synced 2026-06-05 02:52:12 +02:00
Compare commits
22 Commits
99cc4e0146
...
chore/pin-
| Author | SHA1 | Date | |
|---|---|---|---|
| 5d4d2231fb | |||
| a11a3126f2 | |||
| 2fc75cfcf0 | |||
| 6cc48f63f8 | |||
| e0419d1328 | |||
| faa6988801 | |||
| c12247fafd | |||
| 29b4452443 | |||
| 1b168aba1c | |||
| 6aabd977cd | |||
| a882543c53 | |||
| c250b9da4f | |||
| 46bc3dc628 | |||
| d3ce498b8a | |||
| e1a4b98b71 | |||
| 9afbcd5c18 | |||
| 75593b6454 | |||
| 89590d6120 | |||
| fb0978c9bf | |||
| a3796daa42 | |||
| e3004db666 | |||
| fcf5c27914 |
6
.github/workflows/static.yml
vendored
6
.github/workflows/static.yml
vendored
@ -28,10 +28,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2
|
||||
with:
|
||||
php-version: 8.3
|
||||
tools: composer:v2
|
||||
@ -44,7 +44,7 @@ jobs:
|
||||
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Cache Composer dependencies
|
||||
uses: actions/cache@v5
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
|
||||
with:
|
||||
path: ${{ steps.composer-cache.outputs.dir }}
|
||||
key: static-php-8.3-${{ matrix.dependency-version }}-composer-${{ hashFiles('**/composer.json', '**/composer.lock') }}
|
||||
|
||||
6
.github/workflows/tests.yml
vendored
6
.github/workflows/tests.yml
vendored
@ -35,10 +35,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2
|
||||
with:
|
||||
php-version: ${{ matrix.php }}
|
||||
tools: composer:v2
|
||||
@ -51,7 +51,7 @@ jobs:
|
||||
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Cache Composer dependencies
|
||||
uses: actions/cache@v5
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
|
||||
with:
|
||||
path: ${{ steps.composer-cache.outputs.dir }}
|
||||
key: ${{ matrix.os }}-php-${{ matrix.php }}-symfony-${{ matrix.symfony }}-composer-${{ hashFiles('**/composer.json', '**/composer.lock') }}
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
<a href="https://packagist.org/packages/pestphp/pest"><img alt="Latest Version" src="https://img.shields.io/packagist/v/pestphp/pest"></a>
|
||||
<a href="https://packagist.org/packages/pestphp/pest"><img alt="License" src="https://img.shields.io/packagist/l/pestphp/pest"></a>
|
||||
<a href="https://whyphp.dev"><img src="https://img.shields.io/badge/Why_PHP-in_2026-7A86E8?style=flat-square&labelColor=18181b" alt="Why PHP in 2026"></a>
|
||||
<a href="https://youtube.com/@nunomaduro?sub_confirmation=1"><img alt="YouTube Channel Subscribers" src="https://img.shields.io/youtube/channel/subscribers/UCO_hYZF2gb_CyG5sA7ArlGg?style=flat&label=youtube&color=brightgreen"></a>
|
||||
</p>
|
||||
</p>
|
||||
|
||||
|
||||
@ -6,7 +6,7 @@ namespace Pest;
|
||||
|
||||
function version(): string
|
||||
{
|
||||
return '4.6.3';
|
||||
return '4.7.0';
|
||||
}
|
||||
|
||||
function testDirectory(string $file = ''): string
|
||||
|
||||
@ -10,6 +10,7 @@ use Pest\Contracts\Plugins\HandlesArguments;
|
||||
use Pest\Contracts\Plugins\Terminable;
|
||||
use Pest\Exceptions\NoAffectedTestsFound;
|
||||
use Pest\Panic;
|
||||
use Pest\Plugins\Concerns\HandleArguments;
|
||||
use Pest\Plugins\Tia\BaselineSync;
|
||||
use Pest\Plugins\Tia\ChangedFiles;
|
||||
use Pest\Plugins\Tia\Contracts\State;
|
||||
@ -36,7 +37,7 @@ use Symfony\Component\Process\Process;
|
||||
*/
|
||||
final class Tia implements AddsOutput, HandlesArguments, Terminable
|
||||
{
|
||||
use Concerns\HandleArguments;
|
||||
use HandleArguments;
|
||||
|
||||
private const string OPTION = '--tia';
|
||||
|
||||
@ -52,6 +53,8 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
|
||||
|
||||
private const string BASELINED_OPTION = '--baselined';
|
||||
|
||||
private const string BASELINE_PATH_OPTION = '--baseline';
|
||||
|
||||
private const string ENV_TIA = 'PEST_TIA';
|
||||
|
||||
private const string ENV_FILTERED = 'PEST_TIA_FILTERED';
|
||||
@ -138,6 +141,12 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
|
||||
|
||||
private bool $filteredMode = false;
|
||||
|
||||
private ?string $driftLabel = null;
|
||||
|
||||
private ?string $driftDetails = null;
|
||||
|
||||
private ?string $freshGraphReason = null;
|
||||
|
||||
public function __construct(
|
||||
private readonly OutputInterface $output,
|
||||
private readonly Recorder $recorder,
|
||||
@ -230,7 +239,7 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
|
||||
}
|
||||
|
||||
/**
|
||||
* Mirrors {@see \Pest\Plugins\Concerns\HandleArguments::hasArgument()} for
|
||||
* Mirrors {@see HandleArguments::hasArgument()} for
|
||||
* use from static contexts — matches both `--flag` and `--flag=value`.
|
||||
*
|
||||
* @param array<int, string> $arguments
|
||||
@ -309,6 +318,12 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
|
||||
*/
|
||||
public function handleArguments(array $arguments): array
|
||||
{
|
||||
if ($this->hasArgument(self::BASELINE_PATH_OPTION, $arguments)) {
|
||||
$this->output->writeln(Storage::tempDir(TestSuite::getInstance()->rootPath));
|
||||
|
||||
exit(0);
|
||||
}
|
||||
|
||||
$isWorker = Parallel::isWorker();
|
||||
$recordingGlobal = $isWorker && (string) Parallel::getGlobal(self::RECORDING_GLOBAL) === '1';
|
||||
$replayingGlobal = $isWorker && (string) Parallel::getGlobal(self::REPLAYING_GLOBAL) === '1';
|
||||
@ -322,7 +337,8 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
|
||||
&& (! $watchPatterns->isLocally() || Environment::name() === Environment::LOCAL);
|
||||
$enabled = ! $disabled && ($cliEnabled || $alwaysEnabled);
|
||||
$this->filteredMode = ($this->hasArgument(self::FILTERED_OPTION, $arguments) || self::envFlagEnabled(self::ENV_FILTERED) || $watchPatterns->isFiltered())
|
||||
&& ! $this->hasExplicitPathArgument($arguments);
|
||||
&& ! $this->hasExplicitPathArgument($arguments)
|
||||
&& ! $this->coverageReportActive();
|
||||
$freshRequested = $this->hasArgument(self::FRESH_OPTION, $arguments);
|
||||
$this->forceRefetch = $this->hasArgument(self::REFETCH_OPTION, $arguments);
|
||||
|
||||
@ -557,10 +573,7 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
|
||||
if (! Fingerprint::structuralMatches($stored, $current)) {
|
||||
$drift = Fingerprint::structuralDrift($stored, $current);
|
||||
|
||||
$this->renderChild(sprintf(
|
||||
'Graph structure outdated (%s).',
|
||||
$this->formatStructuralDrift($drift),
|
||||
));
|
||||
$this->driftLabel = $this->formatStructuralDrift($drift);
|
||||
|
||||
if (in_array('composer_lock', $drift, true)) {
|
||||
$branchSha = $graph->recordedAtSha($this->branch);
|
||||
@ -570,7 +583,7 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
|
||||
$branchSha,
|
||||
);
|
||||
if ($summary !== '') {
|
||||
$this->renderChild($summary);
|
||||
$this->driftDetails = $summary;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -654,6 +667,10 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
|
||||
}
|
||||
|
||||
if ($this->piggybackCoverage && ! $this->state->exists(self::KEY_COVERAGE_CACHE)) {
|
||||
if ($graph instanceof Graph && $this->driftLabel === null) {
|
||||
$this->freshGraphReason = 'recording coverage baseline';
|
||||
}
|
||||
|
||||
return $this->enterRecordMode($arguments);
|
||||
}
|
||||
|
||||
@ -919,7 +936,7 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
|
||||
}
|
||||
|
||||
$this->renderChild(sprintf(
|
||||
'TIA mode enabled / %d affected test file%s%s.',
|
||||
'Experimental TIA mode enabled / %d affected test file%s%s.',
|
||||
count($affected),
|
||||
count($affected) === 1 ? '' : 's',
|
||||
$reasons === [] ? '' : ' ('.implode(', ', $reasons).')',
|
||||
@ -980,7 +997,7 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
|
||||
}
|
||||
|
||||
$this->output->writeln('');
|
||||
$this->renderChild('TIA mode enabled / fresh graph.');
|
||||
$this->renderFreshGraph();
|
||||
|
||||
return $arguments;
|
||||
}
|
||||
@ -989,7 +1006,7 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
|
||||
$this->recordingActive = true;
|
||||
|
||||
$this->output->writeln('');
|
||||
$this->renderChild('TIA mode enabled / fresh graph.');
|
||||
$this->renderFreshGraph();
|
||||
|
||||
return $arguments;
|
||||
}
|
||||
@ -1002,11 +1019,32 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
|
||||
return $arguments;
|
||||
}
|
||||
|
||||
private function renderFreshGraph(): void
|
||||
{
|
||||
$headline = 'Experimental TIA mode enabled / fresh graph';
|
||||
|
||||
if ($this->driftLabel !== null) {
|
||||
$headline .= sprintf(' (%s changed)', $this->driftLabel);
|
||||
} elseif ($this->freshGraphReason !== null) {
|
||||
$headline .= sprintf(' (%s)', $this->freshGraphReason);
|
||||
} else {
|
||||
$headline .= '.';
|
||||
}
|
||||
|
||||
$this->renderChild($headline);
|
||||
|
||||
if ($this->driftDetails !== null) {
|
||||
foreach (explode(', ', $this->driftDetails) as $detail) {
|
||||
$this->output->writeln(sprintf(' <fg=gray>%s</>', $detail));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function emitCoverageDriverMissing(): void
|
||||
{
|
||||
$this->renderBadge('WARN', 'No coverage driver is available — skipped.');
|
||||
$this->renderChild('Needs ext-pcov or Xdebug with coverage mode enabled to record the dependency graph.');
|
||||
$this->renderChild('Install or enable one and rerun with --tia.');
|
||||
$this->output->writeln('');
|
||||
|
||||
$this->renderChild('Running in TIA mode, however TIA as skipped as it needs Needs ext-pcov or Xdebug.');
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1324,7 +1362,16 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
|
||||
/** @var ResultCollector $collector */
|
||||
$collector = Container::getInstance()->get(ResultCollector::class);
|
||||
|
||||
foreach ($collector->all() as $testId => $result) {
|
||||
$results = $collector->all();
|
||||
$touchedFiles = [];
|
||||
|
||||
foreach ($results as $testId => $result) {
|
||||
$file = $result['file'] ?? null;
|
||||
|
||||
if (is_string($file) && $file !== '') {
|
||||
$touchedFiles[$file] = true;
|
||||
}
|
||||
|
||||
$graph->setResult(
|
||||
$this->branch,
|
||||
$testId,
|
||||
@ -1332,10 +1379,12 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
|
||||
$result['message'],
|
||||
$result['time'],
|
||||
$result['assertions'],
|
||||
$result['file'] ?? null,
|
||||
$file,
|
||||
);
|
||||
}
|
||||
|
||||
$graph->pruneStaleResults($this->branch, array_keys($touchedFiles), array_keys($results));
|
||||
|
||||
$collector->reset();
|
||||
}
|
||||
|
||||
@ -1358,6 +1407,8 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
|
||||
return;
|
||||
}
|
||||
|
||||
$touchedFiles = [];
|
||||
|
||||
foreach ($results as $testId => $result) {
|
||||
$file = $result['file'] ?? null;
|
||||
|
||||
@ -1365,6 +1416,10 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
|
||||
$file = $this->resolveFailedTestFile($testId);
|
||||
}
|
||||
|
||||
if (is_string($file) && $file !== '') {
|
||||
$touchedFiles[$file] = true;
|
||||
}
|
||||
|
||||
$graph->setResult(
|
||||
$this->branch,
|
||||
$testId,
|
||||
@ -1376,6 +1431,8 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
|
||||
);
|
||||
}
|
||||
|
||||
$graph->pruneStaleResults($this->branch, array_keys($touchedFiles), array_keys($results));
|
||||
|
||||
$this->saveGraph($graph);
|
||||
$collector->reset();
|
||||
}
|
||||
@ -1541,7 +1598,7 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
|
||||
}
|
||||
|
||||
if (! Fingerprint::structuralMatches($fetched->fingerprint(), $current)) {
|
||||
$this->renderBadge('WARN', 'Fetched baseline still drifts — discarding.');
|
||||
$this->output->writeln(' <fg=gray> However, baseline still drifts — discarding.</>');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -109,11 +109,6 @@ final readonly class BaselineSync
|
||||
|
||||
$this->clearCooldown();
|
||||
|
||||
$this->renderBadge('INFO', sprintf(
|
||||
'Baseline ready (%s).',
|
||||
$this->formatSize($payload['sizeOnDisk']),
|
||||
));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -244,7 +239,7 @@ final readonly class BaselineSync
|
||||
if (is_file($runCacheDir.DIRECTORY_SEPARATOR.self::GRAPH_ASSET)) {
|
||||
@touch($runCacheDir);
|
||||
|
||||
$this->renderBadge('INFO', sprintf(
|
||||
$this->renderChild(sprintf(
|
||||
'Using cached baseline from %s (run %s).',
|
||||
$repo,
|
||||
$runId,
|
||||
@ -312,14 +307,15 @@ final readonly class BaselineSync
|
||||
{
|
||||
$artifactSize = $this->artifactSize($repo, $runId);
|
||||
|
||||
$this->renderBadge('INFO', $artifactSize !== null
|
||||
$this->output->writeln('');
|
||||
$this->renderChild($artifactSize !== null
|
||||
? sprintf(
|
||||
'Fetching baseline (%s) from %s…',
|
||||
'Downloading TIA baseline (%s) from %s…',
|
||||
$this->formatSize($artifactSize),
|
||||
$repo,
|
||||
)
|
||||
: sprintf(
|
||||
'Fetching baseline from %s…',
|
||||
'Downloading TIA baseline from %s…',
|
||||
$repo,
|
||||
));
|
||||
|
||||
@ -333,10 +329,11 @@ final readonly class BaselineSync
|
||||
$process->start();
|
||||
|
||||
$startedAt = microtime(true);
|
||||
$tick = 0;
|
||||
|
||||
while ($process->isRunning()) {
|
||||
$this->renderDownloadProgress($runCacheDir, $artifactSize, $startedAt);
|
||||
usleep(250_000);
|
||||
$this->renderDownloadProgress($startedAt, $tick++);
|
||||
usleep(120_000);
|
||||
}
|
||||
|
||||
$process->wait();
|
||||
@ -402,30 +399,18 @@ final readonly class BaselineSync
|
||||
return is_numeric($size) ? (int) $size : null;
|
||||
}
|
||||
|
||||
private function renderDownloadProgress(string $dir, ?int $totalBytes, float $startedAt): void
|
||||
private function renderDownloadProgress(float $startedAt, int $tick): void
|
||||
{
|
||||
$current = $this->dirSize($dir);
|
||||
$elapsed = max(0.001, microtime(true) - $startedAt);
|
||||
$speed = (int) ($current / $elapsed);
|
||||
static $frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
||||
|
||||
if ($totalBytes !== null && $totalBytes > 0) {
|
||||
$percent = min(99, (int) floor(($current / $totalBytes) * 100));
|
||||
$message = sprintf(
|
||||
' <fg=cyan>Downloading</> %s / %s (%d%%, %s/s)',
|
||||
$this->formatSize($current),
|
||||
$this->formatSize($totalBytes),
|
||||
$percent,
|
||||
$this->formatSize($speed),
|
||||
);
|
||||
} else {
|
||||
$message = sprintf(
|
||||
' <fg=cyan>Downloading</> %s (%s/s)',
|
||||
$this->formatSize($current),
|
||||
$this->formatSize($speed),
|
||||
);
|
||||
}
|
||||
$elapsed = max(0.0, microtime(true) - $startedAt);
|
||||
$frame = $frames[$tick % count($frames)];
|
||||
|
||||
$this->output->write("\r\033[K".$message);
|
||||
$this->output->write(sprintf(
|
||||
"\r\033[K <fg=gray>%s %.1fs elapsed</>",
|
||||
$frame,
|
||||
$elapsed,
|
||||
));
|
||||
}
|
||||
|
||||
private function clearProgressLine(): void
|
||||
|
||||
@ -31,6 +31,7 @@ final class CoverageMerger
|
||||
$current = self::requireCoverage($reportPath);
|
||||
|
||||
if ($current instanceof CodeCoverage) {
|
||||
self::primeUncoveredFiles($current);
|
||||
$state->write(Tia::KEY_COVERAGE_CACHE, self::compress(serialize($current)));
|
||||
}
|
||||
|
||||
@ -52,6 +53,9 @@ final class CoverageMerger
|
||||
return;
|
||||
}
|
||||
|
||||
self::primeUncoveredFiles($cached);
|
||||
self::primeUncoveredFiles($current);
|
||||
|
||||
self::stripCurrentTestsFromCached($cached, $current);
|
||||
|
||||
$cached->merge($current);
|
||||
@ -65,6 +69,11 @@ final class CoverageMerger
|
||||
$state->write(Tia::KEY_COVERAGE_CACHE, self::compress($serialised));
|
||||
}
|
||||
|
||||
private static function primeUncoveredFiles(CodeCoverage $coverage): void
|
||||
{
|
||||
$coverage->getData(false);
|
||||
}
|
||||
|
||||
private static function compress(string $bytes): string
|
||||
{
|
||||
$compressed = @gzencode($bytes);
|
||||
|
||||
@ -4,12 +4,14 @@ declare(strict_types=1);
|
||||
|
||||
namespace Pest\Plugins\Tia;
|
||||
|
||||
use Symfony\Component\Finder\Finder;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final readonly class Fingerprint
|
||||
{
|
||||
private const int SCHEMA_VERSION = 15;
|
||||
private const int SCHEMA_VERSION = 17;
|
||||
|
||||
/**
|
||||
* @return array{
|
||||
@ -23,15 +25,15 @@ final readonly class Fingerprint
|
||||
'structural' => [
|
||||
'schema' => self::SCHEMA_VERSION,
|
||||
'composer_lock' => self::composerLockHash($projectRoot),
|
||||
'phpunit_xml' => self::hashIfExists($projectRoot.'/phpunit.xml'),
|
||||
'phpunit_xml_dist' => self::hashIfExists($projectRoot.'/phpunit.xml.dist'),
|
||||
'pest_factory' => self::contentHashOrNull(__DIR__.'/../../Factories/TestCaseFactory.php'),
|
||||
'pest_method_factory' => self::contentHashOrNull(__DIR__.'/../../Factories/TestCaseMethodFactory.php'),
|
||||
'phpunit_xml' => self::trackedHash($projectRoot, 'phpunit.xml'),
|
||||
'phpunit_xml_dist' => self::trackedHash($projectRoot, 'phpunit.xml.dist'),
|
||||
// 'pest_factory' => self::contentHashOrNull(__DIR__.'/../../Factories/TestCaseFactory.php'),
|
||||
// 'pest_method_factory' => self::contentHashOrNull(__DIR__.'/../../Factories/TestCaseMethodFactory.php'),
|
||||
'vite_config' => self::viteConfigHash($projectRoot),
|
||||
'package_json' => self::packageJsonHash($projectRoot),
|
||||
// 'package_json' => self::packageJsonHash($projectRoot),
|
||||
'package_lock' => self::packageLockHash($projectRoot),
|
||||
'js_config' => self::jsConfigHash($projectRoot),
|
||||
'composer_json' => self::composerJsonHash($projectRoot),
|
||||
// 'composer_json' => self::composerJsonHash($projectRoot),
|
||||
],
|
||||
'environmental' => [
|
||||
'php_minor' => PHP_MAJOR_VERSION,
|
||||
@ -160,6 +162,10 @@ final readonly class Fingerprint
|
||||
$parts = [];
|
||||
|
||||
foreach (JsModuleGraph::VITE_CONFIG_NAMES as $name) {
|
||||
if (! self::isTrackedByGit($projectRoot, $name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$hash = self::contentHashOrNull($projectRoot.'/'.$name);
|
||||
|
||||
if ($hash !== null) {
|
||||
@ -175,6 +181,10 @@ final readonly class Fingerprint
|
||||
$parts = [];
|
||||
|
||||
foreach (['tsconfig.json', 'tsconfig.app.json', 'jsconfig.json'] as $name) {
|
||||
if (! self::isTrackedByGit($projectRoot, $name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$hash = self::hashIfExists($projectRoot.'/'.$name);
|
||||
|
||||
if ($hash !== null) {
|
||||
@ -185,52 +195,9 @@ final readonly class Fingerprint
|
||||
return $parts === [] ? null : hash('xxh128', implode("\n", $parts));
|
||||
}
|
||||
|
||||
private static function packageJsonHash(string $projectRoot): ?string
|
||||
{
|
||||
$path = $projectRoot.'/package.json';
|
||||
|
||||
if (! is_file($path)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$raw = @file_get_contents($path);
|
||||
|
||||
if ($raw === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$data = json_decode($raw, true);
|
||||
|
||||
if (! is_array($data)) {
|
||||
$hash = @hash_file('xxh128', $path);
|
||||
|
||||
return $hash === false ? null : $hash;
|
||||
}
|
||||
|
||||
$relevant = [
|
||||
'type' => $data['type'] ?? null,
|
||||
'packageManager' => $data['packageManager'] ?? null,
|
||||
'dependencies' => $data['dependencies'] ?? null,
|
||||
'devDependencies' => $data['devDependencies'] ?? null,
|
||||
'optionalDependencies' => $data['optionalDependencies'] ?? null,
|
||||
'peerDependencies' => $data['peerDependencies'] ?? null,
|
||||
'overrides' => $data['overrides'] ?? null,
|
||||
'resolutions' => $data['resolutions'] ?? null,
|
||||
'imports' => $data['imports'] ?? null,
|
||||
'exports' => $data['exports'] ?? null,
|
||||
'browser' => $data['browser'] ?? null,
|
||||
];
|
||||
|
||||
self::sortRecursively($relevant);
|
||||
|
||||
$json = json_encode($relevant);
|
||||
|
||||
return $json === false ? null : hash('xxh128', $json);
|
||||
}
|
||||
|
||||
private static function composerLockHash(string $projectRoot): ?string
|
||||
{
|
||||
return self::hashIfExists($projectRoot.'/composer.lock');
|
||||
return self::trackedHash($projectRoot, 'composer.lock');
|
||||
}
|
||||
|
||||
private static function packageLockHash(string $projectRoot): ?string
|
||||
@ -238,7 +205,7 @@ final readonly class Fingerprint
|
||||
$parts = [];
|
||||
|
||||
foreach (['package-lock.json', 'pnpm-lock.yaml', 'yarn.lock', 'bun.lock', 'bun.lockb'] as $name) {
|
||||
$hash = self::hashIfExists($projectRoot.'/'.$name);
|
||||
$hash = self::trackedHash($projectRoot, $name);
|
||||
|
||||
if ($hash !== null) {
|
||||
$parts[] = $name.':'.$hash;
|
||||
@ -248,68 +215,47 @@ final readonly class Fingerprint
|
||||
return $parts === [] ? null : hash('xxh128', implode("\n", $parts));
|
||||
}
|
||||
|
||||
private static function composerJsonHash(string $projectRoot): ?string
|
||||
private static function trackedHash(string $projectRoot, string $relativePath): ?string
|
||||
{
|
||||
$path = $projectRoot.'/composer.json';
|
||||
|
||||
if (! is_file($path)) {
|
||||
if (! self::isTrackedByGit($projectRoot, $relativePath)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$raw = @file_get_contents($path);
|
||||
|
||||
if ($raw === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$data = json_decode($raw, true);
|
||||
|
||||
if (! is_array($data)) {
|
||||
$hash = @hash_file('xxh128', $path);
|
||||
|
||||
return $hash === false ? null : $hash;
|
||||
}
|
||||
|
||||
$config = is_array($data['config'] ?? null) ? $data['config'] : [];
|
||||
$relevantConfig = array_intersect_key($config, [
|
||||
'platform' => true,
|
||||
'allow-plugins' => true,
|
||||
]);
|
||||
|
||||
$relevant = [
|
||||
'autoload' => $data['autoload'] ?? null,
|
||||
'autoload-dev' => $data['autoload-dev'] ?? null,
|
||||
'require' => $data['require'] ?? null,
|
||||
'require-dev' => $data['require-dev'] ?? null,
|
||||
'extra' => $data['extra'] ?? null,
|
||||
'repositories' => $data['repositories'] ?? null,
|
||||
'minimum-stability' => $data['minimum-stability'] ?? null,
|
||||
'prefer-stable' => $data['prefer-stable'] ?? null,
|
||||
'config' => $relevantConfig === [] ? null : $relevantConfig,
|
||||
];
|
||||
|
||||
self::sortRecursively($relevant);
|
||||
|
||||
$json = json_encode($relevant);
|
||||
|
||||
return $json === false ? null : hash('xxh128', $json);
|
||||
return self::hashIfExists($projectRoot.'/'.$relativePath);
|
||||
}
|
||||
|
||||
private static function sortRecursively(mixed &$value): void
|
||||
/**
|
||||
* Returns true when the file exists and is not gitignored.
|
||||
*
|
||||
* Gitignored lockfiles (e.g. `package-lock.json` excluded from the repo)
|
||||
* regenerate per-machine with OS-specific optional deps, which would
|
||||
* otherwise force a fingerprint mismatch on every fetched baseline.
|
||||
*/
|
||||
private static function isTrackedByGit(string $projectRoot, string $relativePath): bool
|
||||
{
|
||||
if (! is_array($value)) {
|
||||
return;
|
||||
if (! is_file($projectRoot.'/'.$relativePath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$isAssoc = ! array_is_list($value);
|
||||
static $cache = [];
|
||||
|
||||
if ($isAssoc) {
|
||||
ksort($value);
|
||||
$key = $projectRoot."\0".$relativePath;
|
||||
|
||||
if (isset($cache[$key])) {
|
||||
return $cache[$key];
|
||||
}
|
||||
|
||||
foreach ($value as &$child) {
|
||||
self::sortRecursively($child);
|
||||
if (! is_dir($projectRoot.'/.git') && ! is_file($projectRoot.'/.git')) {
|
||||
return $cache[$key] = true;
|
||||
}
|
||||
|
||||
$finder = (new Finder)
|
||||
->in($projectRoot)
|
||||
->depth('== 0')
|
||||
->name($relativePath)
|
||||
->ignoreVCSIgnored(true);
|
||||
|
||||
return $cache[$key] = $finder->hasResults();
|
||||
}
|
||||
|
||||
private static function contentHashOrNull(string $path): ?string
|
||||
|
||||
@ -1321,6 +1321,51 @@ final class Graph
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prune baseline result entries whose test files were just executed but whose
|
||||
* test IDs are no longer present (e.g. the test method was removed or renamed).
|
||||
*
|
||||
* @param array<int, string> $touchedFiles Absolute or project-relative paths.
|
||||
* @param array<int, string> $keepTestIds Test IDs that produced a result this run.
|
||||
*/
|
||||
public function pruneStaleResults(string $branch, array $touchedFiles, array $keepTestIds): void
|
||||
{
|
||||
if (! isset($this->baselines[$branch]['results'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$touched = [];
|
||||
foreach ($touchedFiles as $file) {
|
||||
$rel = $this->relative($file);
|
||||
|
||||
if ($rel !== null) {
|
||||
$touched[$rel] = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ($touched === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
$keep = array_fill_keys($keepTestIds, true);
|
||||
|
||||
foreach ($this->baselines[$branch]['results'] as $testId => $result) {
|
||||
$file = $result['file'] ?? null;
|
||||
if (! is_string($file)) {
|
||||
continue;
|
||||
}
|
||||
if (! isset($touched[$file])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isset($keep[$testId])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
unset($this->baselines[$branch]['results'][$testId]);
|
||||
}
|
||||
}
|
||||
|
||||
public static function decode(string $json, string $projectRoot): ?self
|
||||
{
|
||||
$data = json_decode($json, true);
|
||||
|
||||
@ -24,6 +24,9 @@ final class PcovRestarter implements Restarter
|
||||
}
|
||||
|
||||
if (getenv(self::ENV_RESTARTED) === '1') {
|
||||
putenv(self::ENV_RESTARTED);
|
||||
unset($_ENV[self::ENV_RESTARTED]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
|
||||
Pest Testing Framework 4.6.3.
|
||||
Pest Testing Framework 4.7.0.
|
||||
|
||||
USAGE: pest <file> [options]
|
||||
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
|
||||
Pest Testing Framework 4.6.3.
|
||||
Pest Testing Framework 4.7.0.
|
||||
|
||||
|
||||
@ -1,28 +1,56 @@
|
||||
##teamcity[testSuiteStarted name='Tests/tests/Failure' locationHint='pest_qn://tests/.tests/Failure.php' flowId='1234']
|
||||
##teamcity[testCount count='8' flowId='1234']
|
||||
##teamcity[testSuiteStarted name='Tests/tests/Failure' locationHint='pest_qn://tests/.tests/Failure.php' flowId='1234']
|
||||
##teamcity[testCount count='8' flowId='1234']
|
||||
##teamcity[testStarted name='it can fail with comparison' locationHint='pest_qn://tests/.tests/Failure.php::it can fail with comparison' flowId='1234']
|
||||
##teamcity[testStarted name='it can fail with comparison' locationHint='pest_qn://tests/.tests/Failure.php::it can fail with comparison' flowId='1234']
|
||||
##teamcity[testFailed name='it can fail with comparison' message='Failed asserting that true matches expected false.' details='at tests/.tests/Failure.php:6' type='comparisonFailure' actual='true' expected='false' flowId='1234']
|
||||
##teamcity[testFailed name='it can fail with comparison' message='Failed asserting that true matches expected false.' details='at tests/.tests/Failure.php:6' type='comparisonFailure' actual='true' expected='false' flowId='1234']
|
||||
##teamcity[testFinished name='it can fail with comparison' duration='100000' flowId='1234']
|
||||
##teamcity[testFinished name='it can fail with comparison' duration='100000' flowId='1234']
|
||||
##teamcity[testStarted name='it can be ignored because of no assertions' locationHint='pest_qn://tests/.tests/Failure.php::it can be ignored because of no assertions' flowId='1234']
|
||||
##teamcity[testStarted name='it can be ignored because of no assertions' locationHint='pest_qn://tests/.tests/Failure.php::it can be ignored because of no assertions' flowId='1234']
|
||||
##teamcity[testIgnored name='it can be ignored because of no assertions' message='This test did not perform any assertions' details='' flowId='1234']
|
||||
##teamcity[testIgnored name='it can be ignored because of no assertions' message='This test did not perform any assertions' details='' flowId='1234']
|
||||
##teamcity[testFinished name='it can be ignored because of no assertions' duration='100000' flowId='1234']
|
||||
##teamcity[testFinished name='it can be ignored because of no assertions' duration='100000' flowId='1234']
|
||||
##teamcity[testStarted name='it can be ignored because it is skipped' locationHint='pest_qn://tests/.tests/Failure.php::it can be ignored because it is skipped' flowId='1234']
|
||||
##teamcity[testStarted name='it can be ignored because it is skipped' locationHint='pest_qn://tests/.tests/Failure.php::it can be ignored because it is skipped' flowId='1234']
|
||||
##teamcity[testIgnored name='it can be ignored because it is skipped' message='This test was ignored.' details='' flowId='1234']
|
||||
##teamcity[testIgnored name='it can be ignored because it is skipped' message='This test was ignored.' details='' flowId='1234']
|
||||
##teamcity[testFinished name='it can be ignored because it is skipped' duration='100000' flowId='1234']
|
||||
##teamcity[testFinished name='it can be ignored because it is skipped' duration='100000' flowId='1234']
|
||||
##teamcity[testStarted name='it can fail' locationHint='pest_qn://tests/.tests/Failure.php::it can fail' flowId='1234']
|
||||
##teamcity[testStarted name='it can fail' locationHint='pest_qn://tests/.tests/Failure.php::it can fail' flowId='1234']
|
||||
##teamcity[testFailed name='it can fail' message='oh noo' details='at tests/.tests/Failure.php:18' flowId='1234']
|
||||
##teamcity[testFailed name='it can fail' message='oh noo' details='at tests/.tests/Failure.php:18' flowId='1234']
|
||||
##teamcity[testFinished name='it can fail' duration='100000' flowId='1234']
|
||||
##teamcity[testFinished name='it can fail' duration='100000' flowId='1234']
|
||||
##teamcity[testStarted name='it throws exception' locationHint='pest_qn://tests/.tests/Failure.php::it throws exception' flowId='1234']
|
||||
##teamcity[testStarted name='it throws exception' locationHint='pest_qn://tests/.tests/Failure.php::it throws exception' flowId='1234']
|
||||
##teamcity[testFailed name='it throws exception' message='Exception: test error' details='at tests/.tests/Failure.php:22' flowId='1234']
|
||||
##teamcity[testFailed name='it throws exception' message='Exception: test error' details='at tests/.tests/Failure.php:22' flowId='1234']
|
||||
##teamcity[testFinished name='it throws exception' duration='100000' flowId='1234']
|
||||
##teamcity[testFinished name='it throws exception' duration='100000' flowId='1234']
|
||||
##teamcity[testStarted name='it is not done yet' locationHint='pest_qn://tests/.tests/Failure.php::it is not done yet' flowId='1234']
|
||||
##teamcity[testStarted name='it is not done yet' locationHint='pest_qn://tests/.tests/Failure.php::it is not done yet' flowId='1234']
|
||||
##teamcity[testFinished name='it is not done yet' duration='100000' flowId='1234']
|
||||
##teamcity[testFinished name='it is not done yet' duration='100000' flowId='1234']
|
||||
##teamcity[testStarted name='build this one.' locationHint='pest_qn://tests/.tests/Failure.php::build this one.' flowId='1234']
|
||||
##teamcity[testStarted name='build this one.' locationHint='pest_qn://tests/.tests/Failure.php::build this one.' flowId='1234']
|
||||
##teamcity[testFinished name='build this one.' duration='100000' flowId='1234']
|
||||
##teamcity[testFinished name='build this one.' duration='100000' flowId='1234']
|
||||
##teamcity[testStarted name='it is passing' locationHint='pest_qn://tests/.tests/Failure.php::it is passing' flowId='1234']
|
||||
##teamcity[testStarted name='it is passing' locationHint='pest_qn://tests/.tests/Failure.php::it is passing' flowId='1234']
|
||||
##teamcity[testFinished name='it is passing' duration='100000' flowId='1234']
|
||||
##teamcity[testFinished name='it is passing' duration='100000' flowId='1234']
|
||||
##teamcity[testSuiteFinished name='Tests/tests/Failure' flowId='1234']
|
||||
##teamcity[testSuiteFinished name='Tests/tests/Failure' flowId='1234']
|
||||
|
||||
[90mTests:[39m [31;1m3 failed[39;22m[90m,[39m[39m [39m[33;1m1 risky[39;22m[90m,[39m[39m [39m[36;1m2 todos[39;22m[90m,[39m[39m [39m[33;1m1 skipped[39;22m[90m,[39m[39m [39m[32;1m1 passed[39;22m[90m (3 assertions)[39m
|
||||
[90mDuration:[39m [39m1.00s[39m
|
||||
|
||||
|
||||
[90mTests:[39m [31;1m3 failed[39;22m[90m,[39m[39m [39m[33;1m1 risky[39;22m[90m,[39m[39m [39m[36;1m2 todos[39;22m[90m,[39m[39m [39m[33;1m1 skipped[39;22m[90m,[39m[39m [39m[32;1m1 passed[39;22m[90m (3 assertions)[39m
|
||||
[90mDuration:[39m [39m1.00s[39m
|
||||
|
||||
|
||||
@ -1,19 +1,38 @@
|
||||
##teamcity[testSuiteStarted name='Tests/tests/SuccessOnly' locationHint='pest_qn://tests/.tests/SuccessOnly.php' flowId='1234']
|
||||
##teamcity[testCount count='4' flowId='1234']
|
||||
##teamcity[testSuiteStarted name='Tests/tests/SuccessOnly' locationHint='pest_qn://tests/.tests/SuccessOnly.php' flowId='1234']
|
||||
##teamcity[testCount count='4' flowId='1234']
|
||||
##teamcity[testStarted name='it can pass with comparison' locationHint='pest_qn://tests/.tests/SuccessOnly.php::it can pass with comparison' flowId='1234']
|
||||
##teamcity[testStarted name='it can pass with comparison' locationHint='pest_qn://tests/.tests/SuccessOnly.php::it can pass with comparison' flowId='1234']
|
||||
##teamcity[testFinished name='it can pass with comparison' duration='100000' flowId='1234']
|
||||
##teamcity[testFinished name='it can pass with comparison' duration='100000' flowId='1234']
|
||||
##teamcity[testStarted name='can also pass' locationHint='pest_qn://tests/.tests/SuccessOnly.php::can also pass' flowId='1234']
|
||||
##teamcity[testStarted name='can also pass' locationHint='pest_qn://tests/.tests/SuccessOnly.php::can also pass' flowId='1234']
|
||||
##teamcity[testFinished name='can also pass' duration='100000' flowId='1234']
|
||||
##teamcity[testFinished name='can also pass' duration='100000' flowId='1234']
|
||||
##teamcity[testSuiteStarted name='can pass with dataset' locationHint='pest_qn://tests/.tests/SuccessOnly.php::can pass with dataset' flowId='1234']
|
||||
##teamcity[testSuiteStarted name='can pass with dataset' locationHint='pest_qn://tests/.tests/SuccessOnly.php::can pass with dataset' flowId='1234']
|
||||
##teamcity[testStarted name='can pass with dataset with data set "(true)"' locationHint='pest_qn://tests/.tests/SuccessOnly.php::can pass with dataset with data set "(true)"' flowId='1234']
|
||||
##teamcity[testStarted name='can pass with dataset with data set "(true)"' locationHint='pest_qn://tests/.tests/SuccessOnly.php::can pass with dataset with data set "(true)"' flowId='1234']
|
||||
##teamcity[testFinished name='can pass with dataset with data set "(true)"' duration='100000' flowId='1234']
|
||||
##teamcity[testFinished name='can pass with dataset with data set "(true)"' duration='100000' flowId='1234']
|
||||
##teamcity[testSuiteFinished name='can pass with dataset' flowId='1234']
|
||||
##teamcity[testSuiteFinished name='can pass with dataset' flowId='1234']
|
||||
##teamcity[testSuiteStarted name='`block` → can pass with dataset in describe block' locationHint='pest_qn://tests/.tests/SuccessOnly.php::`block` → can pass with dataset in describe block' flowId='1234']
|
||||
##teamcity[testSuiteStarted name='`block` → can pass with dataset in describe block' locationHint='pest_qn://tests/.tests/SuccessOnly.php::`block` → can pass with dataset in describe block' flowId='1234']
|
||||
##teamcity[testStarted name='`block` → can pass with dataset in describe block with data set "(1)"' locationHint='pest_qn://tests/.tests/SuccessOnly.php::`block` → can pass with dataset in describe block with data set "(1)"' flowId='1234']
|
||||
##teamcity[testStarted name='`block` → can pass with dataset in describe block with data set "(1)"' locationHint='pest_qn://tests/.tests/SuccessOnly.php::`block` → can pass with dataset in describe block with data set "(1)"' flowId='1234']
|
||||
##teamcity[testFinished name='`block` → can pass with dataset in describe block with data set "(1)"' duration='100000' flowId='1234']
|
||||
##teamcity[testFinished name='`block` → can pass with dataset in describe block with data set "(1)"' duration='100000' flowId='1234']
|
||||
##teamcity[testSuiteFinished name='`block` → can pass with dataset in describe block' flowId='1234']
|
||||
##teamcity[testSuiteFinished name='`block` → can pass with dataset in describe block' flowId='1234']
|
||||
##teamcity[testSuiteFinished name='Tests/tests/SuccessOnly' flowId='1234']
|
||||
##teamcity[testSuiteFinished name='Tests/tests/SuccessOnly' flowId='1234']
|
||||
|
||||
[90mTests:[39m [32;1m4 passed[39;22m[90m (4 assertions)[39m
|
||||
[90mDuration:[39m [39m1.00s[39m
|
||||
|
||||
|
||||
[90mTests:[39m [32;1m4 passed[39;22m[90m (4 assertions)[39m
|
||||
[90mDuration:[39m [39m1.00s[39m
|
||||
|
||||
|
||||
@ -1716,6 +1716,43 @@
|
||||
PASS Tests\Unit\Plugins\Retry
|
||||
✓ it orders by defects and stop on defects if when --retry is used
|
||||
|
||||
PASS Tests\Unit\Plugins\Tia\ContentHash
|
||||
✓ of() → it returns false when file does not exist
|
||||
✓ of() → it hashes an existing file
|
||||
✓ PHP files → it produces the same hash regardless of whitespace differences
|
||||
✓ PHP files → it ignores single-line comments
|
||||
✓ PHP files → it ignores hash-style comments
|
||||
✓ PHP files → it ignores multi-line comments
|
||||
✓ PHP files → it ignores doc comments
|
||||
✓ PHP files → it detects code changes
|
||||
✓ PHP files → it preserves whitespace inside string literals
|
||||
✓ PHP files → it treats variable renames as a change
|
||||
✓ PHP files → it falls back to a raw hash for unparseable PHP
|
||||
✓ PHP files → it is case-insensitive on the file extension
|
||||
✓ Blade files → it strips blade comments
|
||||
✓ Blade files → it strips multi-line blade comments
|
||||
✓ Blade files → it collapses whitespace
|
||||
✓ Blade files → it detects content changes
|
||||
✓ Blade files → it keeps blade directives intact
|
||||
✓ Blade files → it does not use the PHP tokenizer for blade files
|
||||
✓ JavaScript-like files → it strips line comments
|
||||
✓ JavaScript-like files → it strips block comments on their own lines
|
||||
✓ JavaScript-like files → it collapses whitespace
|
||||
✓ JavaScript-like files → it detects code changes
|
||||
✓ JavaScript-like files → it does not strip inline trailing comments
|
||||
✓ JavaScript-like files → it applies the same rules to .ts files
|
||||
✓ JavaScript-like files → it applies the same rules to .tsx files
|
||||
✓ JavaScript-like files → it applies the same rules to .jsx files
|
||||
✓ JavaScript-like files → it applies the same rules to .vue files
|
||||
✓ JavaScript-like files → it applies the same rules to .svelte files
|
||||
✓ JavaScript-like files → it applies the same rules to .mjs, .cjs, and .mts files
|
||||
✓ unknown extensions → it hashes the raw content for unknown extensions
|
||||
✓ unknown extensions → it does not normalise whitespace for unknown extensions
|
||||
✓ unknown extensions → it does not strip comments for unknown extensions
|
||||
✓ unknown extensions → it hashes files with no extension as raw content
|
||||
✓ output format → it returns a 32-character hex xxh128 hash
|
||||
✓ output format → it returns a stable hash for empty content
|
||||
|
||||
PASS Tests\Unit\Preset
|
||||
✓ preset invalid name
|
||||
✓ preset → myFramework
|
||||
@ -1901,4 +1938,4 @@
|
||||
✓ pass with dataset with ('my-datas-set-value')
|
||||
✓ within describe → pass with dataset with ('my-datas-set-value')
|
||||
|
||||
Tests: 2 deprecated, 4 warnings, 5 incomplete, 2 notices, 40 todos, 35 skipped, 1294 passed (2971 assertions)
|
||||
Tests: 2 deprecated, 4 warnings, 5 incomplete, 2 notices, 40 todos, 35 skipped, 1329 passed (3010 assertions)
|
||||
@ -16,6 +16,7 @@ $run = function () {
|
||||
|
||||
test('parallel', function () use ($run) {
|
||||
$output = $run('--exclude-group=integration');
|
||||
$output = implode("\n", array_slice(explode("\n", $output), -10));
|
||||
|
||||
if (getenv('REBUILD_SNAPSHOTS')) {
|
||||
preg_match('/Tests:\s+(.+\(\d+ assertions\))/', $output, $matches);
|
||||
@ -23,13 +24,13 @@ test('parallel', function () use ($run) {
|
||||
$file = file_get_contents(__FILE__);
|
||||
$file = preg_replace(
|
||||
'/\$expected = \'.*?\';/',
|
||||
"\$expected = '2 deprecated, 4 warnings, 5 incomplete, 3 notices, 40 todos, 27 skipped, 1278 passed (2920 assertions)';",
|
||||
"\$expected = '2 deprecated, 4 warnings, 5 incomplete, 3 notices, 40 todos, 27 skipped, 1313 passed (2959 assertions)';",
|
||||
$file,
|
||||
);
|
||||
file_put_contents(__FILE__, $file);
|
||||
}
|
||||
|
||||
$expected = '2 deprecated, 4 warnings, 5 incomplete, 3 notices, 40 todos, 27 skipped, 1278 passed (2920 assertions)';
|
||||
$expected = '2 deprecated, 4 warnings, 5 incomplete, 3 notices, 40 todos, 27 skipped, 1313 passed (2959 assertions)';
|
||||
|
||||
expect($output)
|
||||
->toContain("Tests: {$expected}")
|
||||
|
||||
Reference in New Issue
Block a user