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:
@@ -14,3 +14,6 @@ coverage.xml
|
|||||||
.vscode/
|
.vscode/
|
||||||
.STREAM.md
|
.STREAM.md
|
||||||
|
|
||||||
|
|
||||||
|
/.claude/skills/
|
||||||
|
/.agents/skills/
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# The intention of this project
|
||||||
|
|
||||||
|
Write the intention of the project here. Say what the project does and who uses it.
|
||||||
|
|
||||||
|
Give the command that installs the dependencies, the command that runs the tests and the command that starts the program. Name each directory and give its function.
|
||||||
|
|
||||||
|
Write a decision that one directory needs in the `AGENTS.md` file of that directory.
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
1a51f3f6c4eef45e6995bb5ac9a82cf3f23ccad0c68cc4f957796bb9fc198f58 .agents/skills/deps-upgrade/SKILL.md
|
||||||
|
984c7232ae1681ebba980ede2a0543db9a850dd2406990cdfa011384f929e981 .agents/skills/deps-upgrade/agents/openai.yaml
|
||||||
|
cb04cd147c57791f740b37a9b486851858388867ba4b32b4af4454ca28ba0b5f .agents/skills/learn/SKILL.md
|
||||||
|
6a9515e9c8b30b33743ac5b1beb797d683ae7b1acaee50f4dd3d0a445da735c6 .agents/skills/learn/agents/openai.yaml
|
||||||
|
1a51f3f6c4eef45e6995bb5ac9a82cf3f23ccad0c68cc4f957796bb9fc198f58 .claude/skills/deps-upgrade/SKILL.md
|
||||||
|
984c7232ae1681ebba980ede2a0543db9a850dd2406990cdfa011384f929e981 .claude/skills/deps-upgrade/agents/openai.yaml
|
||||||
|
cb04cd147c57791f740b37a9b486851858388867ba4b32b4af4454ca28ba0b5f .claude/skills/learn/SKILL.md
|
||||||
|
6a9515e9c8b30b33743ac5b1beb797d683ae7b1acaee50f4dd3d0a445da735c6 .claude/skills/learn/agents/openai.yaml
|
||||||
|
c246ffcdee10cea23b94b1b1efa379beca374c672e8aefb54d52e8fc007f98cc AGENTS.md
|
||||||
|
336cc4fbf19beaada7ccf9986414fa91851a8d7a07dfb3ccbe800a69eed0ab49 CLAUDE.md
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
# The project
|
||||||
|
|
||||||
|
Read `.hod/PROJECT.md` first. It holds the intention of this project.
|
||||||
|
|
||||||
|
`hod` writes this file. Write no sentence in it, because `hod update` writes it again. Write the intention of the project in `.hod/PROJECT.md`. Write a rule in a file in `.hod/rules/`. Write a skill in a directory in `.hod/skills/`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. The reader of this file
|
||||||
|
|
||||||
|
This file is for a coding agent. No user reads it. Write an instruction that an agent obeys during its work, then stop. Do not write an introduction, a conclusion, an argument for a decision that the project made, or a sentence that no agent can obey. Give a reason only when the reason changes the next decision of the agent.
|
||||||
|
|
||||||
|
Start each rule with a verb in the imperative. Put the condition before the instruction. Give the exact path, the exact command and the exact name that the agent must use. If a rule needs a test, give the command that does the test.
|
||||||
|
|
||||||
|
The public files are different. `README.md`, the website, the release notes and the `description` in each manifest are for a user. Section 3 controls them.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. How to write for an agent
|
||||||
|
|
||||||
|
Write in [ASD-STE100](https://www.asd-ste100.org) Simplified Technical English: one instruction in one sentence, the imperative, the active voice, one meaning for one word, no contraction, and no synonym for variety. The standard holds the full rules. Do not copy them here. Four items keep their exact form: a quotation from a standard, an identifier in the code, a path or a command, and a name from a different supplier.
|
||||||
|
|
||||||
|
Write GitHub flavored Markdown. Put one `#` heading in a file. Write one paragraph on one line, thus a change stays small in `git diff`. Number the sections of a long file, thus a different file can point to "section 3". Put a path, a command and a name from the code in `code font`.
|
||||||
|
|
||||||
|
Write the intention of the project in `.hod/PROJECT.md`. Write a decision about one directory in the `AGENTS.md` file of that directory. If `.hod/PROJECT.md` and the `AGENTS.md` file of a directory disagree, obey `.hod/PROJECT.md` and correct the other file.
|
||||||
|
|
||||||
|
Write no new rule. This is the correct result of each task, and a task that adds a command, a module or a workflow does not change it. A rule needs a fault that happened: name the decision that you made wrong, or name the decision that these files refused to give you. A wrong change that you imagine is not a fault, thus write no rule to protect the code that you write today.
|
||||||
|
|
||||||
|
A fact that you learned during one task is not a rule. Use it in that task, then write no sentence for it.
|
||||||
|
|
||||||
|
`.hod/PROJECT.md` holds two items: the intention of the project, and a reason that the code cannot hold. Write no sentence below that level.
|
||||||
|
|
||||||
|
Write no sentence for a fact that an agent finds when it reads the code. The cost of that read does not change the answer: a fact that four files hold is in the code. Put the fact in the code first, because a test stops the agent that breaks it and a sentence here does not.
|
||||||
|
|
||||||
|
Ask the user before you write a rule. Give the fault, then call the skill `learn` after the user agrees. Keep the number of the rules: delete a rule file in the same change, or ask the user to accept one more rule. A correction from the user is a rule and needs no question: write it in a file before you continue the work.
|
||||||
|
|
||||||
|
Give no new part of the program its own rule, because the code of that part holds its design. Delete text that follows the order of a file of source code: that text describes the file, and the file describes itself. Replace an old rule. Do not write a second rule near it. Delete a rule that the project does not obey. Do not keep a record of what the project stopped doing, in a file or in a directory. Git holds the history.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. How to write the public text
|
||||||
|
|
||||||
|
These rules control `README.md`, the website, the release notes, the announcement and the `description` in each manifest. Sections 1 and 2 do not control these files.
|
||||||
|
|
||||||
|
The voice is calm and certain. Say what the project does, then stop. Do not say that the project is fast, simple, powerful or intelligent. Show the command and let the reader form that opinion. Cut each superlative. Cut each sentence that a competitor can also write about itself.
|
||||||
|
|
||||||
|
Write for one person and call that person "you". Put the result first and the mechanism second. Keep the sentences short, and let one sentence stand alone when it carries the idea. A demonstration is the strongest argument: one command in a terminal is worth one paragraph of adjectives.
|
||||||
|
|
||||||
|
The care is in the small parts. The alignment of the output, the words in an error message, the space around the text on the page: the reader feels this work before the reader reads one sentence. The project is for people who enjoy their work, thus the text is warm. Put one dry joke on one page, at the most. Do not explain it.
|
||||||
|
|
||||||
|
Obey three limits. Do not promise a feature that does not exist today. Do not give a number without its source. Do not name a different product to make a comparison.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. How to write the code
|
||||||
|
|
||||||
|
These rules control each file of source code and each manifest in this repository. Sections 1, 2 and 3 do not control these files.
|
||||||
|
|
||||||
|
Write no comment. Delete each comment that you find in a file that you change. Turn off the lint that asks for a documentation comment, because that lint asks you to say the name of an item again.
|
||||||
|
|
||||||
|
Put the intention in the code. Give each item a name that says what the item does. Give each value a type that makes a wrong value impossible. Split a long function into two functions with two names. A name and a type stay correct, and a comment does not.
|
||||||
|
|
||||||
|
Write a reason that the code cannot hold in a file in `.hod/rules/`, with the path of the code. Do not write it above the code.
|
||||||
|
|
||||||
|
When a library reads the text of a comment as data, such as the description of a command on a help screen, write that text in an attribute or a field of that library instead.
|
||||||
|
|
||||||
|
A tool that writes a comment into a file that it owns keeps that comment. Do not delete it: the tool fails until it writes the comment again.
|
||||||
@@ -1,34 +1 @@
|
|||||||
# CLAUDE.md
|
@AGENTS.md
|
||||||
|
|
||||||
**Do not edit this file.** Agents must never add, remove, or reword anything here. If a change seems needed, say so and let a human do it.
|
|
||||||
|
|
||||||
## Ask before testing
|
|
||||||
|
|
||||||
When asked to review code or build a feature, do not run the test suite and do not write new tests. Make the change, report it, then ask the user whether tests should be added — describing the tests you have in mind — and wait for the user to confirm.
|
|
||||||
|
|
||||||
Two reasons this matters here: the suite takes minutes, and `tests/.snapshots/success.txt` plus the tally in `tests/Visual/Parallel.php` encode the whole suite's result, so a single added test breaks both.
|
|
||||||
|
|
||||||
Once the user confirms:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
composer test:unit # fast, excludes the visual group
|
|
||||||
composer test:integration # visual and snapshot tests
|
|
||||||
composer test # everything CI runs, in CI's order
|
|
||||||
composer update:snapshots # only when a test was added or removed
|
|
||||||
```
|
|
||||||
|
|
||||||
## TIA scenario tests
|
|
||||||
|
|
||||||
`tests/Features/Tia/*` scaffold a throwaway git project, run a real `pest` subprocess against it, and diff the TIA graph it wrote. They exist because TIA's contract is about what a run *writes* — replay, branch keys, and the COMPLETE / RESULTS-ONLY / HARD-SUPPRESSED tiers are invisible to ordinary assertions, and every case used to be measured by hand against a playground app.
|
|
||||||
|
|
||||||
Add one whenever a change touches branch resolution, replay, filtered mode, or the write tiers. How:
|
|
||||||
|
|
||||||
- `Project::make('master')` scaffolds; `seed('master')` writes a graph and sentinels every cached result (`time=9.999`, `assertions=42`) so any rewrite shows up.
|
|
||||||
- `$project->pest('--tia', …)` runs it; `$project->delta()` compares against that snapshot. `writtenCount()` is the discriminator — `0` means "replayed", not "wrote the same values". `mutateGraph()` bends one entry; overlays in `tests/Fixtures/Tia/overlays/<name>/` supply a different `tests/Pest.php`.
|
|
||||||
- Keep expectations driver-independent: a cold recording run needs pcov/xdebug and behaves differently without one. Seed a graph instead of recording one.
|
|
||||||
|
|
||||||
Run them by file (a directory argument finds nothing) or by `--filter`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
php bin/pest tests/Features/Tia/PartialRunWriteTier.php
|
|
||||||
```
|
|
||||||
|
|||||||
+3
-3
@@ -26,7 +26,7 @@
|
|||||||
"pestphp/pest-plugin-mutate": "^5.0.2",
|
"pestphp/pest-plugin-mutate": "^5.0.2",
|
||||||
"pestphp/pest-plugin-profanity": "^5.0.0",
|
"pestphp/pest-plugin-profanity": "^5.0.0",
|
||||||
"phpunit/phpunit": "^13.3.0",
|
"phpunit/phpunit": "^13.3.0",
|
||||||
"symfony/process": "^8.1.0"
|
"symfony/process": "^8.1.5"
|
||||||
},
|
},
|
||||||
"conflict": {
|
"conflict": {
|
||||||
"filp/whoops": "<2.18.3",
|
"filp/whoops": "<2.18.3",
|
||||||
@@ -61,8 +61,8 @@
|
|||||||
"require-dev": {
|
"require-dev": {
|
||||||
"pestphp/pest-dev-tools": "^5.0.0",
|
"pestphp/pest-dev-tools": "^5.0.0",
|
||||||
"pestphp/pest-plugin-browser": "^5.0.1",
|
"pestphp/pest-plugin-browser": "^5.0.1",
|
||||||
"pestphp/pest-plugin-phpstan": "^5.0.2",
|
"pestphp/pest-plugin-phpstan": "^5.2.0",
|
||||||
"pestphp/pest-plugin-rector": "^5.0.3",
|
"pestphp/pest-plugin-rector": "^5.0.4",
|
||||||
"pestphp/pest-plugin-type-coverage": "^5.0.2",
|
"pestphp/pest-plugin-type-coverage": "^5.0.2",
|
||||||
"psy/psysh": "^0.12.24"
|
"psy/psysh": "^0.12.24"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -344,6 +344,8 @@ trait Testable
|
|||||||
|
|
||||||
$this->tearDown();
|
$this->tearDown();
|
||||||
|
|
||||||
|
TestSuite::getInstance()->snapshots->forget();
|
||||||
|
|
||||||
Closure::bind(fn (): array => $this->mockObjects = [], $this, TestCase::class)();
|
Closure::bind(fn (): array => $this->mockObjects = [], $this, TestCase::class)();
|
||||||
|
|
||||||
foreach (array_keys(array_diff_key(get_object_vars($this), $initialProperties)) as $property) {
|
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\Architecture\Elements\ObjectDescription;
|
||||||
use PHPUnit\Framework\ExpectationFailedException;
|
use PHPUnit\Framework\ExpectationFailedException;
|
||||||
use ReflectionEnum;
|
use ReflectionEnum;
|
||||||
|
use ReflectionFunction;
|
||||||
use ReflectionMethod;
|
use ReflectionMethod;
|
||||||
use ReflectionProperty;
|
use ReflectionProperty;
|
||||||
|
|
||||||
@@ -329,8 +330,9 @@ final class Expectation
|
|||||||
}
|
}
|
||||||
|
|
||||||
$closure = $this->getExpectationClosure($method);
|
$closure = $this->getExpectationClosure($method);
|
||||||
$reflectionClosure = new \ReflectionFunction($closure);
|
$reflectionClosure = new ReflectionFunction($closure);
|
||||||
$expectation = $reflectionClosure->getClosureThis();
|
$expectation = $reflectionClosure->getClosureThis();
|
||||||
|
$parameters = $this->positionalParameters($reflectionClosure, $parameters);
|
||||||
|
|
||||||
if ($reflectionClosure->getReturnType()?->__toString() === ArchExpectation::class) {
|
if ($reflectionClosure->getReturnType()?->__toString() === ArchExpectation::class) {
|
||||||
return $closure(...$parameters);
|
return $closure(...$parameters);
|
||||||
@@ -346,6 +348,43 @@ final class Expectation
|
|||||||
return $this;
|
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
|
* @throws ExpectationNotFound
|
||||||
*/
|
*/
|
||||||
|
|||||||
+28
-30
@@ -14,7 +14,7 @@ use InvalidArgumentException;
|
|||||||
use JsonSerializable;
|
use JsonSerializable;
|
||||||
use Pest\Exceptions\InvalidExpectationValue;
|
use Pest\Exceptions\InvalidExpectationValue;
|
||||||
use Pest\Matchers\Any;
|
use Pest\Matchers\Any;
|
||||||
use Pest\Plugins\Snapshot;
|
use Pest\Plugins\Snapshot as SnapshotPlugin;
|
||||||
use Pest\Support\Arr;
|
use Pest\Support\Arr;
|
||||||
use Pest\Support\Exporter;
|
use Pest\Support\Exporter;
|
||||||
use Pest\Support\NullClosure;
|
use Pest\Support\NullClosure;
|
||||||
@@ -712,10 +712,11 @@ final class Expectation
|
|||||||
/**
|
/**
|
||||||
* @return self<TValue>
|
* @return self<TValue>
|
||||||
*/
|
*/
|
||||||
public function toMatchSnapshot(string $message = ''): self
|
public function toMatchSnapshot(string $message = '', ?string $as = null): self
|
||||||
{
|
{
|
||||||
$snapshots = TestSuite::getInstance()->snapshots;
|
$snapshots = TestSuite::getInstance()->snapshots;
|
||||||
$snapshots->startNewExpectation();
|
|
||||||
|
$snapshot = $as === null ? $snapshots->next() : $snapshots->named($as);
|
||||||
|
|
||||||
$testCase = TestSuite::getInstance()->test;
|
$testCase = TestSuite::getInstance()->test;
|
||||||
assert($testCase instanceof TestCase);
|
assert($testCase instanceof TestCase);
|
||||||
@@ -733,39 +734,36 @@ final class Expectation
|
|||||||
default => InvalidExpectationValue::expected('array|object|string'),
|
default => InvalidExpectationValue::expected('array|object|string'),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (! $snapshots->has()) {
|
if (! $snapshot->exists()) {
|
||||||
if (! Snapshot::shouldCreateMissingSnapshots()) {
|
if (! SnapshotPlugin::shouldCreateMissingSnapshots()) {
|
||||||
$filename = $snapshots->filename();
|
Assert::fail($message === '' ? "Snapshot is missing at [{$snapshot->path()}]. Run Pest with --update-snapshots to create it." : $message);
|
||||||
|
|
||||||
Assert::fail($message === '' ? "Snapshot is missing at [$filename]. Run Pest with --update-snapshots to create it." : $message);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$filename = $snapshots->save($string);
|
$snapshot->write($string);
|
||||||
|
|
||||||
TestSuite::getInstance()->registerSnapshotChange("Snapshot created at [$filename]");
|
TestSuite::getInstance()->registerSnapshotChange("Snapshot created at [{$snapshot->path()}]");
|
||||||
} else {
|
|
||||||
[$filename, $content] = $snapshots->get();
|
|
||||||
|
|
||||||
$normalizedContent = strtr($content, ["\r\n" => "\n", "\r" => "\n"]);
|
return $this;
|
||||||
$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
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
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;
|
namespace Pest\Repositories;
|
||||||
|
|
||||||
|
use InvalidArgumentException;
|
||||||
use Pest\Exceptions\ShouldNotHappen;
|
use Pest\Exceptions\ShouldNotHappen;
|
||||||
use Pest\TestSuite;
|
use Pest\TestSuite;
|
||||||
|
|
||||||
@@ -12,8 +13,9 @@ use Pest\TestSuite;
|
|||||||
*/
|
*/
|
||||||
final class SnapshotRepository
|
final class SnapshotRepository
|
||||||
{
|
{
|
||||||
/** @var array<string, int> */
|
private static ?string $key = null;
|
||||||
private static array $expectationsCounter = [];
|
|
||||||
|
private static int $ordinal = 0;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly string $rootPath,
|
private readonly string $rootPath,
|
||||||
@@ -21,47 +23,39 @@ final class SnapshotRepository
|
|||||||
private readonly string $snapshotsPath,
|
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 : '');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public function next(): Snapshot
|
||||||
* @return array{0: string, 1: string}
|
|
||||||
*
|
|
||||||
* @throws ShouldNotHappen
|
|
||||||
*/
|
|
||||||
public function get(): array
|
|
||||||
{
|
{
|
||||||
$contents = file_get_contents($snapshotFilename = $this->getSnapshotFilename());
|
$this->synchronize();
|
||||||
|
|
||||||
if ($contents === false) {
|
self::$ordinal++;
|
||||||
throw ShouldNotHappen::fromMessage('Snapshot file could not be read.');
|
|
||||||
|
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 $this->snapshot('__'.$suffix);
|
||||||
|
|
||||||
return [$snapshot, $contents];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function save(string $snapshot): string
|
public function forget(): void
|
||||||
{
|
{
|
||||||
$snapshotFilename = $this->getSnapshotFilename();
|
self::$key = null;
|
||||||
|
self::$ordinal = 0;
|
||||||
$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());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function flush(): void
|
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();
|
$testFile = TestSuite::getInstance()->getFilename();
|
||||||
|
|
||||||
if (str_starts_with($testFile, $this->testsPath)) {
|
$startPath = str_starts_with($testFile, $this->testsPath) ? $this->testsPath : $this->rootPath;
|
||||||
$startPath = $this->testsPath;
|
|
||||||
} else {
|
|
||||||
$startPath = $this->rootPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
$relativePath = substr($testFile, strlen($startPath));
|
$relativePath = substr($testFile, strlen($startPath));
|
||||||
|
|
||||||
$relativePath = substr($relativePath, 0, (int) strrpos($relativePath, '.'));
|
$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) {
|
private function synchronize(): void
|
||||||
$description .= '__'.$this->getCurrentSnapshotCounter();
|
{
|
||||||
|
$key = TestSuite::getInstance()->getFilename().'###'.TestSuite::getInstance()->getDescription();
|
||||||
|
|
||||||
|
if (self::$key === $key) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return sprintf('%s/%s.snap', $this->testsPath.'/'.$this->snapshotsPath.$relativePath, $description);
|
self::$key = $key;
|
||||||
}
|
self::$ordinal = 0;
|
||||||
|
|
||||||
private function getCurrentSnapshotKey(): string
|
|
||||||
{
|
|
||||||
return TestSuite::getInstance()->getFilename().'###'.TestSuite::getInstance()->getDescription();
|
|
||||||
}
|
|
||||||
|
|
||||||
private function getCurrentSnapshotCounter(): int
|
|
||||||
{
|
|
||||||
return self::$expectationsCounter[$this->getCurrentSnapshotKey()] ?? 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Use `next` and `current` instead.
|
||||||
|
*/
|
||||||
public function startNewExpectation(): void
|
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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ beforeEach(function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('pass with dataset', function ($data) {
|
test('pass with dataset', function ($data) {
|
||||||
TestSuite::getInstance()->snapshots->save($this->snapshotable);
|
TestSuite::getInstance()->snapshots->current()->write($this->snapshotable);
|
||||||
[$filename] = TestSuite::getInstance()->snapshots->get();
|
$filename = TestSuite::getInstance()->snapshots->current()->path();
|
||||||
|
|
||||||
expect($filename)->toStartWith('tests/.pest/snapshots-external/')
|
expect($filename)->toStartWith('tests/.pest/snapshots-external/')
|
||||||
->toEndWith('pass_with_dataset_with_data_set____my_datas_set_value___.snap')
|
->toEndWith('pass_with_dataset_with_data_set____my_datas_set_value___.snap')
|
||||||
@@ -25,8 +25,8 @@ test('pass with dataset', function ($data) {
|
|||||||
|
|
||||||
describe('within describe', function () {
|
describe('within describe', function () {
|
||||||
test('pass with dataset', function ($data) {
|
test('pass with dataset', function ($data) {
|
||||||
TestSuite::getInstance()->snapshots->save($this->snapshotable);
|
TestSuite::getInstance()->snapshots->current()->write($this->snapshotable);
|
||||||
[$filename] = TestSuite::getInstance()->snapshots->get();
|
$filename = TestSuite::getInstance()->snapshots->current()->path();
|
||||||
|
|
||||||
expect($filename)->toStartWith('tests/.pest/snapshots-external/')
|
expect($filename)->toStartWith('tests/.pest/snapshots-external/')
|
||||||
->toEndWith('pass_with_dataset_with_data_set____my_datas_set_value___.snap')
|
->toEndWith('pass_with_dataset_with_data_set____my_datas_set_value___.snap')
|
||||||
|
|||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
foo bar 1
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
foo bar 2
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
foo bar 1
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
foo bar 2
|
||||||
+7
@@ -0,0 +1,7 @@
|
|||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<h1>Snapshot</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
foo bar
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
foo bar
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
foo bar
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
foo bar
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
foo bar
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
foo bar
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
the same on every attempt
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
the same on every attempt
|
||||||
@@ -439,6 +439,7 @@
|
|||||||
✓ interceptor can be filter the expected parameter as well
|
✓ interceptor can be filter the expected parameter as well
|
||||||
✓ interceptor works with negated expectation
|
✓ interceptor works with negated expectation
|
||||||
✓ intercept can add new parameters to the expectation
|
✓ intercept can add new parameters to the expectation
|
||||||
|
✓ named arguments reach the pipes of an expectation
|
||||||
|
|
||||||
PASS Tests\Features\Expect\ray
|
PASS Tests\Features\Expect\ray
|
||||||
✓ ray calls do not fail when ray is not installed
|
✓ ray calls do not fail when ray is not installed
|
||||||
@@ -1122,6 +1123,17 @@
|
|||||||
✓ multiple snapshot expectations with repeat @ repetition 8 of 10
|
✓ multiple snapshot expectations with repeat @ repetition 8 of 10
|
||||||
✓ multiple snapshot expectations with repeat @ repetition 9 of 10
|
✓ multiple snapshot expectations with repeat @ repetition 9 of 10
|
||||||
✓ multiple snapshot expectations with repeat @ repetition 10 of 10
|
✓ multiple snapshot expectations with repeat @ repetition 10 of 10
|
||||||
|
✓ pass with named snapshot
|
||||||
|
✓ named snapshots do not depend on the order they are asserted in
|
||||||
|
✓ named snapshots do not consume the ordinal of the unnamed ones
|
||||||
|
✓ named snapshots require a name
|
||||||
|
✓ ordinal snapshots start over once forgotten
|
||||||
|
✓ ordinal snapshots start over on every repetition @ repetition 1 of 3
|
||||||
|
✓ ordinal snapshots start over on every repetition @ repetition 2 of 3
|
||||||
|
✓ ordinal snapshots start over on every repetition @ repetition 3 of 3
|
||||||
|
✓ snapshots of a repeated test are recorded per repetition @ repetition 1 of 3
|
||||||
|
✓ snapshots of a repeated test are recorded per repetition @ repetition 2 of 3
|
||||||
|
✓ snapshots of a repeated test are recorded per repetition @ repetition 3 of 3
|
||||||
|
|
||||||
PASS Tests\Features\Expect\toStartWith
|
PASS Tests\Features\Expect\toStartWith
|
||||||
✓ pass
|
✓ pass
|
||||||
@@ -1224,6 +1236,8 @@
|
|||||||
✓ it does not retry fails()
|
✓ it does not retry fails()
|
||||||
✓ it retries unexpected exceptions even with throws set
|
✓ it retries unexpected exceptions even with throws set
|
||||||
✓ it does not leak mock objects between retries
|
✓ it does not leak mock objects between retries
|
||||||
|
✓ it resolves the same snapshot ordinals on every retry
|
||||||
|
✓ it matches the same snapshot on every retry
|
||||||
✓ it does not stop retrying when snapshot changes are absent
|
✓ it does not stop retrying when snapshot changes are absent
|
||||||
✓ it does not leak dynamic properties between retries
|
✓ it does not leak dynamic properties between retries
|
||||||
! it clears output buffer between retries when expectOutputString is used → Creation of dynamic property PHPUnit\Framework\TestCase\OutputBuffer::$expectedString is deprecated
|
! it clears output buffer between retries when expectOutputString is used → Creation of dynamic property PHPUnit\Framework\TestCase\OutputBuffer::$expectedString is deprecated
|
||||||
@@ -2237,4 +2251,4 @@
|
|||||||
✓ pass with dataset with ('my-datas-set-value')
|
✓ pass with dataset with ('my-datas-set-value')
|
||||||
✓ within describe → 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, 1582 passed (3435 assertions)
|
Tests: 2 deprecated, 4 warnings, 5 incomplete, 2 notices, 40 todos, 35 skipped, 1596 passed (3481 assertions)
|
||||||
@@ -239,3 +239,14 @@ test('intercept can add new parameters to the expectation', function (): void {
|
|||||||
|
|
||||||
expect('Foo')->toBe('foo', $ignoreCase);
|
expect('Foo')->toBe('foo', $ignoreCase);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('named arguments reach the pipes of an expectation', function () use ($state): void {
|
||||||
|
$char = new Char('A');
|
||||||
|
|
||||||
|
$state->reset();
|
||||||
|
|
||||||
|
expect($char)->toBe(expected: new Char('A'))
|
||||||
|
->and($state)
|
||||||
|
->runCount->toHaveKey('char', 1)
|
||||||
|
->appliedCount->toHaveKey('char', 1);
|
||||||
|
});
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ beforeEach(function (): void {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('pass', function (): void {
|
test('pass', function (): void {
|
||||||
TestSuite::getInstance()->snapshots->save($this->snapshotable);
|
TestSuite::getInstance()->snapshots->current()->write($this->snapshotable);
|
||||||
|
|
||||||
expect($this->snapshotable)->toMatchSnapshot();
|
expect($this->snapshotable)->toMatchSnapshot();
|
||||||
});
|
});
|
||||||
@@ -39,7 +39,7 @@ test('pass using pipes', function (): void {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('pass with `__toString`', function (): void {
|
test('pass with `__toString`', function (): void {
|
||||||
TestSuite::getInstance()->snapshots->save($this->snapshotable);
|
TestSuite::getInstance()->snapshots->current()->write($this->snapshotable);
|
||||||
|
|
||||||
$object = new class($this->snapshotable)
|
$object = new class($this->snapshotable)
|
||||||
{
|
{
|
||||||
@@ -55,7 +55,7 @@ test('pass with `__toString`', function (): void {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('pass with `toString`', function (): void {
|
test('pass with `toString`', function (): void {
|
||||||
TestSuite::getInstance()->snapshots->save($this->snapshotable);
|
TestSuite::getInstance()->snapshots->current()->write($this->snapshotable);
|
||||||
|
|
||||||
$object = new class($this->snapshotable)
|
$object = new class($this->snapshotable)
|
||||||
{
|
{
|
||||||
@@ -71,8 +71,8 @@ test('pass with `toString`', function (): void {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('pass with dataset', function ($data): void {
|
test('pass with dataset', function ($data): void {
|
||||||
TestSuite::getInstance()->snapshots->save($this->snapshotable);
|
TestSuite::getInstance()->snapshots->current()->write($this->snapshotable);
|
||||||
[$filename] = TestSuite::getInstance()->snapshots->get();
|
$filename = TestSuite::getInstance()->snapshots->current()->path();
|
||||||
|
|
||||||
expect($filename)->toStartWith('tests/.pest/snapshots/')
|
expect($filename)->toStartWith('tests/.pest/snapshots/')
|
||||||
->toEndWith('pass_with_dataset_with_data_set____my_datas_set_value___.snap')
|
->toEndWith('pass_with_dataset_with_data_set____my_datas_set_value___.snap')
|
||||||
@@ -81,8 +81,8 @@ test('pass with dataset', function ($data): void {
|
|||||||
|
|
||||||
describe('within describe', function (): void {
|
describe('within describe', function (): void {
|
||||||
test('pass with dataset', function ($data): void {
|
test('pass with dataset', function ($data): void {
|
||||||
TestSuite::getInstance()->snapshots->save($this->snapshotable);
|
TestSuite::getInstance()->snapshots->current()->write($this->snapshotable);
|
||||||
[$filename] = TestSuite::getInstance()->snapshots->get();
|
$filename = TestSuite::getInstance()->snapshots->current()->path();
|
||||||
|
|
||||||
expect($filename)->toStartWith('tests/.pest/snapshots/')
|
expect($filename)->toStartWith('tests/.pest/snapshots/')
|
||||||
->toEndWith('pass_with_dataset_with_data_set____my_datas_set_value___.snap')
|
->toEndWith('pass_with_dataset_with_data_set____my_datas_set_value___.snap')
|
||||||
@@ -91,7 +91,7 @@ describe('within describe', function (): void {
|
|||||||
})->with(['my-datas-set-value']);
|
})->with(['my-datas-set-value']);
|
||||||
|
|
||||||
test('pass with `toArray`', function (): void {
|
test('pass with `toArray`', function (): void {
|
||||||
TestSuite::getInstance()->snapshots->save(json_encode(['key' => $this->snapshotable], JSON_PRETTY_PRINT));
|
TestSuite::getInstance()->snapshots->current()->write(json_encode(['key' => $this->snapshotable], JSON_PRETTY_PRINT));
|
||||||
|
|
||||||
$object = new class($this->snapshotable)
|
$object = new class($this->snapshotable)
|
||||||
{
|
{
|
||||||
@@ -109,7 +109,7 @@ test('pass with `toArray`', function (): void {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('pass with array', function (): void {
|
test('pass with array', function (): void {
|
||||||
TestSuite::getInstance()->snapshots->save(json_encode(['key' => $this->snapshotable], JSON_PRETTY_PRINT));
|
TestSuite::getInstance()->snapshots->current()->write(json_encode(['key' => $this->snapshotable], JSON_PRETTY_PRINT));
|
||||||
|
|
||||||
expect([
|
expect([
|
||||||
'key' => $this->snapshotable,
|
'key' => $this->snapshotable,
|
||||||
@@ -117,7 +117,7 @@ test('pass with array', function (): void {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('pass with `toSnapshot`', function (): void {
|
test('pass with `toSnapshot`', function (): void {
|
||||||
TestSuite::getInstance()->snapshots->save(json_encode(['key' => $this->snapshotable], JSON_PRETTY_PRINT));
|
TestSuite::getInstance()->snapshots->current()->write(json_encode(['key' => $this->snapshotable], JSON_PRETTY_PRINT));
|
||||||
|
|
||||||
$object = new class($this->snapshotable)
|
$object = new class($this->snapshotable)
|
||||||
{
|
{
|
||||||
@@ -135,7 +135,7 @@ test('pass with `toSnapshot`', function (): void {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('not failures', function (): void {
|
test('not failures', function (): void {
|
||||||
TestSuite::getInstance()->snapshots->save($this->snapshotable);
|
TestSuite::getInstance()->snapshots->current()->write($this->snapshotable);
|
||||||
|
|
||||||
expect($this->snapshotable)->not->toMatchSnapshot();
|
expect($this->snapshotable)->not->toMatchSnapshot();
|
||||||
})->throws(ExpectationFailedException::class);
|
})->throws(ExpectationFailedException::class);
|
||||||
@@ -161,3 +161,68 @@ test('multiple snapshot expectations with repeat', function (): void {
|
|||||||
expect('foo bar 1')->toMatchSnapshot()
|
expect('foo bar 1')->toMatchSnapshot()
|
||||||
->and('foo bar 2')->toMatchSnapshot();
|
->and('foo bar 2')->toMatchSnapshot();
|
||||||
})->repeat(10);
|
})->repeat(10);
|
||||||
|
|
||||||
|
test('pass with named snapshot', function (): void {
|
||||||
|
$snapshots = TestSuite::getInstance()->snapshots;
|
||||||
|
$snapshots->named('header')->write($this->snapshotable);
|
||||||
|
|
||||||
|
expect($snapshots->named('header')->path())
|
||||||
|
->toStartWith('tests/.pest/snapshots/')
|
||||||
|
->toEndWith('pass_with_named_snapshot__header.snap')
|
||||||
|
->and($this->snapshotable)->toMatchSnapshot(as: 'header');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('named snapshots do not depend on the order they are asserted in', function (): void {
|
||||||
|
$snapshots = TestSuite::getInstance()->snapshots;
|
||||||
|
$snapshots->named('first')->write('foo bar 1');
|
||||||
|
$snapshots->named('second')->write('foo bar 2');
|
||||||
|
|
||||||
|
expect('foo bar 2')->toMatchSnapshot(as: 'second')
|
||||||
|
->and('foo bar 1')->toMatchSnapshot(as: 'first');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('named snapshots do not consume the ordinal of the unnamed ones', function (): void {
|
||||||
|
$snapshots = TestSuite::getInstance()->snapshots;
|
||||||
|
$snapshots->current()->write('foo bar 1');
|
||||||
|
$snapshots->named('named')->write('foo bar 2');
|
||||||
|
|
||||||
|
expect('foo bar 1')->toMatchSnapshot()
|
||||||
|
->and('foo bar 2')->toMatchSnapshot(as: 'named')
|
||||||
|
->and($snapshots->current()->path())->toEndWith('named_snapshots_do_not_consume_the_ordinal_of_the_unnamed_ones.snap');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('named snapshots require a name', function (): void {
|
||||||
|
TestSuite::getInstance()->snapshots->named('_');
|
||||||
|
})->throws(InvalidArgumentException::class, 'The snapshot name must contain at least one alphanumeric character.');
|
||||||
|
|
||||||
|
test('ordinal snapshots start over once forgotten', function (): void {
|
||||||
|
$snapshots = TestSuite::getInstance()->snapshots;
|
||||||
|
|
||||||
|
$first = $snapshots->next()->path();
|
||||||
|
|
||||||
|
expect($snapshots->next()->path())->toEndWith('ordinal_snapshots_start_over_once_forgotten__2.snap');
|
||||||
|
|
||||||
|
$snapshots->forget();
|
||||||
|
|
||||||
|
expect($snapshots->next()->path())->toBe($first);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ordinal snapshots start over on every repetition', function (): void {
|
||||||
|
$snapshots = TestSuite::getInstance()->snapshots;
|
||||||
|
|
||||||
|
$first = $snapshots->next()->path();
|
||||||
|
|
||||||
|
expect($first)->toContain('ordinal_snapshots_start_over_on_every_repetition')
|
||||||
|
->and($snapshots->next()->path())->toBe(str_replace('.snap', '__2.snap', $first));
|
||||||
|
})->repeat(3);
|
||||||
|
|
||||||
|
test('snapshots of a repeated test are recorded per repetition', function (int $iteration): void {
|
||||||
|
$snapshots = TestSuite::getInstance()->snapshots;
|
||||||
|
$snapshots->current()->write('foo bar');
|
||||||
|
$snapshots->named('named')->write('foo bar');
|
||||||
|
|
||||||
|
expect('foo bar')->toMatchSnapshot()
|
||||||
|
->and($snapshots->current()->path())->toEndWith("_with_data_set___{$iteration}__.snap")
|
||||||
|
->and('foo bar')->toMatchSnapshot(as: 'named')
|
||||||
|
->and($snapshots->named('named')->path())->toEndWith("_with_data_set___{$iteration}____named.snap");
|
||||||
|
})->repeat(3);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use Pest\TestSuite;
|
||||||
use Symfony\Component\Process\Process;
|
use Symfony\Component\Process\Process;
|
||||||
|
|
||||||
it('passes on first try', function (): void {
|
it('passes on first try', function (): void {
|
||||||
@@ -231,6 +232,41 @@ it('does not leak mock objects between retries', function (): void {
|
|||||||
expect($mock->count())->toBe(1);
|
expect($mock->count())->toBe(1);
|
||||||
})->flaky(tries: 3);
|
})->flaky(tries: 3);
|
||||||
|
|
||||||
|
it('resolves the same snapshot ordinals on every retry', function (): void {
|
||||||
|
$file = sys_get_temp_dir().'/pest_flaky_snapshot_ordinals';
|
||||||
|
$count = file_exists($file) ? (int) file_get_contents($file) : 0;
|
||||||
|
file_put_contents($file, (string) ++$count);
|
||||||
|
|
||||||
|
$snapshots = TestSuite::getInstance()->snapshots;
|
||||||
|
|
||||||
|
expect($snapshots->next()->path())->toEndWith('resolves_the_same_snapshot_ordinals_on_every_retry.snap')
|
||||||
|
->and($snapshots->next()->path())->toEndWith('resolves_the_same_snapshot_ordinals_on_every_retry__2.snap');
|
||||||
|
|
||||||
|
if ($count < 3) {
|
||||||
|
throw new Exception('Flaky snapshot ordinals');
|
||||||
|
}
|
||||||
|
|
||||||
|
@unlink($file);
|
||||||
|
})->flaky(tries: 3);
|
||||||
|
|
||||||
|
it('matches the same snapshot on every retry', function (): void {
|
||||||
|
$file = sys_get_temp_dir().'/pest_flaky_snapshot_match';
|
||||||
|
$count = file_exists($file) ? (int) file_get_contents($file) : 0;
|
||||||
|
file_put_contents($file, (string) ++$count);
|
||||||
|
|
||||||
|
$snapshots = TestSuite::getInstance()->snapshots;
|
||||||
|
$snapshots->current()->write('the same on every attempt');
|
||||||
|
|
||||||
|
expect('the same on every attempt')->toMatchSnapshot()
|
||||||
|
->and($snapshots->current()->path())->toEndWith('matches_the_same_snapshot_on_every_retry.snap');
|
||||||
|
|
||||||
|
if ($count < 2) {
|
||||||
|
throw new Exception('Flaky snapshot match');
|
||||||
|
}
|
||||||
|
|
||||||
|
@unlink($file);
|
||||||
|
})->flaky(tries: 3);
|
||||||
|
|
||||||
it('does not stop retrying when snapshot changes are absent', function (): void {
|
it('does not stop retrying when snapshot changes are absent', function (): void {
|
||||||
$file = sys_get_temp_dir().'/pest_flaky_no_snapshot';
|
$file = sys_get_temp_dir().'/pest_flaky_no_snapshot';
|
||||||
$count = file_exists($file) ? (int) file_get_contents($file) : 0;
|
$count = file_exists($file) ? (int) file_get_contents($file) : 0;
|
||||||
|
|||||||
@@ -26,13 +26,13 @@ test('parallel', function () use ($run): void {
|
|||||||
$file = file_get_contents(__FILE__);
|
$file = file_get_contents(__FILE__);
|
||||||
$file = preg_replace(
|
$file = preg_replace(
|
||||||
'/\$expected = \'.*?\';/',
|
'/\$expected = \'.*?\';/',
|
||||||
"\$expected = '2 deprecated, 4 warnings, 5 incomplete, 3 notices, 40 todos, 27 skipped, 1564 passed (3380 assertions)';",
|
"\$expected = '2 deprecated, 4 warnings, 5 incomplete, 3 notices, 40 todos, 27 skipped, 1578 passed (3426 assertions)';",
|
||||||
$file,
|
$file,
|
||||||
);
|
);
|
||||||
file_put_contents(__FILE__, $file);
|
file_put_contents(__FILE__, $file);
|
||||||
}
|
}
|
||||||
|
|
||||||
$expected = '2 deprecated, 4 warnings, 5 incomplete, 3 notices, 40 todos, 27 skipped, 1564 passed (3380 assertions)';
|
$expected = '2 deprecated, 4 warnings, 5 incomplete, 3 notices, 40 todos, 27 skipped, 1578 passed (3426 assertions)';
|
||||||
|
|
||||||
expect($output)
|
expect($output)
|
||||||
->toContain("Tests: {$expected}")
|
->toContain("Tests: {$expected}")
|
||||||
|
|||||||
Reference in New Issue
Block a user