mirror of
https://github.com/pestphp/pest.git
synced 2026-09-05 22:33:35 +02:00
fix: snapshot key on retry / repeat
This commit is contained in:
@@ -344,6 +344,8 @@ trait Testable
|
||||
|
||||
$this->tearDown();
|
||||
|
||||
TestSuite::getInstance()->snapshots->forget();
|
||||
|
||||
Closure::bind(fn (): array => $this->mockObjects = [], $this, TestCase::class)();
|
||||
|
||||
foreach (array_keys(array_diff_key(get_object_vars($this), $initialProperties)) as $property) {
|
||||
|
||||
+40
-1
@@ -34,6 +34,7 @@ use Pest\Support\Reflection;
|
||||
use PHPUnit\Architecture\Elements\ObjectDescription;
|
||||
use PHPUnit\Framework\ExpectationFailedException;
|
||||
use ReflectionEnum;
|
||||
use ReflectionFunction;
|
||||
use ReflectionMethod;
|
||||
use ReflectionProperty;
|
||||
|
||||
@@ -329,8 +330,9 @@ final class Expectation
|
||||
}
|
||||
|
||||
$closure = $this->getExpectationClosure($method);
|
||||
$reflectionClosure = new \ReflectionFunction($closure);
|
||||
$reflectionClosure = new ReflectionFunction($closure);
|
||||
$expectation = $reflectionClosure->getClosureThis();
|
||||
$parameters = $this->positionalParameters($reflectionClosure, $parameters);
|
||||
|
||||
if ($reflectionClosure->getReturnType()?->__toString() === ArchExpectation::class) {
|
||||
return $closure(...$parameters);
|
||||
@@ -346,6 +348,43 @@ final class Expectation
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<array-key, mixed> $parameters
|
||||
* @return array<array-key, mixed>
|
||||
*/
|
||||
private function positionalParameters(ReflectionFunction $closure, array $parameters): array
|
||||
{
|
||||
if ($parameters === [] || array_is_list($parameters)) {
|
||||
return $parameters;
|
||||
}
|
||||
|
||||
$positional = [];
|
||||
|
||||
foreach ($closure->getParameters() as $position => $parameter) {
|
||||
if ($parameter->isVariadic()) {
|
||||
return $parameters;
|
||||
}
|
||||
|
||||
$name = $parameter->getName();
|
||||
|
||||
if (array_key_exists($position, $parameters)) {
|
||||
$positional[] = $parameters[$position];
|
||||
|
||||
unset($parameters[$position]);
|
||||
} elseif (array_key_exists($name, $parameters)) {
|
||||
$positional[] = $parameters[$name];
|
||||
|
||||
unset($parameters[$name]);
|
||||
} elseif ($parameter->isDefaultValueAvailable()) {
|
||||
$positional[] = $parameter->getDefaultValue();
|
||||
} else {
|
||||
return $parameters;
|
||||
}
|
||||
}
|
||||
|
||||
return $parameters === [] ? $positional : $parameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ExpectationNotFound
|
||||
*/
|
||||
|
||||
+28
-30
@@ -14,7 +14,7 @@ use InvalidArgumentException;
|
||||
use JsonSerializable;
|
||||
use Pest\Exceptions\InvalidExpectationValue;
|
||||
use Pest\Matchers\Any;
|
||||
use Pest\Plugins\Snapshot;
|
||||
use Pest\Plugins\Snapshot as SnapshotPlugin;
|
||||
use Pest\Support\Arr;
|
||||
use Pest\Support\Exporter;
|
||||
use Pest\Support\NullClosure;
|
||||
@@ -712,10 +712,11 @@ final class Expectation
|
||||
/**
|
||||
* @return self<TValue>
|
||||
*/
|
||||
public function toMatchSnapshot(string $message = ''): self
|
||||
public function toMatchSnapshot(string $message = '', ?string $as = null): self
|
||||
{
|
||||
$snapshots = TestSuite::getInstance()->snapshots;
|
||||
$snapshots->startNewExpectation();
|
||||
|
||||
$snapshot = $as === null ? $snapshots->next() : $snapshots->named($as);
|
||||
|
||||
$testCase = TestSuite::getInstance()->test;
|
||||
assert($testCase instanceof TestCase);
|
||||
@@ -733,39 +734,36 @@ final class Expectation
|
||||
default => InvalidExpectationValue::expected('array|object|string'),
|
||||
};
|
||||
|
||||
if (! $snapshots->has()) {
|
||||
if (! Snapshot::shouldCreateMissingSnapshots()) {
|
||||
$filename = $snapshots->filename();
|
||||
|
||||
Assert::fail($message === '' ? "Snapshot is missing at [$filename]. Run Pest with --update-snapshots to create it." : $message);
|
||||
if (! $snapshot->exists()) {
|
||||
if (! SnapshotPlugin::shouldCreateMissingSnapshots()) {
|
||||
Assert::fail($message === '' ? "Snapshot is missing at [{$snapshot->path()}]. Run Pest with --update-snapshots to create it." : $message);
|
||||
}
|
||||
|
||||
$filename = $snapshots->save($string);
|
||||
$snapshot->write($string);
|
||||
|
||||
TestSuite::getInstance()->registerSnapshotChange("Snapshot created at [$filename]");
|
||||
} else {
|
||||
[$filename, $content] = $snapshots->get();
|
||||
TestSuite::getInstance()->registerSnapshotChange("Snapshot created at [{$snapshot->path()}]");
|
||||
|
||||
$normalizedContent = strtr($content, ["\r\n" => "\n", "\r" => "\n"]);
|
||||
$normalizedString = strtr($string, ["\r\n" => "\n", "\r" => "\n"]);
|
||||
|
||||
if (Snapshot::$updateSnapshots && $normalizedContent !== $normalizedString) {
|
||||
$snapshots->save($string);
|
||||
|
||||
TestSuite::getInstance()->registerSnapshotChange("Snapshot updated at [$filename]");
|
||||
} else {
|
||||
if (Snapshot::$updateSnapshots) {
|
||||
TestSuite::getInstance()->registerSnapshotChange("Snapshot unchanged at [$filename]");
|
||||
}
|
||||
|
||||
Assert::assertSame(
|
||||
$normalizedContent,
|
||||
$normalizedString,
|
||||
$message === '' ? "Failed asserting that the string value matches its snapshot ($filename)." : $message
|
||||
);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
if (SnapshotPlugin::$updateSnapshots) {
|
||||
if (! $snapshot->matches($string)) {
|
||||
$snapshot->write($string);
|
||||
|
||||
TestSuite::getInstance()->registerSnapshotChange("Snapshot updated at [{$snapshot->path()}]");
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
TestSuite::getInstance()->registerSnapshotChange("Snapshot unchanged at [{$snapshot->path()}]");
|
||||
}
|
||||
|
||||
Assert::assertSame(
|
||||
$snapshot->normalize($snapshot->read()),
|
||||
$snapshot->normalize($string),
|
||||
$message === '' ? "Failed asserting that the string value matches its snapshot ({$snapshot->path()})." : $message
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
@@ -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"]);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user