fix: snapshot key on retry / repeat

This commit is contained in:
nuno maduro
2026-08-24 09:31:34 +01:00
parent 39cbdcd52b
commit b8c2265d66
30 changed files with 479 additions and 145 deletions
+68
View File
@@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
namespace Pest\Repositories;
use Pest\Exceptions\ShouldNotHappen;
/**
* @internal
*/
final readonly class Snapshot
{
public function __construct(
private string $filename,
private string $basePath,
) {}
public function exists(): bool
{
return file_exists($this->filename);
}
public function path(): string
{
return str_replace($this->basePath, '', $this->filename);
}
/**
* @throws ShouldNotHappen
*/
public function read(): string
{
$contents = file_get_contents($this->filename);
if ($contents === false) {
throw ShouldNotHappen::fromMessage('Snapshot file could not be read.');
}
return $contents;
}
public function write(string $contents): self
{
$directory = dirname($this->filename);
if (! is_dir($directory)) {
@mkdir($directory, 0755, true);
}
file_put_contents($this->filename, $contents);
return $this;
}
/**
* @throws ShouldNotHappen
*/
public function matches(string $value): bool
{
return $this->normalize($this->read()) === $this->normalize($value);
}
public function normalize(string $value): string
{
return strtr($value, ["\r\n" => "\n", "\r" => "\n"]);
}
}
+87 -59
View File
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Pest\Repositories;
use InvalidArgumentException;
use Pest\Exceptions\ShouldNotHappen;
use Pest\TestSuite;
@@ -12,8 +13,9 @@ use Pest\TestSuite;
*/
final class SnapshotRepository
{
/** @var array<string, int> */
private static array $expectationsCounter = [];
private static ?string $key = null;
private static int $ordinal = 0;
public function __construct(
private readonly string $rootPath,
@@ -21,47 +23,39 @@ final class SnapshotRepository
private readonly string $snapshotsPath,
) {}
public function has(): bool
public function current(): Snapshot
{
return file_exists($this->getSnapshotFilename());
$this->synchronize();
return $this->snapshot(self::$ordinal > 1 ? '__'.self::$ordinal : '');
}
/**
* @return array{0: string, 1: string}
*
* @throws ShouldNotHappen
*/
public function get(): array
public function next(): Snapshot
{
$contents = file_get_contents($snapshotFilename = $this->getSnapshotFilename());
$this->synchronize();
if ($contents === false) {
throw ShouldNotHappen::fromMessage('Snapshot file could not be read.');
self::$ordinal++;
return $this->current();
}
public function named(string $name): Snapshot
{
$this->synchronize();
$suffix = trim((string) preg_replace('/[^\w-]+/', '_', $name), '_');
if ($suffix === '') {
throw new InvalidArgumentException('The snapshot name must contain at least one alphanumeric character.');
}
$snapshot = str_replace(dirname($this->testsPath).'/', '', $snapshotFilename);
return [$snapshot, $contents];
return $this->snapshot('__'.$suffix);
}
public function save(string $snapshot): string
public function forget(): void
{
$snapshotFilename = $this->getSnapshotFilename();
$directory = dirname($snapshotFilename);
if (! is_dir($directory)) {
@mkdir($directory, 0755, true);
}
file_put_contents($snapshotFilename, $snapshot);
return $this->filename();
}
public function filename(): string
{
return str_replace(dirname($this->testsPath).'/', '', $this->getSnapshotFilename());
self::$key = null;
self::$ordinal = 0;
}
public function flush(): void
@@ -92,47 +86,81 @@ final class SnapshotRepository
}
}
private function getSnapshotFilename(): string
private function snapshot(string $suffix): Snapshot
{
$testFile = TestSuite::getInstance()->getFilename();
if (str_starts_with($testFile, $this->testsPath)) {
$startPath = $this->testsPath;
} else {
$startPath = $this->rootPath;
}
$startPath = str_starts_with($testFile, $this->testsPath) ? $this->testsPath : $this->rootPath;
$relativePath = substr($testFile, strlen($startPath));
$relativePath = substr($relativePath, 0, (int) strrpos($relativePath, '.'));
$description = TestSuite::getInstance()->getDescription();
return new Snapshot(
sprintf(
'%s/%s%s.snap',
$this->testsPath.'/'.$this->snapshotsPath.$relativePath,
TestSuite::getInstance()->getDescription(),
$suffix,
),
dirname($this->testsPath).'/',
);
}
if ($this->getCurrentSnapshotCounter() > 1) {
$description .= '__'.$this->getCurrentSnapshotCounter();
private function synchronize(): void
{
$key = TestSuite::getInstance()->getFilename().'###'.TestSuite::getInstance()->getDescription();
if (self::$key === $key) {
return;
}
return sprintf('%s/%s.snap', $this->testsPath.'/'.$this->snapshotsPath.$relativePath, $description);
}
private function getCurrentSnapshotKey(): string
{
return TestSuite::getInstance()->getFilename().'###'.TestSuite::getInstance()->getDescription();
}
private function getCurrentSnapshotCounter(): int
{
return self::$expectationsCounter[$this->getCurrentSnapshotKey()] ?? 0;
self::$key = $key;
self::$ordinal = 0;
}
/**
* @deprecated Use `next` and `current` instead.
*/
public function startNewExpectation(): void
{
$key = $this->getCurrentSnapshotKey();
$this->next();
}
if (! isset(self::$expectationsCounter[$key])) {
self::$expectationsCounter[$key] = 0;
}
/**
* @deprecated Use `current` instead.
*/
public function has(): bool
{
return $this->current()->exists();
}
self::$expectationsCounter[$key]++;
/**
* @deprecated Use `current` instead.
*
* @return array{0: string, 1: string}
*
* @throws ShouldNotHappen
*/
public function get(): array
{
$snapshot = $this->current();
return [$snapshot->path(), $snapshot->read()];
}
/**
* @deprecated Use `current` instead.
*/
public function save(string $snapshot): string
{
return $this->current()->write($snapshot)->path();
}
/**
* @deprecated Use `current` instead.
*/
public function filename(): string
{
return $this->current()->path();
}
}