mirror of
https://github.com/pestphp/pest.git
synced 2026-09-05 14:23:34 +02:00
Compare commits
14 Commits
v5.0.2
..
92c7677c6e
| Author | SHA1 | Date | |
|---|---|---|---|
| 92c7677c6e | |||
| 872f0a50c2 | |||
| 668809bc75 | |||
| 8d8f45c843 | |||
| 086b3e9107 | |||
| 43fe26f324 | |||
| 865c1e5113 | |||
| 19eed8d581 | |||
| ad1850b110 | |||
| 585de259a2 | |||
| 953664dce0 | |||
| 1dd959848c | |||
| 047753c836 | |||
| b63626a94d |
@@ -0,0 +1,18 @@
|
||||
# CLAUDE.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
|
||||
```
|
||||
@@ -35,6 +35,7 @@ We cannot thank our sponsors enough for their incredible support in funding Pest
|
||||
- **[SerpApi](https://serpapi.com/?ref=nunomaduro)**
|
||||
- **[Typesense](https://typesense.org/?ref=nunomaduro)**
|
||||
- **[Bento](https://bentonow.com/?ref=nunomaduro)**
|
||||
- **[Redberry](https://redberry.international/laravel-development/)**
|
||||
- **[Pixel](https://wearepixel.com.au/?ref=nunomaduro)**
|
||||
- **[Redberry](https://redberry.international/laravel-development/?ref=nunomaduro)**
|
||||
|
||||
Pest is an open-sourced software licensed under the **[MIT license](https://opensource.org/licenses/MIT)**.
|
||||
|
||||
+3
-4
@@ -23,7 +23,7 @@
|
||||
"nunomaduro/termwind": "^2.4.0",
|
||||
"pestphp/pest-plugin": "^5.0.0",
|
||||
"pestphp/pest-plugin-arch": "^5.0.0",
|
||||
"pestphp/pest-plugin-mutate": "^5.0.0",
|
||||
"pestphp/pest-plugin-mutate": "^5.0.1",
|
||||
"pestphp/pest-plugin-profanity": "^5.0.0",
|
||||
"phpunit/phpunit": "^13.2.6",
|
||||
"symfony/process": "^8.1.0"
|
||||
@@ -58,12 +58,11 @@
|
||||
]
|
||||
},
|
||||
"require-dev": {
|
||||
"laravel/pao": "^1.1.3",
|
||||
"pestphp/pest-dev-tools": "^5.0.0",
|
||||
"pestphp/pest-plugin-browser": "^5.0.0",
|
||||
"pestphp/pest-plugin-phpstan": "^5.0.0",
|
||||
"pestphp/pest-plugin-rector": "^5.0.0",
|
||||
"pestphp/pest-plugin-type-coverage": "^5.0.0",
|
||||
"pestphp/pest-plugin-rector": "^5.0.2",
|
||||
"pestphp/pest-plugin-type-coverage": "^5.0.2",
|
||||
"psy/psysh": "^0.12.24"
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
|
||||
@@ -90,11 +90,11 @@ final class TestSuiteLoader
|
||||
(static function () use ($suiteClassFile) {
|
||||
try {
|
||||
include_once $suiteClassFile;
|
||||
|
||||
TestSuite::getInstance()->tests->makeIfNeeded($suiteClassFile);
|
||||
} catch (Throwable $e) {
|
||||
Panic::with($e);
|
||||
}
|
||||
|
||||
TestSuite::getInstance()->tests->makeIfNeeded($suiteClassFile);
|
||||
})();
|
||||
|
||||
$loadedClasses = array_values(
|
||||
|
||||
+1
-1
@@ -18,8 +18,8 @@
|
||||
<directory suffix=".php">./tests</directory>
|
||||
<directory suffix=".php">./tests-external</directory>
|
||||
<exclude>./tests/.snapshots</exclude>
|
||||
<exclude>./tests/.tests</exclude>
|
||||
<exclude>./tests/Fixtures/Inheritance</exclude>
|
||||
<exclude>./tests/Fixtures/Suites</exclude>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
<source>
|
||||
|
||||
@@ -28,6 +28,9 @@ return RectorConfig::configure()
|
||||
->withSkip([
|
||||
__DIR__.'/src/Plugins/Parallel/Paratest/WrapperRunner.php',
|
||||
__DIR__.'/tests/Fixtures/Arch',
|
||||
// Fixture suites are pinned by the TeamCity / JUnit snapshots, down to
|
||||
// the line numbers — rewriting their source would break them.
|
||||
__DIR__.'/tests/Fixtures/Suites',
|
||||
ReturnNeverTypeRector::class,
|
||||
ArrowFunctionDelegatingCallToFirstClassCallableRector::class,
|
||||
NarrowObjectReturnTypeRector::class,
|
||||
|
||||
@@ -280,12 +280,17 @@ trait Testable
|
||||
|
||||
/** @var Tia $tia */
|
||||
$tia = Container::getInstance()->get(Tia::class);
|
||||
$status = $tia->getStatus(self::$__filename, $this::class.'::'.$this->name());
|
||||
$status = $tia->getStatus(self::$__filename, $this->valueObjectForEvents()->id());
|
||||
$replay = ReplayType::fromStatus($status);
|
||||
|
||||
if ($replay !== ReplayType::None) {
|
||||
assert($status !== null);
|
||||
|
||||
// Marks the replay before the branches below throw, so `tearDown`
|
||||
// short-circuits for every replayed result — the throwing branches
|
||||
// never reach `parent::setUp`, so no user hook may run after them.
|
||||
$this->__replay = $replay;
|
||||
|
||||
match ($replay) {
|
||||
ReplayType::Pass, ReplayType::Risky => $this->__beginReplay($replay, $tia),
|
||||
ReplayType::Skipped => $this->markTestSkipped($status->message()),
|
||||
@@ -319,7 +324,7 @@ trait Testable
|
||||
private function __beginReplay(ReplayType $replay, Tia $tia): void
|
||||
{
|
||||
$this->__replay = $replay;
|
||||
$this->__replayAssertions = $tia->getAssertionCount($this::class.'::'.$this->name());
|
||||
$this->__replayAssertions = $tia->getAssertionCount($this->valueObjectForEvents()->id());
|
||||
$this->__ran = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,6 @@ final class AfterAllAlreadyExist extends InvalidArgumentException implements Exc
|
||||
*/
|
||||
public function __construct(string $filename)
|
||||
{
|
||||
parent::__construct(sprintf('The afterAll already exists in the filename `%s`.', $filename));
|
||||
parent::__construct(sprintf('The afterAll already exists in the filename [%s].', $filename));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,6 @@ final class AfterAllWithinDescribe extends InvalidArgumentException implements E
|
||||
*/
|
||||
public function __construct(string $filename)
|
||||
{
|
||||
parent::__construct(sprintf('The afterAll method can not be used within describe functions. Filename `%s`.', $filename));
|
||||
parent::__construct(sprintf('The afterAll method can not be used within describe functions. Filename [%s].', $filename));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,6 @@ final class AfterBeforeTestFunction extends InvalidArgumentException implements
|
||||
*/
|
||||
public function __construct(string $filename)
|
||||
{
|
||||
parent::__construct('After method cannot be used with before the [test|it] functions in the filename `['.$filename.']`.');
|
||||
parent::__construct('After method cannot be used with before the [test|it] functions in the filename ['.$filename.'].');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,6 @@ final class BeforeAllAlreadyExist extends InvalidArgumentException implements Ex
|
||||
*/
|
||||
public function __construct(string $filename)
|
||||
{
|
||||
parent::__construct(sprintf('The beforeAll already exists in the filename `%s`.', $filename));
|
||||
parent::__construct(sprintf('The beforeAll already exists in the filename [%s].', $filename));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,6 @@ final class BeforeAllWithinDescribe extends InvalidArgumentException implements
|
||||
*/
|
||||
public function __construct(string $filename)
|
||||
{
|
||||
parent::__construct(sprintf('The beforeAll method can not be used within describe functions. Filename `%s`.', $filename));
|
||||
parent::__construct(sprintf('The beforeAll method can not be used within describe functions. Filename [%s].', $filename));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,6 @@ final class DatasetAlreadyExists extends InvalidArgumentException implements Exc
|
||||
*/
|
||||
public function __construct(string $name, string $scope)
|
||||
{
|
||||
parent::__construct(sprintf('A dataset with the name `%s` already exists in scope [%s].', $name, $scope));
|
||||
parent::__construct(sprintf('A dataset with the name [%s] already exists in scope [%s].', $name, $scope));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,6 @@ final class DatasetDoesNotExist extends InvalidArgumentException implements Exce
|
||||
*/
|
||||
public function __construct(string $name)
|
||||
{
|
||||
parent::__construct(sprintf("A dataset with the name `%s` does not exist. You can create it using `dataset('%s', ['a', 'b']);`.", $name, $name));
|
||||
parent::__construct(sprintf("A dataset with the name [%s] does not exist. You can create it using `dataset('%s', ['a', 'b']);`.", $name, $name));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,6 @@ final class FileOrFolderNotFound extends InvalidArgumentException implements Exc
|
||||
*/
|
||||
public function __construct(string $filename)
|
||||
{
|
||||
parent::__construct(sprintf('The file or folder with the name `%s` could not be found.', $filename));
|
||||
parent::__construct(sprintf('The file or folder with the name [%s] could not be found.', $filename));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pest\Exceptions;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use NunoMaduro\Collision\Contracts\RenderlessEditor;
|
||||
use NunoMaduro\Collision\Contracts\RenderlessTrace;
|
||||
use Symfony\Component\Console\Exception\ExceptionInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class InvalidTestClassName extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
|
||||
{
|
||||
/**
|
||||
* Creates a new Exception instance for the given class name.
|
||||
*/
|
||||
public static function fromClassName(string $filename, string $className): self
|
||||
{
|
||||
return new self(sprintf(
|
||||
'The test file [%s] would create the class [%s], which is not a valid PHP class name. Please rename the test file.',
|
||||
$filename,
|
||||
$className,
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new Exception instance for the given namespace.
|
||||
*/
|
||||
public static function fromNamespace(string $filename, string $namespace, string $part): self
|
||||
{
|
||||
return new self(sprintf(
|
||||
'The test file [%s] would create the namespace [%s], which is not a valid PHP namespace, as [%s] may not be used as a namespace name. Please rename the folder in question.',
|
||||
$filename,
|
||||
$namespace,
|
||||
$part,
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,6 @@ final class TestAlreadyExist extends InvalidArgumentException implements Excepti
|
||||
*/
|
||||
public function __construct(string $fileName, string $description)
|
||||
{
|
||||
parent::__construct(sprintf('A test with the description `%s` already exists in the filename `%s`.', $description, $fileName));
|
||||
parent::__construct(sprintf('A test with the description [%s] already exists in the filename [%s].', $description, $fileName));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,6 @@ final class TestCaseClassOrTraitNotFound extends InvalidArgumentException implem
|
||||
*/
|
||||
public function __construct(string $testCaseClass)
|
||||
{
|
||||
parent::__construct(sprintf('The class `%s` was not found.', $testCaseClass));
|
||||
parent::__construct(sprintf('The class [%s] was not found.', $testCaseClass));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,6 @@ final class TestDescriptionMissing extends InvalidArgumentException implements E
|
||||
*/
|
||||
public function __construct(string $fileName)
|
||||
{
|
||||
parent::__construct(sprintf('Test description is missing in the filename `%s`.', $fileName));
|
||||
parent::__construct(sprintf('Test description is missing in the filename [%s].', $fileName));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ use Pest\Concerns;
|
||||
use Pest\Contracts\HasPrintableTestCaseName;
|
||||
use Pest\Evaluators\Attributes;
|
||||
use Pest\Exceptions\DatasetMissing;
|
||||
use Pest\Exceptions\InvalidTestClassName;
|
||||
use Pest\Exceptions\ShouldNotHappen;
|
||||
use Pest\Exceptions\TestAlreadyExist;
|
||||
use Pest\Exceptions\TestClosureMustNotBeStatic;
|
||||
@@ -138,6 +139,16 @@ final class TestCaseFactory
|
||||
|
||||
if (trim($className) === '') {
|
||||
$className = 'InvalidTestName'.Str::random();
|
||||
} elseif (! Str::isValidClassName($className)) {
|
||||
throw InvalidTestClassName::fromClassName($this->filename, $className);
|
||||
}
|
||||
|
||||
if ($this->namespace === null) {
|
||||
foreach ($partsFQN as $partFQN) {
|
||||
if (! Str::isValidIdentifier($partFQN)) {
|
||||
throw InvalidTestClassName::fromNamespace($this->filename, $namespace, $partFQN);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->attributes = [
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ namespace Pest;
|
||||
|
||||
function version(): string
|
||||
{
|
||||
return '5.0.2';
|
||||
return '5.0.3';
|
||||
}
|
||||
|
||||
function testDirectory(string $file = ''): string
|
||||
|
||||
+1
-1
@@ -1065,7 +1065,7 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
|
||||
{
|
||||
$this->output->writeln('');
|
||||
|
||||
$this->renderChild('Running in TIA mode, however TIA as skipped as it needs Needs ext-pcov or Xdebug.');
|
||||
$this->renderChild('Running in TIA mode, however TIA is skipped as it needs ext-pcov or Xdebug.');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -56,7 +56,7 @@ final class TableExtractor
|
||||
$tables[strtolower($name)] = true;
|
||||
}
|
||||
|
||||
$out = array_keys($tables);
|
||||
$out = array_map(strval(...), array_keys($tables));
|
||||
sort($out);
|
||||
|
||||
return $out;
|
||||
@@ -112,7 +112,7 @@ final class TableExtractor
|
||||
}
|
||||
}
|
||||
|
||||
$out = array_keys($tables);
|
||||
$out = array_map(strval(...), array_keys($tables));
|
||||
sort($out);
|
||||
|
||||
return $out;
|
||||
|
||||
@@ -22,7 +22,7 @@ final readonly class EnsureTiaAssertionsAreRecordedOnFinished implements Finishe
|
||||
|
||||
if ($test instanceof TestMethod) {
|
||||
$this->collector->recordAssertions(
|
||||
$test->className().'::'.$test->methodName(),
|
||||
$test->id(),
|
||||
$event->numberOfAssertionsPerformed(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ final readonly class EnsureTiaResultsAreCollected implements PreparationStartedS
|
||||
$test = $event->test();
|
||||
|
||||
if ($test instanceof TestMethod) {
|
||||
$this->collector->testPrepared($test->className().'::'.$test->methodName(), $test->file());
|
||||
$this->collector->testPrepared($test->id(), $test->file());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,33 @@ final class Str
|
||||
|
||||
private const string PREFIX = '__pest_evaluable_';
|
||||
|
||||
/**
|
||||
* The list of names PHP reserves, and therefore refuses, as class names.
|
||||
*
|
||||
* @see https://github.com/php/php-src/blob/master/Zend/zend_compile.c
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
private const array RESERVED_CLASS_NAMES = [
|
||||
'array',
|
||||
'bool',
|
||||
'callable',
|
||||
'false',
|
||||
'float',
|
||||
'int',
|
||||
'iterable',
|
||||
'mixed',
|
||||
'never',
|
||||
'null',
|
||||
'object',
|
||||
'parent',
|
||||
'self',
|
||||
'static',
|
||||
'string',
|
||||
'true',
|
||||
'void',
|
||||
];
|
||||
|
||||
/**
|
||||
* Create a (unsecure & non-cryptographically safe) random alpha-numeric
|
||||
* string value.
|
||||
@@ -64,6 +91,35 @@ final class Str
|
||||
return (string) preg_replace('/[^a-zA-Z0-9_\x80-\xff]/', '_', $code);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the given name is a valid PHP identifier, and therefore may
|
||||
* be used as a single namespace name.
|
||||
*/
|
||||
public static function isValidIdentifier(string $name): bool
|
||||
{
|
||||
return preg_match('/^[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*$/', $name) === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the given name may be declared as a class name by an `eval`.
|
||||
*/
|
||||
public static function isValidClassName(string $name): bool
|
||||
{
|
||||
if (! self::isValidIdentifier($name)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (in_array(strtolower($name), self::RESERVED_CLASS_NAMES, true)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$tokens = token_get_all(sprintf('<?php %s;', $name));
|
||||
|
||||
// Anything the lexer sees as a keyword, like `list` or `match`, may not
|
||||
// be used as a class name.
|
||||
return is_array($tokens[1] ?? null) && $tokens[1][0] === T_STRING;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the portion of a string before the last occurrence of a given value.
|
||||
*/
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
|
||||
Pest Testing Framework 5.0.2.
|
||||
Pest Testing Framework 5.0.3.
|
||||
|
||||
USAGE: pest <file> [options]
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
|
||||
Pest Testing Framework 5.0.2.
|
||||
Pest Testing Framework 5.0.3.
|
||||
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
##teamcity[testSuiteStarted name='Tests/tests/Failure' locationHint='pest_qn://tests/.tests/Failure.php' flowId='1234']
|
||||
##teamcity[testSuiteStarted name='Tests/Fixtures/Suites/Failure' locationHint='pest_qn://tests/Fixtures/Suites/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[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[testStarted name='it can fail with comparison' locationHint='pest_qn://tests/Fixtures/Suites/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/Fixtures/Suites/Failure.php:6' type='comparisonFailure' actual='true' expected='false' 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/Fixtures/Suites/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[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/Fixtures/Suites/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[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[testFailed name='it can fail' message='oh noo' details='at tests/.tests/Failure.php:18' flowId='1234']
|
||||
##teamcity[testStarted name='it can fail' locationHint='pest_qn://tests/Fixtures/Suites/Failure.php::it can fail' flowId='1234']
|
||||
##teamcity[testFailed name='it can fail' message='oh noo' details='at tests/Fixtures/Suites/Failure.php:18' 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[testFailed name='it throws exception' message='Exception: test error' details='at tests/.tests/Failure.php:22' flowId='1234']
|
||||
##teamcity[testStarted name='it throws exception' locationHint='pest_qn://tests/Fixtures/Suites/Failure.php::it throws exception' flowId='1234']
|
||||
##teamcity[testFailed name='it throws exception' message='Exception: test error' details='at tests/Fixtures/Suites/Failure.php:22' 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/Fixtures/Suites/Failure.php::it is not done yet' 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/Fixtures/Suites/Failure.php::build this one.' 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/Fixtures/Suites/Failure.php::it is passing' flowId='1234']
|
||||
##teamcity[testFinished name='it is passing' duration='100000' flowId='1234']
|
||||
##teamcity[testSuiteFinished name='Tests/tests/Failure' flowId='1234']
|
||||
##teamcity[testSuiteFinished name='Tests/Fixtures/Suites/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
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
##teamcity[testSuiteStarted name='Tests/tests/SuccessOnly' locationHint='pest_qn://tests/.tests/SuccessOnly.php' flowId='1234']
|
||||
##teamcity[testSuiteStarted name='Tests/Fixtures/Suites/SuccessOnly' locationHint='pest_qn://tests/Fixtures/Suites/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/Fixtures/Suites/SuccessOnly.php::it can pass with comparison' 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/Fixtures/Suites/SuccessOnly.php::can also pass' 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[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[testSuiteStarted name='can pass with dataset' locationHint='pest_qn://tests/Fixtures/Suites/SuccessOnly.php::can pass with dataset' flowId='1234']
|
||||
##teamcity[testStarted name='can pass with dataset with data set "(true)"' locationHint='pest_qn://tests/Fixtures/Suites/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[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[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[testSuiteStarted name='`block` → can pass with dataset in describe block' locationHint='pest_qn://tests/Fixtures/Suites/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/Fixtures/Suites/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[testSuiteFinished name='`block` → can pass with dataset in describe block' flowId='1234']
|
||||
##teamcity[testSuiteFinished name='Tests/tests/SuccessOnly' flowId='1234']
|
||||
##teamcity[testSuiteFinished name='Tests/Fixtures/Suites/SuccessOnly' flowId='1234']
|
||||
|
||||
[90mTests:[39m [32;1m4 passed[39;22m[90m (4 assertions)[39m
|
||||
[90mDuration:[39m [39m1.00s[39m
|
||||
|
||||
@@ -1,17 +1,4 @@
|
||||
WARN Tests\Fixtures\CollisionTest
|
||||
- error
|
||||
- success
|
||||
|
||||
PASS Tests\Fixtures\DirectoryWithTests\ExampleTest
|
||||
✓ it example 1
|
||||
|
||||
PASS Tests\Fixtures\ExampleTest
|
||||
✓ it example 2
|
||||
|
||||
WARN Tests\Fixtures\Inheritance\Base\ExampleTest
|
||||
- example
|
||||
|
||||
PASS Tests\Fixtures\Inheritance\ExampleTest
|
||||
✓ example
|
||||
|
||||
Tests: 3 skipped, 3 passed (3 assertions)
|
||||
Tests: 1 passed (1 assertions)
|
||||
|
||||
@@ -1565,6 +1565,9 @@
|
||||
✓ it allows performing no expectations without being risky
|
||||
✓ a "describe" group of tests → it allows performing no expectations without being risky
|
||||
|
||||
PASS Tests\Features\Tia
|
||||
✓ it does not run user hooks when replaying cached skipped and incomplete results
|
||||
|
||||
PASS Tests\Features\Ticket
|
||||
✓ it may be associated with an ticket #1, #2
|
||||
✓ nested → it may be associated with an ticket #1, #4, #5, #6, #3
|
||||
@@ -1908,6 +1911,11 @@
|
||||
✓ activateLinkTracking() → it tracks linked sources across consecutive tests
|
||||
✓ activateLinkTracking() → it records nothing while inactive
|
||||
|
||||
PASS Tests\Unit\Plugins\Tia\ResultKey
|
||||
✓ it keys a result per dataset row rather than per method
|
||||
✓ it records assertions against the same per-dataset key
|
||||
✓ it leaves a test without a dataset keyed by class and method
|
||||
|
||||
PASS Tests\Unit\Plugins\Tia\TableExtractor
|
||||
✓ fromSql() → it extracts tables from plain DML
|
||||
✓ fromSql() → it extracts tables from joins
|
||||
@@ -1916,10 +1924,12 @@
|
||||
✓ fromSql() → it records the table, not the schema, for qualified identifiers
|
||||
✓ fromSql() → it handles quoted identifiers
|
||||
✓ fromSql() → it ignores schema metadata tables
|
||||
✓ fromSql() → it does not leak int keys for numeric identifiers
|
||||
✓ fromSql() → it returns nothing for non-DML statements
|
||||
✓ fromMigrationSource() → it extracts tables from Schema builder calls
|
||||
✓ fromMigrationSource() → it extracts tables from raw DDL statements
|
||||
✓ fromMigrationSource() → it records the table, not the schema, in qualified DDL and DML
|
||||
✓ fromMigrationSource() → it does not leak int keys for numeric table names
|
||||
✓ fromMigrationSource() → it extracts tables from DB::table calls
|
||||
|
||||
PASS Tests\Unit\Plugins\Tia\TestPaths
|
||||
@@ -2153,6 +2163,7 @@
|
||||
✓ a parallel test can extend another test with same name
|
||||
✓ parallel reports invalid datasets as failures
|
||||
✓ parallel can have multiple exclude-groups
|
||||
✓ parallel can have multiple groups
|
||||
|
||||
PASS Tests\Visual\ParallelNestedDatasets
|
||||
✓ parallel loads nested datasets from nested directories
|
||||
@@ -2186,4 +2197,4 @@
|
||||
✓ pass with dataset with ('my-datas-set-value')
|
||||
✓ within describe → pass with dataset with ('my-datas-set-value')
|
||||
|
||||
Tests: 1 deprecated, 4 warnings, 5 incomplete, 2 notices, 40 todos, 35 skipped, 1542 passed (3375 assertions)
|
||||
Tests: 1 deprecated, 4 warnings, 5 incomplete, 2 notices, 40 todos, 35 skipped, 1549 passed (3389 assertions)
|
||||
@@ -18,27 +18,27 @@ $run = function (string $target): array {
|
||||
};
|
||||
|
||||
test('reports missing datasets as errors for a single file run', function () use ($run): void {
|
||||
$result = $run('tests/.tests/IssueOnly.php');
|
||||
$result = $run('tests/Fixtures/Suites/IssueOnly.php');
|
||||
|
||||
expect($result['output'])
|
||||
->toContain("A dataset with the name `missing` does not exist. You can create it using `dataset('missing', ['a', 'b']);`.")
|
||||
->toContain("A dataset with the name [missing] does not exist. You can create it using `dataset('missing', ['a', 'b']);`.")
|
||||
->toContain('FAILED')
|
||||
->toContain('Tests: 1 failed')
|
||||
->and($result['code'])->not->toBe(0);
|
||||
})->skipOnWindows();
|
||||
|
||||
test('reports missing datasets as errors alongside passing tests', function () use ($run): void {
|
||||
$result = $run('tests/.tests/IssueWithPassing.php');
|
||||
$result = $run('tests/Fixtures/Suites/IssueWithPassing.php');
|
||||
|
||||
expect($result['output'])
|
||||
->toContain("A dataset with the name `missing` does not exist. You can create it using `dataset('missing', ['a', 'b']);`.")
|
||||
->toContain("A dataset with the name [missing] does not exist. You can create it using `dataset('missing', ['a', 'b']);`.")
|
||||
->toContain('1 passed')
|
||||
->toContain('1 failed')
|
||||
->and($result['code'])->not->toBe(0);
|
||||
})->skipOnWindows();
|
||||
|
||||
test('reports dataset closure exceptions as errors', function () use ($run): void {
|
||||
$result = $run('tests/.tests/DatasetClosureThrows.php');
|
||||
$result = $run('tests/Fixtures/Suites/DatasetClosureThrows.php');
|
||||
|
||||
expect($result['output'])
|
||||
->toContain('boom from dataset')
|
||||
|
||||
@@ -10,12 +10,12 @@ beforeEach(function (): void {
|
||||
});
|
||||
|
||||
it('throws exception if dataset does not exist', function (): void {
|
||||
expect(fn () => DatasetsRepository::resolve(['first'], __FILE__))->toThrow(DatasetDoesNotExist::class, "A dataset with the name `first` does not exist. You can create it using `dataset('first', ['a', 'b']);`.");
|
||||
expect(fn () => DatasetsRepository::resolve(['first'], __FILE__))->toThrow(DatasetDoesNotExist::class, "A dataset with the name [first] does not exist. You can create it using `dataset('first', ['a', 'b']);`.");
|
||||
});
|
||||
|
||||
it('throws exception if dataset already exist', function (): void {
|
||||
DatasetsRepository::set('second', [[]], __DIR__);
|
||||
expect(fn () => DatasetsRepository::set('second', [[]], __DIR__))->toThrow(DatasetAlreadyExists::class, 'A dataset with the name `second` already exists in scope ['.__DIR__.'].');
|
||||
expect(fn () => DatasetsRepository::set('second', [[]], __DIR__))->toThrow(DatasetAlreadyExists::class, 'A dataset with the name [second] already exists in scope ['.__DIR__.'].');
|
||||
});
|
||||
|
||||
it('sets closures', function (): void {
|
||||
|
||||
@@ -142,7 +142,7 @@ it('works as higher order test')
|
||||
|
||||
it('fails after exhausting all retries', function (): void {
|
||||
$process = new Process(
|
||||
['php', 'bin/pest', 'tests/.tests/FlakyFailure.php'],
|
||||
['php', 'bin/pest', 'tests/Fixtures/Suites/FlakyFailure.php'],
|
||||
dirname(__DIR__, 2),
|
||||
['COLLISION_PRINTER' => 'DefaultPrinter', 'COLLISION_IGNORE_DURATION' => 'true', 'PAO_DISABLE' => '1'],
|
||||
);
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
use Pest\Plugins\Tia;
|
||||
use Pest\Plugins\Tia\ChangedFiles;
|
||||
use Pest\Plugins\Tia\FileState;
|
||||
use Pest\Plugins\Tia\Fingerprint;
|
||||
use Pest\Plugins\Tia\Graph;
|
||||
use Pest\Plugins\Tia\Storage;
|
||||
use Pest\Support\Str;
|
||||
use Symfony\Component\Process\Process;
|
||||
|
||||
it('does not run user hooks when replaying cached skipped and incomplete results', function (): void {
|
||||
$projectRoot = dirname(__DIR__, 2);
|
||||
$home = sys_get_temp_dir().'/pest-tia-'.bin2hex(random_bytes(8));
|
||||
$fixture = 'tests/Fixtures/Suites/TiaReplayHooks.php';
|
||||
|
||||
mkdir($home, 0755, true);
|
||||
|
||||
try {
|
||||
$changedFiles = new ChangedFiles($projectRoot);
|
||||
$branch = $changedFiles->currentBranch() ?? 'main';
|
||||
$sha = $changedFiles->currentSha();
|
||||
|
||||
$id = fn (string $description): string => 'P\Tests\Fixtures\Suites\TiaReplayHooks::'.Str::evaluable($description);
|
||||
|
||||
$graph = new Graph($projectRoot);
|
||||
$graph->setFingerprint(Fingerprint::compute($projectRoot));
|
||||
$graph->setRecordedAtSha($branch, $sha);
|
||||
// Hashes the working tree as it stands, so the replay sees nothing as changed.
|
||||
$graph->setLastRunTree($branch, $changedFiles->snapshotTree($changedFiles->since($sha) ?? []));
|
||||
$graph->markKnownTestFiles([$fixture]);
|
||||
$graph->setResult($branch, $id('replayed pass'), 0, '', 0.01, 1, $fixture);
|
||||
$graph->setResult($branch, $id('replayed skip'), 1, 'cached skip', 0.01, 0, $fixture);
|
||||
$graph->setResult($branch, $id('replayed incomplete'), 2, 'cached incomplete', 0.01, 0, $fixture);
|
||||
|
||||
$json = $graph->encode();
|
||||
|
||||
expect($json)->not->toBeNull();
|
||||
|
||||
$originalHome = getenv('HOME');
|
||||
putenv('HOME='.$home);
|
||||
|
||||
try {
|
||||
$storage = new FileState(Storage::tempDir($projectRoot));
|
||||
} finally {
|
||||
putenv($originalHome === false ? 'HOME' : 'HOME='.$originalHome);
|
||||
}
|
||||
|
||||
expect($storage->write(Tia::KEY_GRAPH, (string) $json))->toBeTrue();
|
||||
|
||||
$process = new Process(
|
||||
['php', 'bin/pest', $fixture, '--tia'],
|
||||
$projectRoot,
|
||||
[
|
||||
'COLLISION_PRINTER' => 'DefaultPrinter',
|
||||
'COLLISION_IGNORE_DURATION' => 'true',
|
||||
'PARATEST' => 0,
|
||||
'PAO_DISABLE' => '1',
|
||||
'HOME' => $home,
|
||||
],
|
||||
);
|
||||
|
||||
$process->run();
|
||||
|
||||
$output = removeAnsiEscapeSequences($process->getOutput().$process->getErrorOutput());
|
||||
|
||||
// Both hooks throw, so the run stays green only if neither one ran.
|
||||
expect($output)->toContain('3 replayed')
|
||||
->and($output)->not->toContain('must not run for replayed tests')
|
||||
->and($output)->toContain('1 incomplete, 1 skipped, 1 passed')
|
||||
->and($process->getExitCode())->toBe(0);
|
||||
} finally {
|
||||
$paths = new RecursiveIteratorIterator(
|
||||
new RecursiveDirectoryIterator($home, FilesystemIterator::SKIP_DOTS),
|
||||
RecursiveIteratorIterator::CHILD_FIRST,
|
||||
);
|
||||
|
||||
foreach ($paths as $path) {
|
||||
$path->isDir() ? @rmdir($path->getPathname()) : @unlink($path->getPathname());
|
||||
}
|
||||
|
||||
@rmdir($home);
|
||||
}
|
||||
})->skipOnWindows();
|
||||
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
|
||||
// The folder name creates the namespace segment `2fa`, which starts with a number.
|
||||
|
||||
it('never runs')->assertTrue(true);
|
||||
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
|
||||
// The file name creates the class `list`, which is a PHP keyword.
|
||||
|
||||
it('never runs')->assertTrue(true);
|
||||
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
|
||||
// The file name creates the class `int`, which is a name PHP reserves.
|
||||
|
||||
it('never runs')->assertTrue(true);
|
||||
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
|
||||
// The file name creates the class `2fa`, which starts with a number.
|
||||
|
||||
it('never runs')->assertTrue(true);
|
||||
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
|
||||
it('belongs to group one', function () {
|
||||
expect(true)->toBeTrue();
|
||||
})->group('one');
|
||||
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
|
||||
it('belongs to group three', function () {
|
||||
expect(true)->toBeTrue();
|
||||
})->group('three');
|
||||
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
|
||||
it('belongs to group two', function () {
|
||||
expect(true)->toBeTrue();
|
||||
})->group('two');
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
// Only ever run through `tests/Features/Tia.php`, against a seeded TIA graph.
|
||||
// Both hooks throw, so a replayed test that wrongly runs one fails the run.
|
||||
|
||||
beforeEach(function (): void {
|
||||
throw new RuntimeException('The beforeEach hook must not run for replayed tests.');
|
||||
});
|
||||
|
||||
afterEach(function (): void {
|
||||
throw new RuntimeException('The afterEach hook must not run for replayed tests.');
|
||||
});
|
||||
|
||||
test('replayed pass', function (): void {
|
||||
expect(true)->toBeTrue();
|
||||
});
|
||||
|
||||
test('replayed skip', function (): void {
|
||||
expect(true)->toBeTrue();
|
||||
});
|
||||
|
||||
test('replayed incomplete', function (): void {
|
||||
expect(true)->toBeTrue();
|
||||
});
|
||||
@@ -0,0 +1,114 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Pest\Plugins\Tia\ResultCollector;
|
||||
use Pest\Subscribers\EnsureTiaAssertionsAreRecordedOnFinished;
|
||||
use Pest\Subscribers\EnsureTiaResultsAreCollected;
|
||||
use PHPUnit\Event\Code\TestDox;
|
||||
use PHPUnit\Event\Code\TestMethod;
|
||||
use PHPUnit\Event\Telemetry\Duration;
|
||||
use PHPUnit\Event\Telemetry\Info;
|
||||
use PHPUnit\Event\Telemetry\MemoryUsage;
|
||||
use PHPUnit\Event\Telemetry\System;
|
||||
use PHPUnit\Event\Telemetry\SystemCpuTimeMeter;
|
||||
use PHPUnit\Event\Telemetry\SystemGarbageCollectorStatusProvider;
|
||||
use PHPUnit\Event\Telemetry\SystemMemoryMeter;
|
||||
use PHPUnit\Event\Telemetry\SystemStopWatch;
|
||||
use PHPUnit\Event\Test\Finished;
|
||||
use PHPUnit\Event\Test\PreparationStarted;
|
||||
use PHPUnit\Event\TestData\DataFromDataProvider;
|
||||
use PHPUnit\Event\TestData\TestDataCollection;
|
||||
use PHPUnit\Metadata\MetadataCollection;
|
||||
|
||||
function tiaResultKeyTestMethod(?string $dataSetName): TestMethod
|
||||
{
|
||||
$testData = $dataSetName === null
|
||||
? TestDataCollection::fromArray([])
|
||||
: TestDataCollection::fromArray([DataFromDataProvider::from($dataSetName, '', '')]);
|
||||
|
||||
return new TestMethod(
|
||||
'Tests\Feature\OrderTest',
|
||||
'it prices an order',
|
||||
'/project/tests/Feature/OrderTest.php',
|
||||
1,
|
||||
new TestDox('Order', 'it prices an order', 'it prices an order'),
|
||||
MetadataCollection::fromArray([]),
|
||||
$testData,
|
||||
);
|
||||
}
|
||||
|
||||
function tiaResultKeyTelemetryInfo(): Info
|
||||
{
|
||||
$system = new System(
|
||||
new SystemStopWatch,
|
||||
new SystemMemoryMeter,
|
||||
new SystemGarbageCollectorStatusProvider,
|
||||
new SystemCpuTimeMeter,
|
||||
);
|
||||
|
||||
$zeroDuration = Duration::fromSecondsAndNanoseconds(0, 0);
|
||||
$zeroMemory = MemoryUsage::fromBytes(0);
|
||||
$zeroCpuTime = $system->snapshot()->userCpuTime();
|
||||
|
||||
return new Info(
|
||||
$system->snapshot(),
|
||||
$zeroDuration,
|
||||
$zeroMemory,
|
||||
$zeroDuration,
|
||||
$zeroMemory,
|
||||
$zeroCpuTime,
|
||||
$zeroCpuTime,
|
||||
$zeroCpuTime,
|
||||
$zeroCpuTime,
|
||||
$zeroCpuTime,
|
||||
$zeroCpuTime,
|
||||
);
|
||||
}
|
||||
|
||||
it('keys a result per dataset row rather than per method', function (): void {
|
||||
$collector = new ResultCollector;
|
||||
$subscriber = new EnsureTiaResultsAreCollected($collector);
|
||||
|
||||
$subscriber->notify(new PreparationStarted(tiaResultKeyTelemetryInfo(), tiaResultKeyTestMethod('opp')));
|
||||
$collector->testPassed();
|
||||
$collector->finishTest();
|
||||
|
||||
$subscriber->notify(new PreparationStarted(tiaResultKeyTelemetryInfo(), tiaResultKeyTestMethod('fake')));
|
||||
$collector->testSkipped('the fake driver does not report balances');
|
||||
$collector->finishTest();
|
||||
|
||||
// Without the dataset in the key both rows write to `Class::method`, so the
|
||||
// second one overwrites the first and a replay hands every row the same
|
||||
// status — a passing row reported as skipped, or a failing one as passed.
|
||||
expect(array_keys($collector->all()))->toBe([
|
||||
'Tests\Feature\OrderTest::it prices an order#opp',
|
||||
'Tests\Feature\OrderTest::it prices an order#fake',
|
||||
]);
|
||||
});
|
||||
|
||||
it('records assertions against the same per-dataset key', function (): void {
|
||||
$collector = new ResultCollector;
|
||||
|
||||
new EnsureTiaResultsAreCollected($collector)->notify(
|
||||
new PreparationStarted(tiaResultKeyTelemetryInfo(), tiaResultKeyTestMethod('opp')),
|
||||
);
|
||||
$collector->testPassed();
|
||||
|
||||
new EnsureTiaAssertionsAreRecordedOnFinished($collector)->notify(
|
||||
new Finished(tiaResultKeyTelemetryInfo(), tiaResultKeyTestMethod('opp'), 7),
|
||||
);
|
||||
|
||||
expect($collector->all()['Tests\Feature\OrderTest::it prices an order#opp']['assertions'])->toBe(7);
|
||||
});
|
||||
|
||||
it('leaves a test without a dataset keyed by class and method', function (): void {
|
||||
$collector = new ResultCollector;
|
||||
|
||||
new EnsureTiaResultsAreCollected($collector)->notify(
|
||||
new PreparationStarted(tiaResultKeyTelemetryInfo(), tiaResultKeyTestMethod(null)),
|
||||
);
|
||||
$collector->testPassed();
|
||||
|
||||
expect(array_keys($collector->all()))->toBe(['Tests\Feature\OrderTest::it prices an order']);
|
||||
});
|
||||
@@ -47,6 +47,15 @@ describe('fromSql()', function (): void {
|
||||
->and(TableExtractor::fromSql('select * from information_schema.tables'))->toBeEmpty();
|
||||
});
|
||||
|
||||
it('does not leak int keys for numeric identifiers', function (): void {
|
||||
// `substring(x FROM 1 FOR 3)` is standard SQL, and the `1` matches the
|
||||
// FROM pattern. Collecting names as array keys makes PHP coerce the
|
||||
// numeric string to an int, which then violates the declared
|
||||
// list<string> and blows up Recorder::linkTable(string).
|
||||
expect(TableExtractor::fromSql('select substring(name from 1 for 3) from users'))
|
||||
->each->toBeString();
|
||||
});
|
||||
|
||||
it('returns nothing for non-DML statements', function (): void {
|
||||
expect(TableExtractor::fromSql('PRAGMA foreign_keys = ON'))->toBeEmpty()
|
||||
->and(TableExtractor::fromSql(''))->toBeEmpty()
|
||||
@@ -89,6 +98,14 @@ describe('fromMigrationSource()', function (): void {
|
||||
->toBe(['audits', 'events', 'sessions', 'settings', 'users']);
|
||||
});
|
||||
|
||||
it('does not leak int keys for numeric table names', function (): void {
|
||||
// A table named `123` is a legal quoted identifier. Collecting names as
|
||||
// array keys makes PHP coerce it to an int, breaking the declared
|
||||
// list<string>, so it must survive as a string rather than be dropped.
|
||||
expect(TableExtractor::fromMigrationSource("DB::table('123')->insert([]);"))
|
||||
->toBe(['123']);
|
||||
});
|
||||
|
||||
it('extracts tables from DB::table calls', function (): void {
|
||||
expect(TableExtractor::fromMigrationSource("DB::table('permissions')->insert([]);"))
|
||||
->toBe(['permissions']);
|
||||
|
||||
@@ -15,7 +15,7 @@ it('does not allow to add the same test description twice', function (): void {
|
||||
$testSuite->tests->set($method);
|
||||
})->throws(
|
||||
TestAlreadyExist::class,
|
||||
sprintf('A test with the description `%s` already exists in the filename `%s`.', 'bar', 'foo'),
|
||||
sprintf('A test with the description [%s] already exists in the filename [%s].', 'bar', 'foo'),
|
||||
);
|
||||
|
||||
it('does not allow static closures', function (): void {
|
||||
|
||||
@@ -29,30 +29,30 @@ $run = function () {
|
||||
$normalizedPath = (fn (string $path): string => str_replace('/', DIRECTORY_SEPARATOR, $path));
|
||||
|
||||
test('junit output', function () use ($normalizedPath, $run): void {
|
||||
$result = $run('tests/.tests/SuccessOnly.php');
|
||||
$result = $run('tests/Fixtures/Suites/SuccessOnly.php');
|
||||
|
||||
expect($result['testsuite']['@attributes'])
|
||||
->name->toBe('Tests\tests\SuccessOnly')
|
||||
->file->toBe($normalizedPath('tests/.tests/SuccessOnly.php'))
|
||||
->name->toBe('Tests\Fixtures\Suites\SuccessOnly')
|
||||
->file->toBe($normalizedPath('tests/Fixtures/Suites/SuccessOnly.php'))
|
||||
->tests->toBe('4')
|
||||
->assertions->toBe('4')
|
||||
->errors->toBe('0')
|
||||
->failures->toBe('0')
|
||||
->skipped->toBe('0')
|
||||
->and($result['testsuite']['testcase'])->toHaveCount(2)
|
||||
->and($result['testsuite']['testcase'][0]['@attributes'])->name->toBe('it can pass with comparison')->file->toBe($normalizedPath('tests/.tests/SuccessOnly.php::it can pass with comparison'))->class->toBe('Tests\tests\SuccessOnly')->classname->toBe('Tests.tests.SuccessOnly')->assertions->toBe('1')->time->toStartWith('0.0');
|
||||
->and($result['testsuite']['testcase'][0]['@attributes'])->name->toBe('it can pass with comparison')->file->toBe($normalizedPath('tests/Fixtures/Suites/SuccessOnly.php::it can pass with comparison'))->class->toBe('Tests\Fixtures\Suites\SuccessOnly')->classname->toBe('Tests.Fixtures.Suites.SuccessOnly')->assertions->toBe('1')->time->toStartWith('0.0');
|
||||
});
|
||||
|
||||
test('junit with parallel', function () use ($normalizedPath, $run): void {
|
||||
$result = $run('tests/.tests/SuccessOnly.php', '--parallel', '--processes=1', '--filter', 'can pass with comparison');
|
||||
$result = $run('tests/Fixtures/Suites/SuccessOnly.php', '--parallel', '--processes=1', '--filter', 'can pass with comparison');
|
||||
|
||||
expect($result['testsuite']['@attributes'])
|
||||
->name->toBe('Tests\tests\SuccessOnly')
|
||||
->file->toBe($normalizedPath('tests/.tests/SuccessOnly.php'))
|
||||
->name->toBe('Tests\Fixtures\Suites\SuccessOnly')
|
||||
->file->toBe($normalizedPath('tests/Fixtures/Suites/SuccessOnly.php'))
|
||||
->tests->toBe('1')
|
||||
->assertions->toBe('1')
|
||||
->errors->toBe('0')
|
||||
->failures->toBe('0')
|
||||
->skipped->toBe('0')
|
||||
->and($result['testsuite']['testcase']['@attributes'])->name->toBe('it can pass with comparison')->file->toBe($normalizedPath('tests/.tests/SuccessOnly.php::it can pass with comparison'))->class->toBe('Tests\tests\SuccessOnly')->classname->toBe('Tests.tests.SuccessOnly')->assertions->toBe('1')->time->toStartWith('0.0');
|
||||
->and($result['testsuite']['testcase']['@attributes'])->name->toBe('it can pass with comparison')->file->toBe($normalizedPath('tests/Fixtures/Suites/SuccessOnly.php::it can pass with comparison'))->class->toBe('Tests\Fixtures\Suites\SuccessOnly')->classname->toBe('Tests.Fixtures.Suites.SuccessOnly')->assertions->toBe('1')->time->toStartWith('0.0');
|
||||
});
|
||||
|
||||
@@ -24,13 +24,13 @@ test('parallel', function () use ($run): void {
|
||||
$file = file_get_contents(__FILE__);
|
||||
$file = preg_replace(
|
||||
'/\$expected = \'.*?\';/',
|
||||
"\$expected = '1 deprecated, 4 warnings, 5 incomplete, 3 notices, 40 todos, 27 skipped, 1525 passed (3322 assertions)';",
|
||||
"\$expected = '1 deprecated, 4 warnings, 5 incomplete, 3 notices, 40 todos, 27 skipped, 1531 passed (3334 assertions)';",
|
||||
$file,
|
||||
);
|
||||
file_put_contents(__FILE__, $file);
|
||||
}
|
||||
|
||||
$expected = '1 deprecated, 4 warnings, 5 incomplete, 3 notices, 40 todos, 27 skipped, 1525 passed (3322 assertions)';
|
||||
$expected = '1 deprecated, 4 warnings, 5 incomplete, 3 notices, 40 todos, 27 skipped, 1531 passed (3334 assertions)';
|
||||
|
||||
expect($output)
|
||||
->toContain("Tests: {$expected}")
|
||||
@@ -42,8 +42,8 @@ test('a parallel test can extend another test with same name', function () use (
|
||||
})->skipOnWindows();
|
||||
|
||||
test('parallel reports invalid datasets as failures', function () use ($run): void {
|
||||
expect($run('tests/.tests/ParallelInvalidDataset'))
|
||||
->toContain("A dataset with the name `missing.dataset` does not exist. You can create it using `dataset('missing.dataset', ['a', 'b']);`.")
|
||||
expect($run('tests/Fixtures/Suites/ParallelInvalidDataset'))
|
||||
->toContain("A dataset with the name [missing.dataset] does not exist. You can create it using `dataset('missing.dataset', ['a', 'b']);`.")
|
||||
->toContain('Tests: 1 failed, 1 passed (1 assertions)')
|
||||
->toContain('Parallel: 3 processes');
|
||||
})->skipOnWindows();
|
||||
@@ -58,3 +58,11 @@ test('parallel can have multiple exclude-groups', function () use ($run): void {
|
||||
expect((int) $doubleMatch[1])->toBeLessThan((int) $singleMatch[1])
|
||||
->and($doubleExclude)->toContain('Parallel: 3 processes');
|
||||
})->skipOnWindows();
|
||||
|
||||
test('parallel can have multiple groups', function () use ($run): void {
|
||||
$output = $run('tests/Fixtures/Suites/MultipleGroups', '--group=one', '--group=two');
|
||||
|
||||
expect($output)
|
||||
->toContain('Tests: 2 passed (2 assertions)')
|
||||
->toContain('Parallel: 3 processes');
|
||||
})->skipOnWindows();
|
||||
|
||||
@@ -27,7 +27,7 @@ test('allows to run a single test', function () use ($run, $snapshot): void {
|
||||
})->skipOnWindows();
|
||||
|
||||
test('allows to run a directory', function () use ($run, $snapshot): void {
|
||||
expect($run('tests/Fixtures'))->toContain($snapshot('allows-to-run-a-directory'));
|
||||
expect($run('tests/Fixtures/DirectoryWithTests'))->toContain($snapshot('allows-to-run-a-directory'));
|
||||
})->skipOnWindows();
|
||||
|
||||
it('disable decorating printer when colors is set to never', function () use ($snapshot): void {
|
||||
|
||||
@@ -10,7 +10,7 @@ function normalize_windows_os_output(string $text): string
|
||||
}
|
||||
|
||||
test('visual snapshot of team city', function (string $testFile): void {
|
||||
$testsPath = dirname(__DIR__)."/.tests/$testFile";
|
||||
$testsPath = dirname(__DIR__)."/Fixtures/Suites/$testFile";
|
||||
|
||||
$snapshot = implode(DIRECTORY_SEPARATOR, [
|
||||
dirname(__DIR__),
|
||||
|
||||
@@ -8,7 +8,7 @@ test('filter works with unicode characters in filename', function (): void {
|
||||
$process = new Process([
|
||||
'php',
|
||||
'bin/pest',
|
||||
'tests/.tests/StraßenTest.php',
|
||||
'tests/Fixtures/Suites/StraßenTest.php',
|
||||
'--colors=never',
|
||||
], dirname(__DIR__, 2), ['COLLISION_PRINTER' => 'DefaultPrinter', 'COLLISION_IGNORE_DURATION' => 'true', 'PAO_DISABLE' => '1']);
|
||||
|
||||
@@ -26,7 +26,7 @@ test('filter with unicode regex matches unicode filename', function (): void {
|
||||
'php',
|
||||
'bin/pest',
|
||||
'--filter=.*Straß.*',
|
||||
'tests/.tests/',
|
||||
'tests/Fixtures/Suites/',
|
||||
'--colors=never',
|
||||
], dirname(__DIR__, 2), ['COLLISION_PRINTER' => 'DefaultPrinter', 'COLLISION_IGNORE_DURATION' => 'true', 'PAO_DISABLE' => '1']);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user