diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml
index 3373eafb9..5cce40fc6 100644
--- a/.github/workflows/static.yml
+++ b/.github/workflows/static.yml
@@ -19,20 +19,24 @@ jobs:
steps:
- name: Checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@v6
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
- php-version: 8.2
+ php-version: 8.3
tools: composer:v2
coverage: none
+ extensions: sockets
- name: Install Dependencies
run: composer update --prefer-stable --no-interaction --no-progress --ansi
- # - name: Type Check
- # run: composer test:type:check
+ - name: Profanity Check
+ run: composer test:profanity
+
+ - name: Type Check
+ run: composer test:type:check
- name: Type Coverage
run: composer test:type:coverage
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index c4902b423..68d070dad 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -12,16 +12,16 @@ jobs:
strategy:
fail-fast: true
matrix:
- os: [ubuntu-latest, macos-latest, windows-latest]
- symfony: ['7.1']
- php: ['8.2', '8.3', '8.4']
- dependency_version: [prefer-lowest, prefer-stable]
+ os: [ubuntu-latest, macos-latest] # windows-latest
+ symfony: ['7.3']
+ php: ['8.3', '8.4', '8.5']
+ dependency_version: [prefer-stable]
name: PHP ${{ matrix.php }} - Symfony ^${{ matrix.symfony }} - ${{ matrix.os }} - ${{ matrix.dependency_version }}
steps:
- name: Checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@v6
- name: Setup PHP
uses: shivammathur/setup-php@v2
@@ -29,6 +29,7 @@ jobs:
php-version: ${{ matrix.php }}
tools: composer:v2
coverage: none
+ extensions: sockets
- name: Setup Problem Matches
run: |
diff --git a/README.md b/README.md
index acb06126f..ed0020c0c 100644
--- a/README.md
+++ b/README.md
@@ -1,23 +1,24 @@
-
+
-
+
+
------
-> Pest v3 Now Available: **[Read the announcement »](https://pestphp.com/docs/pest3-now-available)**.
+> Pest v4 Now Available: **[Read the announcement »](https://pestphp.com/docs/pest-v4-is-here-now-with-browser-testing)**.
**Pest** is an elegant PHP testing Framework with a focus on simplicity, meticulously designed to bring back the joy of testing in PHP.
- Explore our docs at **[pestphp.com »](https://pestphp.com)**
- Follow the creator Nuno Maduro:
- - YouTube: **[youtube.com/@nunomaduro](https://www.youtube.com/@nunomaduro)** — Videos every weekday
- - Twitch: **[twitch.tv/enunomaduro](https://www.twitch.tv/enunomaduro)** — Streams (almost) every weekday
+ - YouTube: **[youtube.com/@nunomaduro](https://youtube.com/@nunomaduro)** — Videos every week
+ - Twitch: **[twitch.tv/nunomaduro](https://twitch.tv/nunomaduro)** — Live coding on Mondays, Wednesdays, and Fridays at 9PM UTC
- Twitter / X: **[x.com/enunomaduro](https://x.com/enunomaduro)**
- LinkedIn: **[linkedin.com/in/nunomaduro](https://www.linkedin.com/in/nunomaduro)**
- Instagram: **[instagram.com/enunomaduro](https://www.instagram.com/enunomaduro)**
@@ -30,23 +31,23 @@ We cannot thank our sponsors enough for their incredible support in funding Pest
### Platinum Sponsors
-- **[Laracasts](https://laracasts.com/?ref=pestphp)**
+- **[CodeRabbit](https://coderabbit.ai/?ref=pestphp)**
+- **[Devin](https://devin.ai/?ref=nunomaduro)**
+- **[Mailtrap](https://l.rw.rw/pestphp)**
+- **[Tighten](https://tighten.com/?ref=nunomaduro)**
+- **[Redberry](https://redberry.international/laravel-development/?utm_source=pest&utm_medium=banner&utm_campaign=pest_sponsorship)**
### Gold Sponsors
-- **[CodeRabbit](https://coderabbit.ai/?ref=pestphp)**
-- **[NativePHP](https://nativephp.com/mobile?ref=pestphp.com)**
- **[CMS Max](https://cmsmax.com/?ref=pestphp)**
### Premium Sponsors
+- [Zapiet](https://zapiet.com/?ref=pestphp)
+- [Load Forge](https://loadforge.com/?ref=pestphp)
+- [Route4Me](https://route4me.com/pt?ref=pestphp)
+- [Nerdify](https://getnerdify.com/?ref=pestphp)
- [Akaunting](https://akaunting.com/?ref=pestphp)
-- [DocuWriter.ai](https://www.docuwriter.ai/?ref=pestphp)
-- [Localazy](https://localazy.com/?ref=pestphp)
-- [Forge](https://forge.laravel.com/?ref=pestphp)
-- [Route4Me](https://www.route4me.com/?ref=pestphp)
-- [Spatie](https://spatie.be/?ref=pestphp)
-- [Worksome](https://www.worksome.com/?ref=pestphp)
-- [Zapiet](https://www.zapiet.com/?ref=pestphp)
+- [LambdaTest](https://lambdatest.com/?ref=pestphp)
Pest is an open-sourced software licensed under the **[MIT license](https://opensource.org/licenses/MIT)**.
diff --git a/RELEASE.md b/RELEASE.md
index f0ad61139..6ba2a721e 100644
--- a/RELEASE.md
+++ b/RELEASE.md
@@ -2,10 +2,10 @@
When releasing a new version of Pest there are some checks and updates that need to be done:
-> **For Pest v2 you should use the `2.x` branch instead.**
+> **For Pest v3 you should use the `3.x` branch instead.**
-- Clear your local repository with: `git add . && git reset --hard && git checkout 3.x`
-- On the GitHub repository, check the contents of [github.com/pestphp/pest/compare/{latest_version}...3.x](https://github.com/pestphp/pest/compare/{latest_version}...3.x)
+- Clear your local repository with: `git add . && git reset --hard && git checkout 4.x`
+- On the GitHub repository, check the contents of [github.com/pestphp/pest/compare/{latest_version}...4.x](https://github.com/pestphp/pest/compare/{latest_version}...4.x)
- Update the version number in [src/Pest.php](src/Pest.php)
- Run the tests locally using: `composer test`
- Commit the Pest file with the message: `git commit -m "release: vX.X.X"`
diff --git a/bin/worker.php b/bin/worker.php
index c26b05eba..dc69d67e3 100644
--- a/bin/worker.php
+++ b/bin/worker.php
@@ -86,7 +86,7 @@
$getopt['teamcity-file'] ?? null,
$getopt['testdox-file'] ?? null,
isset($getopt['testdox-color']),
- $getopt['testdox-columns'] ?? null,
+ (int) ($getopt['testdox-columns'] ?? null),
);
while (true) {
diff --git a/composer.json b/composer.json
index 60d16d8f0..9d7fcdee7 100644
--- a/composer.json
+++ b/composer.json
@@ -17,19 +17,21 @@
}
],
"require": {
- "php": "^8.2.0",
- "brianium/paratest": "^7.8.4",
- "nunomaduro/collision": "^8.8.2",
- "nunomaduro/termwind": "^2.3.1",
- "pestphp/pest-plugin": "^3.0.0",
- "pestphp/pest-plugin-arch": "^3.1.1",
- "pestphp/pest-plugin-mutate": "^3.0.5",
- "phpunit/phpunit": "^11.5.33"
+ "php": "^8.3.0",
+ "brianium/paratest": "^7.16.1",
+ "nunomaduro/collision": "^8.8.3",
+ "nunomaduro/termwind": "^2.3.3",
+ "pestphp/pest-plugin": "^4.0.0",
+ "pestphp/pest-plugin-arch": "^4.0.0",
+ "pestphp/pest-plugin-mutate": "^4.0.1",
+ "pestphp/pest-plugin-profanity": "^4.2.1",
+ "phpunit/phpunit": "^12.5.8",
+ "symfony/process": "^7.4.4|^8.0.0"
},
"conflict": {
- "filp/whoops": "<2.16.0",
- "phpunit/phpunit": ">11.5.33",
- "sebastian/exporter": "<6.0.0",
+ "filp/whoops": "<2.18.3",
+ "phpunit/phpunit": ">12.5.8",
+ "sebastian/exporter": "<7.0.0",
"webmozart/assert": "<1.11.0"
},
"autoload": {
@@ -53,9 +55,10 @@
]
},
"require-dev": {
- "pestphp/pest-dev-tools": "^3.4.0",
- "pestphp/pest-plugin-type-coverage": "^3.6.1",
- "symfony/process": "^7.3.0"
+ "pestphp/pest-dev-tools": "^4.0.0",
+ "pestphp/pest-plugin-browser": "^4.2.1",
+ "pestphp/pest-plugin-type-coverage": "^4.0.3",
+ "psy/psysh": "^0.12.18"
},
"minimum-stability": "dev",
"prefer-stable": true,
@@ -71,16 +74,17 @@
],
"scripts": {
"refacto": "rector",
- "lint": "pint",
+ "lint": "pint --parallel",
"test:refacto": "rector --dry-run",
- "test:lint": "pint --test",
+ "test:lint": "pint --parallel --test",
+ "test:profanity": "php bin/pest --profanity --compact",
"test:type:check": "phpstan analyse --ansi --memory-limit=-1 --debug",
"test:type:coverage": "php -d memory_limit=-1 bin/pest --type-coverage --min=100",
- "test:unit": "php bin/pest --colors=always --exclude-group=integration --compact",
- "test:inline": "php bin/pest --colors=always --configuration=phpunit.inline.xml",
- "test:parallel": "php bin/pest --colors=always --exclude-group=integration --parallel --processes=3",
- "test:integration": "php bin/pest --colors=always --group=integration -v",
- "update:snapshots": "REBUILD_SNAPSHOTS=true php bin/pest --colors=always --update-snapshots",
+ "test:unit": "php bin/pest --exclude-group=integration --compact",
+ "test:inline": "php bin/pest --configuration=phpunit.inline.xml",
+ "test:parallel": "php bin/pest --exclude-group=integration --parallel --processes=3",
+ "test:integration": "php bin/pest --group=integration -v",
+ "update:snapshots": "REBUILD_SNAPSHOTS=true php bin/pest --update-snapshots",
"test": [
"@test:refacto",
"@test:lint",
@@ -111,6 +115,7 @@
"Pest\\Plugins\\Snapshot",
"Pest\\Plugins\\Verbose",
"Pest\\Plugins\\Version",
+ "Pest\\Plugins\\Shard",
"Pest\\Plugins\\Parallel"
]
},
diff --git a/overrides/Event/Value/ThrowableBuilder.php b/overrides/Event/Value/ThrowableBuilder.php
index 21d428e96..d446d03c5 100644
--- a/overrides/Event/Value/ThrowableBuilder.php
+++ b/overrides/Event/Value/ThrowableBuilder.php
@@ -52,6 +52,8 @@
use PHPUnit\Util\ThrowableToStringMapper;
/**
+ * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
+ *
* @internal This class is not covered by the backward compatibility promise for PHPUnit
*/
final readonly class ThrowableBuilder
@@ -82,7 +84,7 @@ public static function from(\Throwable $t): Throwable
$t->getMessage(),
ThrowableToStringMapper::map($t),
$trace,
- $previous
+ $previous,
);
}
}
diff --git a/overrides/Runner/Filter/NameFilterIterator.php b/overrides/Runner/Filter/NameFilterIterator.php
index c637cc609..7834d9dbd 100644
--- a/overrides/Runner/Filter/NameFilterIterator.php
+++ b/overrides/Runner/Filter/NameFilterIterator.php
@@ -99,7 +99,9 @@ public function accept(): bool
}
if ($test instanceof HasPrintableTestCaseName) {
- $name = $test::getPrintableTestCaseName().'::'.$test->getPrintableTestCaseMethodName();
+ $name = trim(
+ $test::getPrintableTestCaseName().'::'.$test->getPrintableTestCaseMethodName().$test->dataSetAsString()
+ );
} else {
$name = $test::class.'::'.$test->nameWithDataSet();
}
diff --git a/overrides/Runner/ResultCache/DefaultResultCache.php b/overrides/Runner/ResultCache/DefaultResultCache.php
index 3eb41a834..7e7f359c6 100644
--- a/overrides/Runner/ResultCache/DefaultResultCache.php
+++ b/overrides/Runner/ResultCache/DefaultResultCache.php
@@ -72,10 +72,7 @@
*/
final class DefaultResultCache implements ResultCache
{
- /**
- * @var string
- */
- private const DEFAULT_RESULT_CACHE_FILENAME = '.phpunit.result.cache';
+ private const string DEFAULT_RESULT_CACHE_FILENAME = '.phpunit.result.cache';
private readonly string $cacheFilename;
diff --git a/overrides/TextUI/TestSuiteFilterProcessor.php b/overrides/TextUI/TestSuiteFilterProcessor.php
index 536ab2081..e13d5c98a 100644
--- a/overrides/TextUI/TestSuiteFilterProcessor.php
+++ b/overrides/TextUI/TestSuiteFilterProcessor.php
@@ -45,6 +45,7 @@
namespace PHPUnit\TextUI;
use Pest\Plugins\Only;
+use Pest\Runner\Filter\EnsureTestCaseIsInitiatedFilter;
use PHPUnit\Event;
use PHPUnit\Framework\TestSuite;
use PHPUnit\Runner\Filter\Factory;
@@ -66,6 +67,12 @@ public function process(Configuration $configuration, TestSuite $suite): void
{
$factory = new Factory;
+ // @phpstan-ignore-next-line
+ (fn () => $this->filters[] = [
+ 'className' => EnsureTestCaseIsInitiatedFilter::class,
+ 'argument' => '',
+ ])->call($factory);
+
if (! $configuration->hasFilter() &&
! $configuration->hasGroups() &&
! $configuration->hasExcludeGroups() &&
@@ -73,6 +80,8 @@ public function process(Configuration $configuration, TestSuite $suite): void
! $configuration->hasTestsCovering() &&
! $configuration->hasTestsUsing() &&
! Only::isEnabled()) {
+ $suite->injectFilter($factory);
+
return;
}
diff --git a/phpunit.xml b/phpunit.xml
index 4aac1aa26..122f54e22 100644
--- a/phpunit.xml
+++ b/phpunit.xml
@@ -16,6 +16,7 @@
./tests
+ ./tests-external
./tests/.snapshots
./tests/.tests
./tests/Fixtures/Inheritance
diff --git a/rector.php b/rector.php
index 191989dbb..caec31883 100644
--- a/rector.php
+++ b/rector.php
@@ -2,7 +2,10 @@
declare(strict_types=1);
+use Rector\CodingStyle\Rector\ArrowFunction\ArrowFunctionDelegatingCallToFirstClassCallableRector;
use Rector\Config\RectorConfig;
+use Rector\DeadCode\Rector\ClassMethod\RemoveParentDelegatingConstructorRector;
+use Rector\TypeDeclaration\Rector\ClassMethod\NarrowObjectReturnTypeRector;
use Rector\TypeDeclaration\Rector\ClassMethod\ReturnNeverTypeRector;
return RectorConfig::configure()
@@ -12,6 +15,9 @@
->withSkip([
__DIR__.'/src/Plugins/Parallel/Paratest/WrapperRunner.php',
ReturnNeverTypeRector::class,
+ ArrowFunctionDelegatingCallToFirstClassCallableRector::class,
+ NarrowObjectReturnTypeRector::class,
+ RemoveParentDelegatingConstructorRector::class,
])
->withPreparedSets(
deadCode: true,
diff --git a/resources/views/installers/plugin-browser.php b/resources/views/installers/plugin-browser.php
new file mode 100644
index 000000000..d717867b2
--- /dev/null
+++ b/resources/views/installers/plugin-browser.php
@@ -0,0 +1,22 @@
+
+
+ Using the visit() function requires the Pest Plugin Browser to be installed.
+
+ Run:
+
+
+
+ -
+ composer require pestphp/pest-plugin-browser:^4.0 --dev
+
+
+
+ -
+ npm install playwright@latest
+
+
+
+ -
+ npx playwright install
+
+
diff --git a/src/ArchPresets/Security.php b/src/ArchPresets/Security.php
index c71427488..40b4f59d0 100644
--- a/src/ArchPresets/Security.php
+++ b/src/ArchPresets/Security.php
@@ -32,7 +32,6 @@ public function execute(): void
'create_function',
'unserialize',
'extract',
- 'parse_str',
'mb_parse_str',
'dl',
'assert',
diff --git a/src/Bootstrappers/BootExcludeList.php b/src/Bootstrappers/BootExcludeList.php
index abd1552c6..69d9dce11 100644
--- a/src/Bootstrappers/BootExcludeList.php
+++ b/src/Bootstrappers/BootExcludeList.php
@@ -17,7 +17,7 @@ final class BootExcludeList implements Bootstrapper
*
* @var array
*/
- private const EXCLUDE_LIST = [
+ private const array EXCLUDE_LIST = [
'bin',
'overrides',
'resources',
diff --git a/src/Bootstrappers/BootFiles.php b/src/Bootstrappers/BootFiles.php
index 2017a7964..2f162eecb 100644
--- a/src/Bootstrappers/BootFiles.php
+++ b/src/Bootstrappers/BootFiles.php
@@ -5,6 +5,7 @@
namespace Pest\Bootstrappers;
use Pest\Contracts\Bootstrapper;
+use Pest\Exceptions\FatalException;
use Pest\Support\DatasetInfo;
use Pest\Support\Str;
use Pest\TestSuite;
@@ -24,7 +25,7 @@ final class BootFiles implements Bootstrapper
*
* @var array
*/
- private const STRUCTURE = [
+ private const array STRUCTURE = [
'Expectations',
'Expectations.php',
'Helpers',
@@ -40,6 +41,10 @@ public function boot(): void
$rootPath = TestSuite::getInstance()->rootPath;
$testsPath = $rootPath.DIRECTORY_SEPARATOR.testDirectory();
+ if (! is_dir($testsPath)) {
+ throw new FatalException(sprintf('The test directory [%s] does not exist.', $testsPath));
+ }
+
foreach (self::STRUCTURE as $filename) {
$filename = sprintf('%s%s%s', $testsPath, DIRECTORY_SEPARATOR, $filename);
@@ -78,7 +83,7 @@ private function load(string $filename): void
private function bootDatasets(string $testsPath): void
{
- assert(strlen($testsPath) > 0);
+ assert($testsPath !== '');
$files = (new PhpUnitFileIterator)->getFilesAsArray($testsPath, '.php');
diff --git a/src/Bootstrappers/BootOverrides.php b/src/Bootstrappers/BootOverrides.php
index efbcf7a35..28851f3a3 100644
--- a/src/Bootstrappers/BootOverrides.php
+++ b/src/Bootstrappers/BootOverrides.php
@@ -15,17 +15,17 @@ final class BootOverrides implements Bootstrapper
/**
* The list of files to be overridden.
*
- * @var array
+ * @var array
*/
- public const FILES = [
- '53c246e5f416a39817ac81124cdd64ea8403038d01d7a202e1ffa486fbdf3fa7' => 'Runner/Filter/NameFilterIterator.php',
- '77ffb7647b583bd82e37962c6fbdc4b04d3344d8a2c1ed103e625ed1ff7cb5c2' => 'Runner/ResultCache/DefaultResultCache.php',
- 'd0e81317889ad88c707db4b08a94cadee4c9010d05ff0a759f04e71af5efed89' => 'Runner/TestSuiteLoader.php',
- '3bb609b0d3bf6dee8df8d6cd62a3c8ece823c4bb941eaaae39e3cb267171b9d2' => 'TextUI/Command/Commands/WarmCodeCoverageCacheCommand.php',
- '8abdad6413329c6fe0d7d44a8b9926e390af32c0b3123f3720bb9c5bbc6fbb7e' => 'TextUI/Output/Default/ProgressPrinter/Subscriber/TestSkippedSubscriber.php',
- 'b4250fc3ffad5954624cb5e682fd940b874e8d3422fa1ee298bd7225e1aa5fc2' => 'TextUI/TestSuiteFilterProcessor.php',
- '8cfcb4999af79463eca51a42058e502ea4ddc776cba5677bf2f8eb6093e21a5c' => 'Event/Value/ThrowableBuilder.php',
- '86cd9bcaa53cdd59c5b13e58f30064a015c549501e7629d93b96893d4dee1eb1' => 'Logging/JUnit/JunitXmlLogger.php',
+ public const array FILES = [
+ 'Runner/Filter/NameFilterIterator.php',
+ 'Runner/ResultCache/DefaultResultCache.php',
+ 'Runner/TestSuiteLoader.php',
+ 'TextUI/Command/Commands/WarmCodeCoverageCacheCommand.php',
+ 'TextUI/Output/Default/ProgressPrinter/Subscriber/TestSkippedSubscriber.php',
+ 'TextUI/TestSuiteFilterProcessor.php',
+ 'Event/Value/ThrowableBuilder.php',
+ 'Logging/JUnit/JunitXmlLogger.php',
];
/**
diff --git a/src/Bootstrappers/BootSubscribers.php b/src/Bootstrappers/BootSubscribers.php
index 57f98e332..7877b2372 100644
--- a/src/Bootstrappers/BootSubscribers.php
+++ b/src/Bootstrappers/BootSubscribers.php
@@ -20,7 +20,7 @@
*
* @var array>
*/
- private const SUBSCRIBERS = [
+ private const array SUBSCRIBERS = [
Subscribers\EnsureConfigurationIsAvailable::class,
Subscribers\EnsureIgnorableTestCasesAreIgnored::class,
Subscribers\EnsureKernelDumpIsFlushed::class,
diff --git a/src/Concerns/Extendable.php b/src/Concerns/Extendable.php
index a2f7e40b7..1ff6626b9 100644
--- a/src/Concerns/Extendable.php
+++ b/src/Concerns/Extendable.php
@@ -8,6 +8,8 @@
/**
* @internal
+ *
+ * @template T of object
*/
trait Extendable
{
@@ -20,6 +22,8 @@ trait Extendable
/**
* Register a new extend.
+ *
+ * @param-closure-this T $extend
*/
public function extend(string $name, Closure $extend): void
{
diff --git a/src/Concerns/Testable.php b/src/Concerns/Testable.php
index 37d3b175a..767a7c696 100644
--- a/src/Concerns/Testable.php
+++ b/src/Concerns/Testable.php
@@ -6,10 +6,12 @@
use Closure;
use Pest\Exceptions\DatasetArgumentsMismatch;
+use Pest\Panic;
use Pest\Preset;
use Pest\Support\ChainableClosure;
use Pest\Support\ExceptionTrace;
use Pest\Support\Reflection;
+use Pest\Support\Shell;
use Pest\TestSuite;
use PHPUnit\Framework\Attributes\PostCondition;
use PHPUnit\Framework\TestCase;
@@ -101,27 +103,6 @@ trait Testable
*/
private array $__snapshotChanges = [];
- /**
- * Creates a new Test Case instance.
- */
- public function __construct(string $name)
- {
- parent::__construct($name);
-
- $test = TestSuite::getInstance()->tests->get(self::$__filename);
-
- if ($test->hasMethod($name)) {
- $method = $test->getMethod($name);
- $this->__description = self::$__latestDescription = $method->description;
- self::$__latestAssignees = $method->assignees;
- self::$__latestNotes = $method->notes;
- self::$__latestIssues = $method->issues;
- self::$__latestPrs = $method->prs;
- $this->__describing = $method->describing;
- $this->__test = $method->getClosure();
- }
- }
-
/**
* Resets the test case static properties.
*/
@@ -214,7 +195,11 @@ public static function setUpBeforeClass(): void
$beforeAll = ChainableClosure::boundStatically(self::$__beforeAll, $beforeAll);
}
- call_user_func(Closure::bind($beforeAll, null, self::class));
+ try {
+ call_user_func(Closure::bind($beforeAll, null, self::class));
+ } catch (Throwable $e) {
+ Panic::with($e);
+ }
}
/**
@@ -242,8 +227,6 @@ protected function setUp(...$arguments): void
$method = TestSuite::getInstance()->tests->get(self::$__filename)->getMethod($this->name());
- $method->setUp($this);
-
$description = $method->description;
if ($this->dataName()) {
$description = str_contains((string) $description, ':dataset')
@@ -285,6 +268,33 @@ protected function setUp(...$arguments): void
$this->__callClosure($beforeEach, $arguments);
}
+ /**
+ * Initialize test case properties from TestSuite.
+ */
+ public function __initializeTestCase(): void
+ {
+ // Return if the test case has already been initialized
+ if (isset($this->__test)) {
+ return;
+ }
+
+ $name = $this->name();
+ $test = TestSuite::getInstance()->tests->get(self::$__filename);
+
+ if ($test->hasMethod($name)) {
+ $method = $test->getMethod($name);
+ $this->__description = self::$__latestDescription = $method->description;
+ self::$__latestAssignees = $method->assignees;
+ self::$__latestNotes = $method->notes;
+ self::$__latestIssues = $method->issues;
+ self::$__latestPrs = $method->prs;
+ $this->__describing = $method->describing;
+ $this->__test = $method->getClosure();
+
+ $method->setUp($this);
+ }
+ }
+
/**
* Gets executed after the Test Case.
*/
@@ -434,15 +444,7 @@ protected function __MarkTestIncompleteIfSnapshotHaveChanged(): void
return;
}
- if (count($this->__snapshotChanges) === 1) {
- $this->markTestIncomplete($this->__snapshotChanges[0]);
-
- return;
- }
-
- $messages = implode(PHP_EOL, array_map(static fn (string $message): string => '- $message', $this->__snapshotChanges));
-
- $this->markTestIncomplete($messages);
+ $this->markTestIncomplete(implode('. ', $this->__snapshotChanges));
}
/**
@@ -466,7 +468,7 @@ public function getPrintableTestCaseMethodName(): string
*/
public static function getLatestPrintableTestCaseMethodName(): string
{
- return self::$__latestDescription;
+ return self::$__latestDescription ?? '';
}
/**
@@ -481,4 +483,12 @@ public static function getPrintableContext(): array
'notes' => self::$__latestNotes,
];
}
+
+ /**
+ * Opens a shell for the test case.
+ */
+ public function shell(): void
+ {
+ Shell::open();
+ }
}
diff --git a/src/Configuration.php b/src/Configuration.php
index c504aa65e..4261f3ef3 100644
--- a/src/Configuration.php
+++ b/src/Configuration.php
@@ -4,6 +4,7 @@
namespace Pest;
+use Pest\PendingCalls\BeforeEachCall;
use Pest\PendingCalls\UsesCall;
/**
@@ -62,6 +63,14 @@ public function group(string ...$groups): UsesCall
return (new UsesCall($this->filename, []))->group(...$groups);
}
+ /**
+ * Marks all tests in the current file to be run exclusively.
+ */
+ public function only(): void
+ {
+ (new BeforeEachCall(TestSuite::getInstance(), $this->filename))->only();
+ }
+
/**
* Depending on where is called, it will extend the given classes and traits globally or locally.
*/
@@ -102,6 +111,14 @@ public function project(): Configuration\Project
return Configuration\Project::getInstance();
}
+ /**
+ * Gets the browser configuration.
+ */
+ public function browser(): Browser\Configuration
+ {
+ return new Browser\Configuration;
+ }
+
/**
* Proxies calls to the uses method.
*
diff --git a/src/Console/Help.php b/src/Console/Help.php
index 3d09d5f55..50823d593 100644
--- a/src/Console/Help.php
+++ b/src/Console/Help.php
@@ -16,7 +16,7 @@
*
* @var array
*/
- private const HELP_MESSAGES = [
+ private const array HELP_MESSAGES = [
'Pest Options:',
' --init Initialise a standard Pest configuration',
' --coverage Enable coverage and output to standard output',
diff --git a/src/Console/Thanks.php b/src/Console/Thanks.php
index 7e68f8711..8b056e481 100644
--- a/src/Console/Thanks.php
+++ b/src/Console/Thanks.php
@@ -22,11 +22,11 @@
*
* @var array
*/
- private const FUNDING_MESSAGES = [
+ private const array FUNDING_MESSAGES = [
'Star' => 'https://github.com/pestphp/pest',
'YouTube' => 'https://youtube.com/@nunomaduro',
- 'TikTok' => 'https://tiktok.com/@nunomaduro',
- 'Twitch' => 'https://twitch.tv/enunomaduro',
+ 'TikTok' => 'https://tiktok.com/@enunomaduro',
+ 'Twitch' => 'https://twitch.tv/nunomaduro',
'LinkedIn' => 'https://linkedin.com/in/nunomaduro',
'Instagram' => 'https://instagram.com/enunomaduro',
'X' => 'https://x.com/enunomaduro',
diff --git a/src/Exceptions/DatasetMissing.php b/src/Exceptions/DatasetMissing.php
index dff803737..da738a448 100644
--- a/src/Exceptions/DatasetMissing.php
+++ b/src/Exceptions/DatasetMissing.php
@@ -22,7 +22,7 @@ final class DatasetMissing extends BadFunctionCallException implements Exception
public function __construct(string $file, string $name, array $arguments)
{
parent::__construct(sprintf(
- "A test with the description '%s' has %d argument(s) ([%s]) and no dataset(s) provided in %s",
+ 'A test with the description [%s] has [%d] argument(s) ([%s]) and no dataset(s) provided in [%s]',
$name,
count($arguments),
implode(', ', array_map(static fn (string $arg, string $type): string => sprintf('%s $%s', $type, $arg), array_keys($arguments), $arguments)),
diff --git a/src/Exceptions/TestCaseAlreadyInUse.php b/src/Exceptions/TestCaseAlreadyInUse.php
index 7ebf62324..926c8ea6f 100644
--- a/src/Exceptions/TestCaseAlreadyInUse.php
+++ b/src/Exceptions/TestCaseAlreadyInUse.php
@@ -19,7 +19,11 @@ final class TestCaseAlreadyInUse extends InvalidArgumentException implements Exc
*/
public function __construct(string $inUse, string $newOne, string $folder)
{
- parent::__construct(sprintf('Test case `%s` can not be used. The folder `%s` already uses the test case `%s`',
- $newOne, $folder, $inUse));
+ parent::__construct(sprintf(
+ 'Test case [%s] can not be used. The folder [%s] already uses the test case [%s].',
+ $newOne,
+ $folder,
+ $inUse,
+ ));
}
}
diff --git a/src/Exceptions/TestClosureMustNotBeStatic.php b/src/Exceptions/TestClosureMustNotBeStatic.php
index b1f91e954..5dda67c0c 100644
--- a/src/Exceptions/TestClosureMustNotBeStatic.php
+++ b/src/Exceptions/TestClosureMustNotBeStatic.php
@@ -22,7 +22,7 @@ public function __construct(TestCaseMethodFactory $method)
{
parent::__construct(
sprintf(
- 'Test closure must not be static. Please remove the `static` keyword from the `%s` method in `%s`.',
+ 'Test closure must not be static. Please remove the [static] keyword from the [%s] method in [%s].',
$method->description,
$method->filename
)
diff --git a/src/Expectation.php b/src/Expectation.php
index 1bef5a8c4..50729d7a0 100644
--- a/src/Expectation.php
+++ b/src/Expectation.php
@@ -52,7 +52,9 @@
*/
final class Expectation
{
+ /** @use Extendable> */
use Extendable;
+
use Pipeable;
use Retrievable;
@@ -330,7 +332,7 @@ public function when(callable|bool $condition, callable $callback): self
* @param array $parameters
* @return Expectation|HigherOrderExpectation, TValue>
*/
- public function __call(string $method, array $parameters): Expectation|HigherOrderExpectation|PendingArchExpectation
+ public function __call(string $method, array $parameters): Expectation|HigherOrderExpectation|PendingArchExpectation|ArchExpectation
{
if (! self::hasMethod($method)) {
if (! is_object($this->value) && method_exists(PendingArchExpectation::class, $method)) {
@@ -355,6 +357,10 @@ public function __call(string $method, array $parameters): Expectation|HigherOrd
$reflectionClosure = new \ReflectionFunction($closure);
$expectation = $reflectionClosure->getClosureThis();
+ if ($reflectionClosure->getReturnType()?->__toString() === ArchExpectation::class) {
+ return $closure(...$parameters);
+ }
+
assert(is_object($expectation));
ExpectationPipeline::for($closure)
@@ -393,7 +399,7 @@ private function getExpectationClosure(string $name): Closure
*
* @return Expectation|OppositeExpectation|EachExpectation|HigherOrderExpectation, TValue|null>|TValue
*/
- public function __get(string $name)
+ public function __get(string $name): mixed
{
if (! self::hasMethod($name)) {
if (! is_object($this->value) && method_exists(PendingArchExpectation::class, $name)) {
@@ -890,6 +896,14 @@ public function toUseNothing(): ArchExpectation
return ToUseNothing::make($this);
}
+ /**
+ * Asserts that the source code of the given expectation target does not include suspicious characters.
+ */
+ public function toHaveSuspiciousCharacters(): ArchExpectation
+ {
+ throw InvalidExpectation::fromMethods(['toHaveSuspiciousCharacters']);
+ }
+
/**
* Not supported.
*/
diff --git a/src/Expectations/OppositeExpectation.php b/src/Expectations/OppositeExpectation.php
index d5c3f083a..2713c7244 100644
--- a/src/Expectations/OppositeExpectation.php
+++ b/src/Expectations/OppositeExpectation.php
@@ -15,6 +15,7 @@
use Pest\Arch\SingleArchExpectation;
use Pest\Arch\Support\FileLineFinder;
use Pest\Exceptions\InvalidExpectation;
+use Pest\Exceptions\MissingDependency;
use Pest\Expectation;
use Pest\Support\Arr;
use Pest\Support\Exporter;
@@ -24,6 +25,7 @@
use PHPUnit\Framework\ExpectationFailedException;
use ReflectionMethod;
use ReflectionProperty;
+use Spoofchecker;
use stdClass;
/**
@@ -278,6 +280,28 @@ public function toHaveMethod(array|string $method): ArchExpectation
);
}
+ /**
+ * Asserts that the given expectation target does not have suspicious characters.
+ */
+ public function toHaveSuspiciousCharacters(): ArchExpectation
+ {
+ if (! class_exists(Spoofchecker::class)) {
+ throw new MissingDependency(__FUNCTION__, 'ext-intl >= 2.0');
+ }
+
+ $checker = new Spoofchecker;
+
+ /** @var Expectation|string> $original */
+ $original = $this->original;
+
+ return Targeted::make(
+ $original,
+ fn (ObjectDescription $object): bool => ! $checker->isSuspicious((string) file_get_contents($object->path)),
+ 'to not include suspicious characters',
+ FileLineFinder::where(fn (string $line): bool => $checker->isSuspicious($line)),
+ );
+ }
+
/**
* Asserts that the given expectation target does not have the given methods.
*
diff --git a/src/Factories/Covers/CoversNothing.php b/src/Factories/Covers/CoversNothing.php
deleted file mode 100644
index 4215edbc1..000000000
--- a/src/Factories/Covers/CoversNothing.php
+++ /dev/null
@@ -1,10 +0,0 @@
-
+ * @var array
*/
public array $describing = [];
diff --git a/src/Functions.php b/src/Functions.php
index 1e12fe7e6..0ed631f36 100644
--- a/src/Functions.php
+++ b/src/Functions.php
@@ -2,11 +2,14 @@
declare(strict_types=1);
+use Pest\Browser\Api\ArrayablePendingAwaitablePage;
+use Pest\Browser\Api\PendingAwaitablePage;
use Pest\Concerns\Expectable;
use Pest\Configuration;
use Pest\Exceptions\AfterAllWithinDescribe;
use Pest\Exceptions\BeforeAllWithinDescribe;
use Pest\Expectation;
+use Pest\Installers\PluginBrowser;
use Pest\Mutate\Contracts\MutationTestRunner;
use Pest\Mutate\Repositories\ConfigurationRepository;
use Pest\PendingCalls\AfterEachCall;
@@ -18,6 +21,7 @@
use Pest\Support\Backtrace;
use Pest\Support\Container;
use Pest\Support\DatasetInfo;
+use Pest\Support\Description;
use Pest\Support\HigherOrderTapProxy;
use Pest\TestSuite;
use PHPUnit\Framework\TestCase;
@@ -95,7 +99,7 @@ function describe(string $description, Closure $tests): DescribeCall
{
$filename = Backtrace::testFile();
- return new DescribeCall(TestSuite::getInstance(), $filename, $description, $tests);
+ return new DescribeCall(TestSuite::getInstance(), $filename, new Description($description), $tests);
}
}
@@ -278,3 +282,51 @@ function mutates(array|string ...$targets): void
}
}
}
+
+if (! function_exists('fixture')) {
+ /**
+ * Returns the absolute path to a fixture file.
+ */
+ function fixture(string $file): string
+ {
+ $file = implode(DIRECTORY_SEPARATOR, [
+ TestSuite::getInstance()->rootPath,
+ TestSuite::getInstance()->testPath,
+ 'Fixtures',
+ str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $file),
+ ]);
+
+ $fileRealPath = realpath($file);
+
+ if ($fileRealPath === false) {
+ throw new InvalidArgumentException(
+ 'The fixture file ['.$file.'] does not exist.',
+ );
+ }
+
+ return $fileRealPath;
+ }
+}
+
+if (! function_exists('visit')) {
+ /**
+ * Browse to the given URL.
+ *
+ * @template TUrl of array|string
+ *
+ * @param TUrl $url
+ * @param array $options
+ * @return (TUrl is array ? ArrayablePendingAwaitablePage : PendingAwaitablePage)
+ */
+ function visit(array|string $url, array $options = []): ArrayablePendingAwaitablePage|PendingAwaitablePage
+ {
+ if (! class_exists(\Pest\Browser\Configuration::class)) {
+ PluginBrowser::install();
+
+ exit(0);
+ }
+
+ // @phpstan-ignore-next-line
+ return test()->visit($url, $options);
+ }
+}
diff --git a/src/Installers/PluginBrowser.php b/src/Installers/PluginBrowser.php
new file mode 100644
index 000000000..2d36ed3da
--- /dev/null
+++ b/src/Installers/PluginBrowser.php
@@ -0,0 +1,15 @@
+
*/
- private const BOOTSTRAPPERS = [
+ private const array BOOTSTRAPPERS = [
Bootstrappers\BootOverrides::class,
Bootstrappers\BootSubscribers::class,
Bootstrappers\BootFiles::class,
@@ -71,7 +71,7 @@ public static function boot(TestSuite $testSuite, InputInterface $input, OutputI
$output,
);
- register_shutdown_function(fn () => $kernel->shutdown());
+ register_shutdown_function($kernel->shutdown(...));
foreach (self::BOOTSTRAPPERS as $bootstrapper) {
$bootstrapper = Container::getInstance()->get($bootstrapper);
diff --git a/src/Logging/Converter.php b/src/Logging/Converter.php
index b4560e227..50a42d1ac 100644
--- a/src/Logging/Converter.php
+++ b/src/Logging/Converter.php
@@ -31,7 +31,7 @@
/**
* The prefix for the test suite name.
*/
- private const PREFIX = 'P\\';
+ private const string PREFIX = 'P\\';
/**
* The state generator.
@@ -131,7 +131,7 @@ public function getStackTrace(Throwable $throwable): string
// clean the paths of each frame.
$frames = array_map(
- fn (string $frame): string => $this->toRelativePath($frame),
+ $this->toRelativePath(...),
$frames
);
diff --git a/src/Mixins/Expectation.php b/src/Mixins/Expectation.php
index 1566b59c9..09974787c 100644
--- a/src/Mixins/Expectation.php
+++ b/src/Mixins/Expectation.php
@@ -781,15 +781,13 @@ public function toMatchArray(iterable $array, string $message = ''): self
foreach ($array as $key => $value) {
Assert::assertArrayHasKey($key, $valueAsArray, $message);
- if ($message === '') {
- $message = sprintf(
- 'Failed asserting that an array has a key %s with the value %s.',
- $this->export($key),
- $this->export($valueAsArray[$key]),
- );
- }
+ $assertMessage = $message !== '' ? $message : sprintf(
+ 'Failed asserting that an array has a key %s with the value %s.',
+ $this->export($key),
+ $this->export($valueAsArray[$key]),
+ );
- Assert::assertEquals($value, $valueAsArray[$key], $message);
+ Assert::assertEquals($value, $valueAsArray[$key], $assertMessage);
}
return $this;
@@ -802,7 +800,7 @@ public function toMatchArray(iterable $array, string $message = ''): self
* @param iterable $object
* @return self
*/
- public function toMatchObject(iterable $object, string $message = ''): self
+ public function toMatchObject(object|iterable $object, string $message = ''): self
{
foreach ((array) $object as $property => $value) {
if (! is_object($this->value) && ! is_string($this->value)) {
@@ -814,15 +812,13 @@ public function toMatchObject(iterable $object, string $message = ''): self
/* @phpstan-ignore-next-line */
$propertyValue = $this->value->{$property};
- if ($message === '') {
- $message = sprintf(
- 'Failed asserting that an object has a property %s with the value %s.',
- $this->export($property),
- $this->export($propertyValue),
- );
- }
+ $assertMessage = $message !== '' ? $message : sprintf(
+ 'Failed asserting that an object has a property %s with the value %s.',
+ $this->export($property),
+ $this->export($propertyValue),
+ );
- Assert::assertEquals($value, $propertyValue, $message);
+ Assert::assertEquals($value, $propertyValue, $assertMessage);
}
return $this;
@@ -1158,4 +1154,21 @@ public function toBeUrl(string $message = ''): self
return $this;
}
+
+ /**
+ * Asserts that the value can be converted to a slug
+ *
+ * @return self
+ */
+ public function toBeSlug(string $message = ''): self
+ {
+ if ($message === '') {
+ $message = "Failed asserting that {$this->value} can be converted to a slug.";
+ }
+
+ $slug = Str::slugify((string) $this->value);
+ Assert::assertNotEmpty($slug, $message);
+
+ return $this;
+ }
}
diff --git a/src/PendingCalls/Concerns/Describable.php b/src/PendingCalls/Concerns/Describable.php
index 06a7eab71..0208ea4b5 100644
--- a/src/PendingCalls/Concerns/Describable.php
+++ b/src/PendingCalls/Concerns/Describable.php
@@ -12,14 +12,14 @@ trait Describable
/**
* Note: this is property is not used; however, it gets added automatically by rector php.
*
- * @var array
+ * @var array
*/
public array $__describing;
/**
* The describing of the test case.
*
- * @var array
+ * @var array
*/
public array $describing = [];
}
diff --git a/src/PendingCalls/DescribeCall.php b/src/PendingCalls/DescribeCall.php
index de4729603..08ebc15e0 100644
--- a/src/PendingCalls/DescribeCall.php
+++ b/src/PendingCalls/DescribeCall.php
@@ -6,6 +6,7 @@
use Closure;
use Pest\Support\Backtrace;
+use Pest\Support\Description;
use Pest\TestSuite;
/**
@@ -16,7 +17,7 @@ final class DescribeCall
/**
* The current describe call.
*
- * @var array
+ * @var array
*/
private static array $describing = [];
@@ -31,7 +32,7 @@ final class DescribeCall
public function __construct(
public readonly TestSuite $testSuite,
public readonly string $filename,
- public readonly string $description,
+ public readonly Description $description,
public readonly Closure $tests
) {
//
@@ -40,7 +41,7 @@ public function __construct(
/**
* What is the current describing.
*
- * @return array
+ * @return array
*/
public static function describing(): array
{
diff --git a/src/PendingCalls/TestCall.php b/src/PendingCalls/TestCall.php
index 50fef3b68..a65c8bb58 100644
--- a/src/PendingCalls/TestCall.php
+++ b/src/PendingCalls/TestCall.php
@@ -12,6 +12,7 @@
use Pest\Factories\TestCaseMethodFactory;
use Pest\Mutate\Repositories\ConfigurationRepository;
use Pest\PendingCalls\Concerns\Describable;
+use Pest\Plugins\Environment;
use Pest\Plugins\Only;
use Pest\Support\Backtrace;
use Pest\Support\Container;
@@ -178,10 +179,9 @@ public function throwsUnless(callable|bool $condition, string|int $exception, ?s
}
/**
- * Runs the current test multiple times with
- * each item of the given `iterable`.
+ * Runs the current test multiple times with each item of the given `iterable`.
*
- * @param array<\Closure|iterable|string> $data
+ * @param Closure|iterable|string $data
*/
public function with(Closure|iterable|string ...$data): self
{
@@ -315,6 +315,61 @@ private function skipOnOs(string $osFamily, string $message): self
: $this;
}
+ /**
+ * Weather the current test is running on a CI environment.
+ */
+ private function runningOnCI(): bool
+ {
+ foreach ([
+ 'CI',
+ 'GITHUB_ACTIONS',
+ 'GITLAB_CI',
+ 'CIRCLECI',
+ 'TRAVIS',
+ 'APPVEYOR',
+ 'BITBUCKET_BUILD_NUMBER',
+ 'BUILDKITE',
+ 'TEAMCITY_VERSION',
+ 'JENKINS_URL',
+ 'SYSTEM_COLLECTIONURI',
+ 'CI_NAME',
+ 'TASKCLUSTER_ROOT_URL',
+ 'DRONE',
+ 'WERCKER',
+ 'NEVERCODE',
+ 'SEMAPHORE',
+ 'NETLIFY',
+ 'NOW_BUILDER',
+ ] as $env) {
+ if (getenv($env) !== false) {
+ return true;
+ }
+ }
+
+ return Environment::name() === Environment::CI;
+ }
+
+ /**
+ * Skips the current test when running on a CI environments.
+ */
+ public function skipOnCI(): self
+ {
+ if ($this->runningOnCI()) {
+ return $this->skip('This test is skipped on [CI].');
+ }
+
+ return $this;
+ }
+
+ public function skipLocally(): self
+ {
+ if ($this->runningOnCI() === false) {
+ return $this->skip('This test is skipped [locally].');
+ }
+
+ return $this;
+ }
+
/**
* Skips the current test unless the given test is running on Windows.
*/
@@ -604,18 +659,29 @@ public function coversFunction(string ...$functions): self
}
/**
- * Sets that the current test covers nothing.
+ * Adds one or more references to the tested method or class. This helps
+ * to link test cases to the source code for easier navigation.
+ *
+ * @param array|class-string ...$classes
*/
- public function coversNothing(): self
+ public function references(string|array ...$classes): self
{
- $this->testCaseMethod->attributes[] = new Attribute(
- \PHPUnit\Framework\Attributes\CoversNothing::class,
- [],
- );
+ assert($classes !== []);
return $this;
}
+ /**
+ * Adds one or more references to the tested method or class. This helps
+ * to link test cases to the source code for easier navigation.
+ *
+ * @param array|class-string ...$classes
+ */
+ public function see(string|array ...$classes): self
+ {
+ return $this->references(...$classes);
+ }
+
/**
* Informs the test runner that no expectations happen in this test,
* and its purpose is simply to check whether the given code can
@@ -693,7 +759,12 @@ public function __destruct()
$this->testSuite->tests->set($this->testCaseMethod);
if (! is_null($testCase = $this->testSuite->tests->get($this->filename))) {
- $testCase->attributes = array_merge($testCase->attributes, $this->testCaseFactoryAttributes);
+ $attributesToMerge = array_filter(
+ $this->testCaseFactoryAttributes,
+ fn (Attribute $attributeToMerge): bool => array_filter($testCase->attributes, fn (Attribute $attribute): bool => serialize($attributeToMerge) === serialize($attribute)) === []
+ );
+
+ $testCase->attributes = array_merge($testCase->attributes, $attributesToMerge);
}
}
}
diff --git a/src/Pest.php b/src/Pest.php
index 0199acc72..ba3a540e4 100644
--- a/src/Pest.php
+++ b/src/Pest.php
@@ -6,7 +6,7 @@
function version(): string
{
- return '3.8.4';
+ return '4.3.2';
}
function testDirectory(string $file = ''): string
diff --git a/src/Plugins/Cache.php b/src/Plugins/Cache.php
index ea3abb78f..3ae0433b8 100644
--- a/src/Plugins/Cache.php
+++ b/src/Plugins/Cache.php
@@ -21,7 +21,7 @@ final class Cache implements HandlesArguments
/**
* The temporary folder.
*/
- private const TEMPORARY_FOLDER = __DIR__
+ private const string TEMPORARY_FOLDER = __DIR__
.DIRECTORY_SEPARATOR
.'..'
.DIRECTORY_SEPARATOR
diff --git a/src/Plugins/Configuration.php b/src/Plugins/Configuration.php
index acae8fb41..54c9627ff 100644
--- a/src/Plugins/Configuration.php
+++ b/src/Plugins/Configuration.php
@@ -21,7 +21,7 @@ final class Configuration implements HandlesArguments, Terminable
/**
* The base PHPUnit file.
*/
- public const BASE_PHPUNIT_FILE = __DIR__
+ public const string BASE_PHPUNIT_FILE = __DIR__
.DIRECTORY_SEPARATOR
.'..'
.DIRECTORY_SEPARATOR
@@ -34,7 +34,7 @@ final class Configuration implements HandlesArguments, Terminable
*/
public function handleArguments(array $arguments): array
{
- if ($this->hasArgument('--configuration', $arguments) || $this->hasCustomConfigurationFile()) {
+ if ($this->hasArgument('--configuration', $arguments) || $this->hasArgument('-c', $arguments) || $this->hasCustomConfigurationFile()) {
return $arguments;
}
diff --git a/src/Plugins/Coverage.php b/src/Plugins/Coverage.php
index a5061d258..712f5de55 100644
--- a/src/Plugins/Coverage.php
+++ b/src/Plugins/Coverage.php
@@ -17,20 +17,11 @@
*/
final class Coverage implements AddsOutput, HandlesArguments
{
- /**
- * @var string
- */
- private const COVERAGE_OPTION = 'coverage';
+ private const string COVERAGE_OPTION = 'coverage';
- /**
- * @var string
- */
- private const MIN_OPTION = 'min';
+ private const string MIN_OPTION = 'min';
- /**
- * @var string
- */
- private const EXACTLY_OPTION = 'exactly';
+ private const string EXACTLY_OPTION = 'exactly';
/**
* Whether it should show the coverage or not.
diff --git a/src/Plugins/Environment.php b/src/Plugins/Environment.php
index 8ff10b4c1..7edbbbd3d 100644
--- a/src/Plugins/Environment.php
+++ b/src/Plugins/Environment.php
@@ -14,12 +14,12 @@ final class Environment implements HandlesArguments
/**
* The continuous integration environment.
*/
- public const CI = 'ci';
+ public const string CI = 'ci';
/**
* The local environment.
*/
- public const LOCAL = 'local';
+ public const string LOCAL = 'local';
/**
* The current environment.
diff --git a/src/Plugins/Help.php b/src/Plugins/Help.php
index 89a47b66e..096f2914e 100644
--- a/src/Plugins/Help.php
+++ b/src/Plugins/Help.php
@@ -99,6 +99,7 @@ private function getContent(): array
{
$helpReflection = new PHPUnitHelp;
+ // @phpstan-ignore-next-line
$content = (fn (): array => $this->elements())->call($helpReflection);
$content['Configuration'] = [...[[
@@ -141,6 +142,9 @@ private function getContent(): array
], [
'arg' => '--retry',
'desc' => 'Run non-passing tests first and stop execution upon first error or failure',
+ ], [
+ 'arg' => '--dirty',
+ 'desc' => 'Only run tests that have uncommitted changes according to Git',
], ...$content['Selection']];
$content['Reporting'] = [...$content['Reporting'], ...[
diff --git a/src/Plugins/Init.php b/src/Plugins/Init.php
index eb87b0869..c31dd7599 100644
--- a/src/Plugins/Init.php
+++ b/src/Plugins/Init.php
@@ -20,12 +20,12 @@
/**
* The option the triggers the init job.
*/
- private const INIT_OPTION = '--init';
+ private const string INIT_OPTION = '--init';
/**
* The files that will be created.
*/
- private const STUBS = [
+ private const array STUBS = [
'phpunit.xml.stub' => 'phpunit.xml',
'Pest.php.stub' => 'tests/Pest.php',
'TestCase.php.stub' => 'tests/TestCase.php',
diff --git a/src/Plugins/Only.php b/src/Plugins/Only.php
index 0d9581736..fd1001de0 100644
--- a/src/Plugins/Only.php
+++ b/src/Plugins/Only.php
@@ -5,7 +5,10 @@
namespace Pest\Plugins;
use Pest\Contracts\Plugins\Terminable;
+use Pest\Factories\Attribute;
+use Pest\Factories\TestCaseMethodFactory;
use Pest\PendingCalls\TestCall;
+use PHPUnit\Framework\Attributes\Group;
/**
* @internal
@@ -15,7 +18,7 @@ final class Only implements Terminable
/**
* The temporary folder.
*/
- private const TEMPORARY_FOLDER = __DIR__
+ private const string TEMPORARY_FOLDER = __DIR__
.DIRECTORY_SEPARATOR
.'..'
.DIRECTORY_SEPARATOR
@@ -23,28 +26,19 @@ final class Only implements Terminable
.DIRECTORY_SEPARATOR
.'.temp';
- /**
- * {@inheritDoc}
- */
- public function terminate(): void
- {
- if (Parallel::isWorker()) {
- return;
- }
-
- $lockFile = self::TEMPORARY_FOLDER.DIRECTORY_SEPARATOR.'only.lock';
-
- if (file_exists($lockFile)) {
- unlink($lockFile);
- }
- }
-
/**
* Creates the lock file.
*/
- public static function enable(TestCall $testCall, string $group = '__pest_only'): void
+ public static function enable(TestCall|TestCaseMethodFactory $testCall, string $group = '__pest_only'): void
{
- $testCall->group($group);
+ if ($testCall instanceof TestCall) {
+ $testCall->group($group);
+ } else {
+ $testCall->attributes[] = new Attribute(
+ Group::class,
+ [$group],
+ );
+ }
if (Environment::name() === Environment::CI || Parallel::isWorker()) {
return;
@@ -88,4 +82,20 @@ public static function group(): string
return file_get_contents($lockFile) ?: '__pest_only'; // @phpstan-ignore-line
}
+
+ /**
+ * {@inheritDoc}
+ */
+ public function terminate(): void
+ {
+ if (Parallel::isWorker()) {
+ return;
+ }
+
+ $lockFile = self::TEMPORARY_FOLDER.DIRECTORY_SEPARATOR.'only.lock';
+
+ if (file_exists($lockFile)) {
+ unlink($lockFile);
+ }
+ }
}
diff --git a/src/Plugins/Parallel.php b/src/Plugins/Parallel.php
index 1632a0504..949028236 100644
--- a/src/Plugins/Parallel.php
+++ b/src/Plugins/Parallel.php
@@ -23,9 +23,9 @@ final class Parallel implements HandlesArguments
{
use HandleArguments;
- private const GLOBAL_PREFIX = 'PEST_PARALLEL_GLOBAL_';
+ private const string GLOBAL_PREFIX = 'PEST_PARALLEL_GLOBAL_';
- private const HANDLERS = [
+ private const array HANDLERS = [
Parallel\Handlers\Parallel::class,
Parallel\Handlers\Pest::class,
Parallel\Handlers\Laravel::class,
@@ -34,7 +34,7 @@ final class Parallel implements HandlesArguments
/**
* @var string[]
*/
- private const UNSUPPORTED_ARGUMENTS = ['--todo', '--todos', '--retry', '--notes', '--issue', '--pr', '--pull-request'];
+ private const array UNSUPPORTED_ARGUMENTS = ['--todo', '--todos', '--retry', '--notes', '--issue', '--pr', '--pull-request'];
/**
* Whether the given command line arguments indicate that the test suite should be run in parallel.
diff --git a/src/Plugins/Parallel/Handlers/Parallel.php b/src/Plugins/Parallel/Handlers/Parallel.php
index 76a59af6c..d99139b2b 100644
--- a/src/Plugins/Parallel/Handlers/Parallel.php
+++ b/src/Plugins/Parallel/Handlers/Parallel.php
@@ -18,7 +18,7 @@ final class Parallel implements HandlesArguments
/**
* The list of arguments to remove.
*/
- private const ARGS_TO_REMOVE = [
+ private const array ARGS_TO_REMOVE = [
'--parallel',
'-p',
'--no-output',
diff --git a/src/Plugins/Parallel/Paratest/CleanConsoleOutput.php b/src/Plugins/Parallel/Paratest/CleanConsoleOutput.php
index d2801ced5..cf5272b15 100644
--- a/src/Plugins/Parallel/Paratest/CleanConsoleOutput.php
+++ b/src/Plugins/Parallel/Paratest/CleanConsoleOutput.php
@@ -11,6 +11,7 @@ final class CleanConsoleOutput extends ConsoleOutput
/**
* {@inheritdoc}
*/
+ #[\Override]
protected function doWrite(string $message, bool $newline): void // @pest-arch-ignore-line
{
if ($this->isOpeningHeadline($message)) {
diff --git a/src/Plugins/Parallel/Paratest/ResultPrinter.php b/src/Plugins/Parallel/Paratest/ResultPrinter.php
index bd416e1e7..e7a1c24de 100644
--- a/src/Plugins/Parallel/Paratest/ResultPrinter.php
+++ b/src/Plugins/Parallel/Paratest/ResultPrinter.php
@@ -59,10 +59,10 @@ public function __construct(
private readonly OutputInterface $output,
private readonly Options $options
) {
- $this->printer = new class($this->output) implements Printer
+ $this->printer = new readonly class($this->output) implements Printer
{
public function __construct(
- private readonly OutputInterface $output,
+ private OutputInterface $output,
) {}
public function print(string $buffer): void
diff --git a/src/Plugins/Parallel/Paratest/WrapperRunner.php b/src/Plugins/Parallel/Paratest/WrapperRunner.php
index 282749d32..469f2aa6f 100644
--- a/src/Plugins/Parallel/Paratest/WrapperRunner.php
+++ b/src/Plugins/Parallel/Paratest/WrapperRunner.php
@@ -17,6 +17,7 @@
use Pest\Result;
use Pest\TestSuite;
use PHPUnit\Event\Facade as EventFacade;
+use PHPUnit\Event\Test\AfterLastTestMethodFailed;
use PHPUnit\Event\TestRunner\WarningTriggered;
use PHPUnit\Runner\CodeCoverage;
use PHPUnit\Runner\ResultCache\DefaultResultCache;
@@ -50,7 +51,7 @@ final class WrapperRunner implements RunnerInterface
/**
* The time to sleep between cycles.
*/
- private const CYCLE_SLEEP = 10000;
+ private const int CYCLE_SLEEP = 10000;
/**
* The result printer.
@@ -313,27 +314,42 @@ private function complete(TestResult $testResultSum): int
$testResult = unserialize($contents);
assert($testResult instanceof TestResult);
+ /** @var list $failedEvents */
+ $failedEvents = array_merge_recursive($testResultSum->testFailedEvents(), $testResult->testFailedEvents());
+
$testResultSum = new TestResult(
(int) $testResultSum->hasTests() + (int) $testResult->hasTests(),
$testResultSum->numberOfTestsRun() + $testResult->numberOfTestsRun(),
$testResultSum->numberOfAssertions() + $testResult->numberOfAssertions(),
array_merge_recursive($testResultSum->testErroredEvents(), $testResult->testErroredEvents()),
- array_merge_recursive($testResultSum->testFailedEvents(), $testResult->testFailedEvents()),
+ $failedEvents,
array_merge_recursive($testResultSum->testConsideredRiskyEvents(), $testResult->testConsideredRiskyEvents()),
array_merge_recursive($testResultSum->testSuiteSkippedEvents(), $testResult->testSuiteSkippedEvents()),
array_merge_recursive($testResultSum->testSkippedEvents(), $testResult->testSkippedEvents()),
array_merge_recursive($testResultSum->testMarkedIncompleteEvents(), $testResult->testMarkedIncompleteEvents()),
array_merge_recursive($testResultSum->testTriggeredPhpunitDeprecationEvents(), $testResult->testTriggeredPhpunitDeprecationEvents()),
array_merge_recursive($testResultSum->testTriggeredPhpunitErrorEvents(), $testResult->testTriggeredPhpunitErrorEvents()),
+ array_merge_recursive($testResultSum->testTriggeredPhpunitNoticeEvents(), $testResult->testTriggeredPhpunitNoticeEvents()),
array_merge_recursive($testResultSum->testTriggeredPhpunitWarningEvents(), $testResult->testTriggeredPhpunitWarningEvents()),
+ // @phpstan-ignore-next-line
array_merge_recursive($testResultSum->testRunnerTriggeredDeprecationEvents(), $testResult->testRunnerTriggeredDeprecationEvents()),
+ // @phpstan-ignore-next-line
+ array_merge_recursive($testResultSum->testRunnerTriggeredNoticeEvents(), $testResult->testRunnerTriggeredNoticeEvents()),
+ // @phpstan-ignore-next-line
array_merge_recursive($testResultSum->testRunnerTriggeredWarningEvents(), $testResult->testRunnerTriggeredWarningEvents()),
+ // @phpstan-ignore-next-line
array_merge_recursive($testResultSum->errors(), $testResult->errors()),
+ // @phpstan-ignore-next-line
array_merge_recursive($testResultSum->deprecations(), $testResult->deprecations()),
+ // @phpstan-ignore-next-line
array_merge_recursive($testResultSum->notices(), $testResult->notices()),
+ // @phpstan-ignore-next-line
array_merge_recursive($testResultSum->warnings(), $testResult->warnings()),
+ // @phpstan-ignore-next-line
array_merge_recursive($testResultSum->phpDeprecations(), $testResult->phpDeprecations()),
+ // @phpstan-ignore-next-line
array_merge_recursive($testResultSum->phpNotices(), $testResult->phpNotices()),
+ // @phpstan-ignore-next-line
array_merge_recursive($testResultSum->phpWarnings(), $testResult->phpWarnings()),
$testResultSum->numberOfIssuesIgnoredByBaseline() + $testResult->numberOfIssuesIgnoredByBaseline(),
);
@@ -351,8 +367,10 @@ private function complete(TestResult $testResultSum): int
$testResultSum->testMarkedIncompleteEvents(),
$testResultSum->testTriggeredPhpunitDeprecationEvents(),
$testResultSum->testTriggeredPhpunitErrorEvents(),
+ $testResultSum->testTriggeredPhpunitNoticeEvents(),
$testResultSum->testTriggeredPhpunitWarningEvents(),
$testResultSum->testRunnerTriggeredDeprecationEvents(),
+ $testResultSum->testRunnerTriggeredNoticeEvents(),
array_values(array_filter(
$testResultSum->testRunnerTriggeredWarningEvents(),
fn (WarningTriggered $event): bool => ! str_contains($event->message(), 'No tests found')
diff --git a/src/Plugins/Parallel/Support/CompactPrinter.php b/src/Plugins/Parallel/Support/CompactPrinter.php
index 25226b10e..aa2da210c 100644
--- a/src/Plugins/Parallel/Support/CompactPrinter.php
+++ b/src/Plugins/Parallel/Support/CompactPrinter.php
@@ -34,7 +34,7 @@ final class CompactPrinter
/**
* @var array>
*/
- private const LOOKUP_TABLE = [
+ private const array LOOKUP_TABLE = [
'.' => ['gray', '.'],
'S' => ['yellow', 's'],
'T' => ['cyan', 't'],
@@ -131,14 +131,14 @@ public function recap(State $state, PHPUnitTestResult $testResult, Duration $dur
$status['collected'],
$status['threshold'],
$status['roots'],
- null,
- null,
- null,
- null,
- null,
- null,
- null,
- null,
+ 0.00,
+ 0.00,
+ 0.00,
+ 0.00,
+ false,
+ false,
+ false,
+ 0,
);
$telemetry = new Info(
diff --git a/src/Plugins/Shard.php b/src/Plugins/Shard.php
new file mode 100644
index 000000000..f48260bb5
--- /dev/null
+++ b/src/Plugins/Shard.php
@@ -0,0 +1,177 @@
+hasArgument('--shard', $arguments)) {
+ return $arguments;
+ }
+
+ // @phpstan-ignore-next-line
+ $input = new ArgvInput($arguments);
+
+ ['index' => $index, 'total' => $total] = self::getShard($input);
+
+ $arguments = $this->popArgument("--shard=$index/$total", $this->popArgument('--shard', $this->popArgument(
+ "$index/$total",
+ $arguments,
+ )));
+
+ /** @phpstan-ignore-next-line */
+ $tests = $this->allTests($arguments);
+ $testsToRun = (array_chunk($tests, max(1, (int) ceil(count($tests) / $total))))[$index - 1] ?? [];
+
+ self::$shard = [
+ 'index' => $index,
+ 'total' => $total,
+ 'testsRan' => count($testsToRun),
+ 'testsCount' => count($tests),
+ ];
+
+ return [...$arguments, '--filter', $this->buildFilterArgument($testsToRun)];
+ }
+
+ /**
+ * Returns all tests that the test suite would run.
+ *
+ * @param list $arguments
+ * @return list
+ */
+ private function allTests(array $arguments): array
+ {
+ $output = (new Process([
+ 'php',
+ ...$this->removeParallelArguments($arguments),
+ '--list-tests',
+ ]))->mustRun()->getOutput();
+
+ preg_match_all('/ - (?:P\\\\)?(Tests\\\\[^:]+)::/', $output, $matches);
+
+ return array_values(array_unique($matches[1]));
+ }
+
+ /**
+ * @param array $arguments
+ * @return array
+ */
+ private function removeParallelArguments(array $arguments): array
+ {
+ return array_filter($arguments, fn (string $argument): bool => ! in_array($argument, ['--parallel', '-p'], strict: true));
+ }
+
+ /**
+ * Builds the filter argument for the given tests to run.
+ */
+ private function buildFilterArgument(mixed $testsToRun): string
+ {
+ return addslashes(implode('|', $testsToRun));
+ }
+
+ /**
+ * Adds output after the Test Suite execution.
+ */
+ public function addOutput(int $exitCode): int
+ {
+ if (self::$shard === null) {
+ return $exitCode;
+ }
+
+ [
+ 'index' => $index,
+ 'total' => $total,
+ 'testsRan' => $testsRan,
+ 'testsCount' => $testsCount,
+ ] = self::$shard;
+
+ $this->output->writeln(sprintf(
+ ' Shard:> %d of %d> — %d file%s ran, out of %d.',
+ $index,
+ $total,
+ $testsRan,
+ $testsRan === 1 ? '' : 's',
+ $testsCount,
+ ));
+
+ return $exitCode;
+ }
+
+ /**
+ * Returns the shard information.
+ *
+ * @return array{index: int, total: int}
+ */
+ public static function getShard(InputInterface $input): array
+ {
+ if ($input->hasParameterOption('--'.self::SHARD_OPTION)) {
+ $shard = $input->getParameterOption('--'.self::SHARD_OPTION);
+ } else {
+ $shard = null;
+ }
+
+ if (! is_string($shard) || ! preg_match('/^\d+\/\d+$/', $shard)) {
+ throw new InvalidOption('The [--shard] option must be in the format "index/total".');
+ }
+
+ [$index, $total] = explode('/', $shard);
+
+ if (! is_numeric($index) || ! is_numeric($total)) {
+ throw new InvalidOption('The [--shard] option must be in the format "index/total".');
+ }
+
+ if ($index <= 0 || $total <= 0 || $index > $total) {
+ throw new InvalidOption('The [--shard] option index must be a non-negative integer less than the total number of shards.');
+ }
+
+ $index = (int) $index;
+ $total = (int) $total;
+
+ return [
+ 'index' => $index,
+ 'total' => $total,
+ ];
+ }
+}
diff --git a/src/Plugins/Verbose.php b/src/Plugins/Verbose.php
index e37938a3a..9cec77deb 100644
--- a/src/Plugins/Verbose.php
+++ b/src/Plugins/Verbose.php
@@ -16,7 +16,7 @@ final class Verbose implements HandlesArguments
/**
* The list of verbosity levels.
*/
- private const VERBOSITY_LEVELS = ['v', 'vv', 'vvv', 'q'];
+ private const array VERBOSITY_LEVELS = ['v', 'vv', 'vvv', 'q'];
/**
* {@inheritDoc}
diff --git a/src/Repositories/DatasetsRepository.php b/src/Repositories/DatasetsRepository.php
index 7d318a2dd..0c6b13a8d 100644
--- a/src/Repositories/DatasetsRepository.php
+++ b/src/Repositories/DatasetsRepository.php
@@ -19,7 +19,7 @@
*/
final class DatasetsRepository
{
- private const SEPARATOR = '>>';
+ private const string SEPARATOR = '>>';
/**
* Holds the datasets.
@@ -67,11 +67,11 @@ public static function has(string $filename, string $description): bool
}
/**
- * @return Closure|array
+ * @return array
*
* @throws ShouldNotHappen
*/
- public static function get(string $filename, string $description): Closure|array // @phpstan-ignore-line
+ public static function get(string $filename, string $description): array // @phpstan-ignore-line
{
$dataset = self::$withs[$filename.self::SEPARATOR.$description];
@@ -191,6 +191,7 @@ private static function getScopedDataset(string $name, string $currentTestFile):
return str_starts_with($currentTestFile, $datasetScope);
}, ARRAY_FILTER_USE_KEY);
+ /** @var string|null $closestScopeDatasetKey */
$closestScopeDatasetKey = array_reduce(
array_keys($matchingDatasets),
fn (string|int|null $keyA, string|int|null $keyB): string|int|null => $keyA !== null && strlen((string) $keyA) > strlen((string) $keyB) ? $keyA : $keyB
diff --git a/src/Repositories/SnapshotRepository.php b/src/Repositories/SnapshotRepository.php
index 7f7a95738..c719f219f 100644
--- a/src/Repositories/SnapshotRepository.php
+++ b/src/Repositories/SnapshotRepository.php
@@ -19,6 +19,7 @@ final class SnapshotRepository
* Creates a snapshot repository instance.
*/
public function __construct(
+ private readonly string $rootPath,
private readonly string $testsPath,
private readonly string $snapshotsPath,
) {}
@@ -103,7 +104,19 @@ public function flush(): void
*/
private function getSnapshotFilename(): string
{
- $relativePath = str_replace($this->testsPath, '', TestSuite::getInstance()->getFilename());
+ $testFile = TestSuite::getInstance()->getFilename();
+
+ if (str_starts_with($testFile, $this->testsPath)) {
+ // if the test file is in the tests directory
+ $startPath = $this->testsPath;
+ } else {
+ // if the test file is in the app, src, etc. directory
+ $startPath = $this->rootPath;
+ }
+
+ // relative path: we use substr() and not str_replace() to remove the start path
+ // for instance, if the $startPath is /app/ and the $testFile is /app/app/tests/Unit/ExampleTest.php, we should only remove the first /app/ from the path
+ $relativePath = substr($testFile, strlen($startPath));
// remove extension from filename
$relativePath = substr($relativePath, 0, (int) strrpos($relativePath, '.'));
diff --git a/src/Result.php b/src/Result.php
index 233ffa2dd..97eda17f4 100644
--- a/src/Result.php
+++ b/src/Result.php
@@ -4,20 +4,16 @@
namespace Pest;
-use NunoMaduro\Collision\Adapters\Phpunit\Support\ResultReflection;
use PHPUnit\TestRunner\TestResult\TestResult;
use PHPUnit\TextUI\Configuration\Configuration;
+use PHPUnit\TextUI\ShellExitCodeCalculator;
/**
* @internal
*/
final class Result
{
- private const SUCCESS_EXIT = 0;
-
- private const FAILURE_EXIT = 1;
-
- private const EXCEPTION_EXIT = 2;
+ private const int SUCCESS_EXIT = 0;
/**
* If the exit code is different from 0.
@@ -40,44 +36,8 @@ public static function ok(Configuration $configuration, TestResult $result): boo
*/
public static function exitCode(Configuration $configuration, TestResult $result): int
{
- if ($result->wasSuccessful()) {
- if ($configuration->failOnWarning()) {
- $warnings = $result->numberOfTestsWithTestTriggeredPhpunitWarningEvents()
- + count($result->warnings())
- + count($result->phpWarnings());
-
- if ($warnings > 0) {
- return self::FAILURE_EXIT;
- }
- }
-
- if (! $result->hasTestTriggeredPhpunitWarningEvents()) {
- return self::SUCCESS_EXIT;
- }
- }
-
- if ($configuration->failOnEmptyTestSuite() && ResultReflection::numberOfTests($result) === 0) {
- return self::FAILURE_EXIT;
- }
-
- if ($result->wasSuccessful()) {
- if ($configuration->failOnRisky() && $result->hasTestConsideredRiskyEvents()) {
- $returnCode = self::FAILURE_EXIT;
- }
-
- if ($configuration->failOnIncomplete() && $result->hasTestMarkedIncompleteEvents()) {
- $returnCode = self::FAILURE_EXIT;
- }
-
- if ($configuration->failOnSkipped() && $result->hasTestSkippedEvents()) {
- $returnCode = self::FAILURE_EXIT;
- }
- }
-
- if ($result->hasTestErroredEvents()) {
- return self::EXCEPTION_EXIT;
- }
+ $shell = new ShellExitCodeCalculator;
- return self::FAILURE_EXIT;
+ return $shell->calculate($configuration, $result);
}
}
diff --git a/src/Runner/Filter/EnsureTestCaseIsInitiatedFilter.php b/src/Runner/Filter/EnsureTestCaseIsInitiatedFilter.php
new file mode 100644
index 000000000..614b38a4a
--- /dev/null
+++ b/src/Runner/Filter/EnsureTestCaseIsInitiatedFilter.php
@@ -0,0 +1,39 @@
+ $iterator
+ */
+ public function __construct(RecursiveIterator $iterator)
+ {
+ parent::__construct($iterator);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function accept(): bool
+ {
+ $test = $this->getInnerIterator()->current();
+
+ if ($test instanceof HasPrintableTestCaseName) {
+ /** @phpstan-ignore-next-line */
+ $test->__initializeTestCase();
+ }
+
+ return true;
+ }
+}
diff --git a/src/Subscribers/EnsureIgnorableTestCasesAreIgnored.php b/src/Subscribers/EnsureIgnorableTestCasesAreIgnored.php
index f571c2f3d..45a7823d8 100644
--- a/src/Subscribers/EnsureIgnorableTestCasesAreIgnored.php
+++ b/src/Subscribers/EnsureIgnorableTestCasesAreIgnored.php
@@ -33,7 +33,7 @@ public function notify(Started $event): void
/** @var array $testRunnerTriggeredWarningEvents */
$testRunnerTriggeredWarningEvents = $property->getValue($collector);
- $testRunnerTriggeredWarningEvents = array_values(array_filter($testRunnerTriggeredWarningEvents, fn (WarningTriggered $event): bool => $event->message() !== 'No tests found in class "Pest\TestCases\IgnorableTestCase".'));
+ $testRunnerTriggeredWarningEvents = array_values(array_filter($testRunnerTriggeredWarningEvents, fn (WarningTriggered $event): bool => str_contains($event->message(), 'No tests found in class') === false));
$property->setValue($collector, $testRunnerTriggeredWarningEvents);
}
diff --git a/src/Support/Backtrace.php b/src/Support/Backtrace.php
index 030019760..652eb4425 100644
--- a/src/Support/Backtrace.php
+++ b/src/Support/Backtrace.php
@@ -11,12 +11,9 @@
*/
final class Backtrace
{
- /**
- * @var string
- */
- private const FILE = 'file';
+ private const string FILE = 'file';
- private const BACKTRACE_OPTIONS = DEBUG_BACKTRACE_IGNORE_ARGS;
+ private const int BACKTRACE_OPTIONS = DEBUG_BACKTRACE_IGNORE_ARGS;
/**
* Returns the current test file.
diff --git a/src/Support/DatasetInfo.php b/src/Support/DatasetInfo.php
index 46f39c416..c67f317c2 100644
--- a/src/Support/DatasetInfo.php
+++ b/src/Support/DatasetInfo.php
@@ -11,9 +11,9 @@
*/
final class DatasetInfo
{
- public const DATASETS_DIR_NAME = 'Datasets';
+ public const string DATASETS_DIR_NAME = 'Datasets';
- public const DATASETS_FILE_NAME = 'Datasets.php';
+ public const string DATASETS_FILE_NAME = 'Datasets.php';
public static function isInsideADatasetsDirectory(string $file): bool
{
diff --git a/src/Support/Description.php b/src/Support/Description.php
new file mode 100644
index 000000000..d7d775f22
--- /dev/null
+++ b/src/Support/Description.php
@@ -0,0 +1,21 @@
+description;
+ }
+}
diff --git a/src/Support/ExceptionTrace.php b/src/Support/ExceptionTrace.php
index 9af6aa5bc..9d4132e27 100644
--- a/src/Support/ExceptionTrace.php
+++ b/src/Support/ExceptionTrace.php
@@ -13,7 +13,7 @@
*/
final class ExceptionTrace
{
- private const UNDEFINED_METHOD = 'Call to undefined method P\\';
+ private const string UNDEFINED_METHOD = 'Call to undefined method P\\';
/**
* Ensures the given closure reports the good execution context.
diff --git a/src/Support/Exporter.php b/src/Support/Exporter.php
index 169f4891b..44367c08e 100644
--- a/src/Support/Exporter.php
+++ b/src/Support/Exporter.php
@@ -15,7 +15,7 @@
/**
* The maximum number of items in an array to export.
*/
- private const MAX_ARRAY_ITEMS = 3;
+ private const int MAX_ARRAY_ITEMS = 3;
/**
* Creates a new Exporter instance.
diff --git a/src/Support/HigherOrderCallables.php b/src/Support/HigherOrderCallables.php
index 9e5bba36f..358b4da5f 100644
--- a/src/Support/HigherOrderCallables.php
+++ b/src/Support/HigherOrderCallables.php
@@ -46,6 +46,7 @@ public function expect(mixed $value): Expectation
*/
public function and(mixed $value): Expectation
{
+ // @phpstan-ignore-next-line
return $this->expect($value);
}
diff --git a/src/Support/HigherOrderMessage.php b/src/Support/HigherOrderMessage.php
index 89c3e1f10..ce9482445 100644
--- a/src/Support/HigherOrderMessage.php
+++ b/src/Support/HigherOrderMessage.php
@@ -13,7 +13,7 @@
*/
final class HigherOrderMessage
{
- public const UNDEFINED_METHOD = 'Method %s does not exist';
+ public const string UNDEFINED_METHOD = 'Method %s does not exist';
/**
* An optional condition that will determine if the message will be executed.
diff --git a/src/Support/HigherOrderTapProxy.php b/src/Support/HigherOrderTapProxy.php
index 08eb5ea77..60e65bac9 100644
--- a/src/Support/HigherOrderTapProxy.php
+++ b/src/Support/HigherOrderTapProxy.php
@@ -31,10 +31,8 @@ public function __set(string $property, mixed $value): void
/**
* Dynamically pass properties gets to the target.
- *
- * @return mixed
*/
- public function __get(string $property)
+ public function __get(string $property): mixed
{
if (property_exists($this->target, $property)) {
return $this->target->{$property};
diff --git a/src/Support/Shell.php b/src/Support/Shell.php
new file mode 100644
index 000000000..b5c5b1573
--- /dev/null
+++ b/src/Support/Shell.php
@@ -0,0 +1,101 @@
+setUpdateCheck(Checker::NEVER);
+
+ $config->getPresenter()->addCasters(self::casters());
+
+ $shell = new PsyShell($config);
+
+ $loader = self::tinkered($shell);
+
+ try {
+ $shell->run();
+ } finally {
+ $loader?->unregister(); // @phpstan-ignore-line
+ }
+ }
+
+ /**
+ * Returns the casters for the Psy Shell.
+ *
+ * @return array
+ */
+ private static function casters(): array
+ {
+ $casters = [
+ 'Illuminate\Support\Collection' => 'Laravel\Tinker\TinkerCaster::castCollection',
+ 'Illuminate\Support\HtmlString' => 'Laravel\Tinker\TinkerCaster::castHtmlString',
+ 'Illuminate\Support\Stringable' => 'Laravel\Tinker\TinkerCaster::castStringable',
+ ];
+
+ if (class_exists('Illuminate\Database\Eloquent\Model')) {
+ $casters['Illuminate\Database\Eloquent\Model'] = 'Laravel\Tinker\TinkerCaster::castModel';
+ }
+
+ if (class_exists('Illuminate\Process\ProcessResult')) {
+ $casters['Illuminate\Process\ProcessResult'] = 'Laravel\Tinker\TinkerCaster::castProcessResult';
+ }
+
+ if (class_exists('Illuminate\Foundation\Application')) {
+ $casters['Illuminate\Foundation\Application'] = 'Laravel\Tinker\TinkerCaster::castApplication';
+ }
+
+ if (function_exists('app') === false) {
+ return $casters; // @phpstan-ignore-line
+ }
+
+ $config = app()->make('config');
+
+ return array_merge($casters, (array) $config->get('tinker.casters', []));
+ }
+
+ /**
+ * Tinkers the current shell, if the Tinker package is available.
+ */
+ private static function tinkered(PsyShell $shell): ?object
+ {
+ if (function_exists('app') === false
+ || ! class_exists(Env::class)
+ || ! class_exists(ClassAliasAutoloader::class)
+ ) {
+ return null;
+ }
+
+ $path = Env::get('COMPOSER_VENDOR_DIR', app()->basePath().DIRECTORY_SEPARATOR.'vendor');
+
+ $path .= '/composer/autoload_classmap.php';
+
+ if (! file_exists($path)) {
+ $path = TestSuite::getInstance()->rootPath.DIRECTORY_SEPARATOR.'vendor'.DIRECTORY_SEPARATOR.'composer'.DIRECTORY_SEPARATOR.'autoload_classmap.php';
+ }
+
+ $config = app()->make('config');
+
+ return ClassAliasAutoloader::register(
+ $shell, $path, $config->get('tinker.alias', []), $config->get('tinker.dont_alias', [])
+ );
+ }
+}
diff --git a/src/Support/StateGenerator.php b/src/Support/StateGenerator.php
index a7ddba1a9..ddff338b0 100644
--- a/src/Support/StateGenerator.php
+++ b/src/Support/StateGenerator.php
@@ -10,6 +10,8 @@
use PHPUnit\Event\Code\TestDoxBuilder;
use PHPUnit\Event\Code\TestMethod;
use PHPUnit\Event\Code\ThrowableBuilder;
+use PHPUnit\Event\Test\AfterLastTestMethodErrored;
+use PHPUnit\Event\Test\BeforeFirstTestMethodErrored;
use PHPUnit\Event\Test\Errored;
use PHPUnit\Event\TestData\TestDataCollection;
use PHPUnit\Framework\SkippedWithMessageException;
@@ -29,9 +31,17 @@ public function fromPhpUnitTestResult(int $passedTests, PHPUnitTestResult $testR
TestResult::FAIL,
$testResultEvent->throwable()
));
- } else {
- // @phpstan-ignore-next-line
+ } elseif ($testResultEvent instanceof BeforeFirstTestMethodErrored) {
$state->add(TestResult::fromBeforeFirstTestMethodErrored($testResultEvent));
+ } elseif ($testResultEvent instanceof AfterLastTestMethodErrored) {
+ $state->add(TestResult::fromBeforeFirstTestMethodErrored(
+ new BeforeFirstTestMethodErrored(
+ $testResultEvent->telemetryInfo(),
+ $testResultEvent->testClassName(),
+ $testResultEvent->calledMethod(),
+ $testResultEvent->throwable(),
+ )
+ ));
}
}
diff --git a/src/Support/Str.php b/src/Support/Str.php
index 0e654bc80..04f4b1fdd 100644
--- a/src/Support/Str.php
+++ b/src/Support/Str.php
@@ -13,12 +13,9 @@ final class Str
* Pool of alpha-numeric characters for generating (unsafe) random strings
* from.
*/
- private const POOL = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
+ private const string POOL = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
- /**
- * @var string
- */
- private const PREFIX = '__pest_evaluable_';
+ private const string PREFIX = '__pest_evaluable_';
/**
* Create a (unsecure & non-cryptographically safe) random alpha-numeric
@@ -82,7 +79,7 @@ public static function beforeLast(string $subject, string $search): string
return $subject;
}
- return substr($subject, 0, $pos);
+ return mb_substr($subject, 0, $pos);
}
/**
@@ -104,7 +101,7 @@ public static function isUuid(string $value): bool
/**
* Creates a describe block as `$describeDescription` → `$testDescription` format.
*
- * @param array $describeDescriptions
+ * @param array $describeDescriptions
*/
public static function describe(array $describeDescriptions, string $testDescription): string
{
@@ -120,4 +117,14 @@ public static function isUrl(string $value): bool
{
return (bool) filter_var($value, FILTER_VALIDATE_URL);
}
+
+ /**
+ * Converts the given `$target` to a URL-friendly "slug".
+ */
+ public static function slugify(string $target): string
+ {
+ $target = preg_replace('/[^a-zA-Z0-9]+/', '-', $target);
+
+ return strtolower(trim((string) $target, '-'));
+ }
}
diff --git a/src/TestSuite.php b/src/TestSuite.php
index ca35a0403..df17ec2df 100644
--- a/src/TestSuite.php
+++ b/src/TestSuite.php
@@ -78,6 +78,7 @@ public function __construct(
$this->afterAll = new AfterAllRepository;
$this->rootPath = (string) realpath($rootPath);
$this->snapshots = new SnapshotRepository(
+ $this->rootPath,
implode(DIRECTORY_SEPARATOR, [$this->rootPath, $this->testPath]),
implode(DIRECTORY_SEPARATOR, ['.pest', 'snapshots']),
);
@@ -101,7 +102,7 @@ public static function getInstance(
}
if (! self::$instance instanceof self) {
- Panic::with(new InvalidPestCommand);
+ throw new InvalidPestCommand;
}
return self::$instance;
@@ -119,7 +120,7 @@ public function getDescription(): string
assert($this->test instanceof TestCase);
$description = str_replace('__pest_evaluable_', '', $this->test->name());
- $datasetAsString = str_replace('__pest_evaluable_', '', Str::evaluable($this->test->dataSetAsStringWithData()));
+ $datasetAsString = str_replace('__pest_evaluable_', '', Str::evaluable($this->test->dataSetAsString()));
return str_replace(' ', '_', $description.$datasetAsString);
}
diff --git a/tests-external/Features/Expect/toMatchSnapshot.php b/tests-external/Features/Expect/toMatchSnapshot.php
new file mode 100644
index 000000000..412ed8ef6
--- /dev/null
+++ b/tests-external/Features/Expect/toMatchSnapshot.php
@@ -0,0 +1,35 @@
+snapshotable = <<<'HTML'
+
+ HTML;
+});
+
+test('pass with dataset', function ($data) {
+ TestSuite::getInstance()->snapshots->save($this->snapshotable);
+ [$filename] = TestSuite::getInstance()->snapshots->get();
+
+ expect($filename)->toStartWith('tests/.pest/snapshots-external/')
+ ->toEndWith('pass_with_dataset_with_data_set____my_datas_set_value___.snap')
+ ->and($this->snapshotable)->toMatchSnapshot();
+})->with(['my-datas-set-value']);
+
+describe('within describe', function () {
+ test('pass with dataset', function ($data) {
+ TestSuite::getInstance()->snapshots->save($this->snapshotable);
+ [$filename] = TestSuite::getInstance()->snapshots->get();
+
+ expect($filename)->toStartWith('tests/.pest/snapshots-external/')
+ ->toEndWith('pass_with_dataset_with_data_set____my_datas_set_value___.snap')
+ ->and($this->snapshotable)->toMatchSnapshot();
+ });
+})->with(['my-datas-set-value']);
diff --git "a/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/_within_describe__\342\206\222_pass_with_dataset_with_data_set____my_datas_set_value______my_datas_set_value__.snap" "b/tests/.pest/snapshots-external/Features/Expect/toMatchSnapshot/_within_describe__\342\206\222_pass_with_dataset___my_datas_set_value___with_data___my_datas_set_value__.snap"
similarity index 100%
rename from "tests/.pest/snapshots/Features/Expect/toMatchSnapshot/_within_describe__\342\206\222_pass_with_dataset_with_data_set____my_datas_set_value______my_datas_set_value__.snap"
rename to "tests/.pest/snapshots-external/Features/Expect/toMatchSnapshot/_within_describe__\342\206\222_pass_with_dataset___my_datas_set_value___with_data___my_datas_set_value__.snap"
diff --git a/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/pass_with_dataset_with_data_set____my_datas_set_value______my_datas_set_value__.snap "b/tests/.pest/snapshots-external/Features/Expect/toMatchSnapshot/_within_describe__\342\206\222_pass_with_dataset_with_data_set_____my_datas_set_value___.snap"
similarity index 100%
rename from tests/.pest/snapshots/Features/Expect/toMatchSnapshot/pass_with_dataset_with_data_set____my_datas_set_value______my_datas_set_value__.snap
rename to "tests/.pest/snapshots-external/Features/Expect/toMatchSnapshot/_within_describe__\342\206\222_pass_with_dataset_with_data_set_____my_datas_set_value___.snap"
diff --git "a/tests/.pest/snapshots-external/Features/Expect/toMatchSnapshot/_within_describe__\342\206\222_pass_with_dataset_with_data_set____my_datas_set_value___.snap" "b/tests/.pest/snapshots-external/Features/Expect/toMatchSnapshot/_within_describe__\342\206\222_pass_with_dataset_with_data_set____my_datas_set_value___.snap"
new file mode 100644
index 000000000..c2b4dc0a1
--- /dev/null
+++ "b/tests/.pest/snapshots-external/Features/Expect/toMatchSnapshot/_within_describe__\342\206\222_pass_with_dataset_with_data_set____my_datas_set_value___.snap"
@@ -0,0 +1,7 @@
+
\ No newline at end of file
diff --git a/tests/.pest/snapshots-external/Features/Expect/toMatchSnapshot/pass_with_dataset___my_datas_set_value___with_data___my_datas_set_value__.snap b/tests/.pest/snapshots-external/Features/Expect/toMatchSnapshot/pass_with_dataset___my_datas_set_value___with_data___my_datas_set_value__.snap
new file mode 100644
index 000000000..c2b4dc0a1
--- /dev/null
+++ b/tests/.pest/snapshots-external/Features/Expect/toMatchSnapshot/pass_with_dataset___my_datas_set_value___with_data___my_datas_set_value__.snap
@@ -0,0 +1,7 @@
+
\ No newline at end of file
diff --git a/tests/.pest/snapshots-external/Features/Expect/toMatchSnapshot/pass_with_dataset_with_data_set_____my_datas_set_value___.snap b/tests/.pest/snapshots-external/Features/Expect/toMatchSnapshot/pass_with_dataset_with_data_set_____my_datas_set_value___.snap
new file mode 100644
index 000000000..c2b4dc0a1
--- /dev/null
+++ b/tests/.pest/snapshots-external/Features/Expect/toMatchSnapshot/pass_with_dataset_with_data_set_____my_datas_set_value___.snap
@@ -0,0 +1,7 @@
+
\ No newline at end of file
diff --git a/tests/.pest/snapshots-external/Features/Expect/toMatchSnapshot/pass_with_dataset_with_data_set____my_datas_set_value___.snap b/tests/.pest/snapshots-external/Features/Expect/toMatchSnapshot/pass_with_dataset_with_data_set____my_datas_set_value___.snap
new file mode 100644
index 000000000..c2b4dc0a1
--- /dev/null
+++ b/tests/.pest/snapshots-external/Features/Expect/toMatchSnapshot/pass_with_dataset_with_data_set____my_datas_set_value___.snap
@@ -0,0 +1,7 @@
+
\ No newline at end of file
diff --git "a/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/_within_describe__\342\206\222_pass_with_dataset_with_data_set____my_datas_set_value___.snap" "b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/_within_describe__\342\206\222_pass_with_dataset_with_data_set____my_datas_set_value___.snap"
new file mode 100644
index 000000000..c2b4dc0a1
--- /dev/null
+++ "b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/_within_describe__\342\206\222_pass_with_dataset_with_data_set____my_datas_set_value___.snap"
@@ -0,0 +1,7 @@
+
\ No newline at end of file
diff --git a/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set___1____1_.snap b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set___1__.snap
similarity index 100%
rename from tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set___1____1_.snap
rename to tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set___1__.snap
diff --git a/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set___1____1___2.snap b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set___1____2.snap
similarity index 100%
rename from tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set___1____1___2.snap
rename to tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set___1____2.snap
diff --git a/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set____bar______bar__.snap b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set____bar___.snap
similarity index 100%
rename from tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set____bar______bar__.snap
rename to tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set____bar___.snap
diff --git a/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set____bar______bar____2.snap b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set____bar_____2.snap
similarity index 100%
rename from tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set____bar______bar____2.snap
rename to tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set____bar_____2.snap
diff --git a/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set____baz______baz__.snap b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set____baz___.snap
similarity index 100%
rename from tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set____baz______baz__.snap
rename to tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set____baz___.snap
diff --git a/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set____baz______baz____2.snap b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set____baz_____2.snap
similarity index 100%
rename from tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set____baz______baz____2.snap
rename to tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set____baz_____2.snap
diff --git a/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set____foo______foo__.snap b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set____foo___.snap
similarity index 100%
rename from tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set____foo______foo__.snap
rename to tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set____foo___.snap
diff --git a/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set____foo______foo____2.snap b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set____foo_____2.snap
similarity index 100%
rename from tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set____foo______foo____2.snap
rename to tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set____foo_____2.snap
diff --git a/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___10____10_.snap b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___10__.snap
similarity index 100%
rename from tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___10____10_.snap
rename to tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___10__.snap
diff --git a/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___10____10___2.snap b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___10____2.snap
similarity index 100%
rename from tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___10____10___2.snap
rename to tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___10____2.snap
diff --git a/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___1____1_.snap b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___1__.snap
similarity index 100%
rename from tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___1____1_.snap
rename to tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___1__.snap
diff --git a/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___1____1___2.snap b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___1____2.snap
similarity index 100%
rename from tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___1____1___2.snap
rename to tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___1____2.snap
diff --git a/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___2____2_.snap b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___2__.snap
similarity index 100%
rename from tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___2____2_.snap
rename to tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___2__.snap
diff --git a/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___2____2___2.snap b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___2____2.snap
similarity index 100%
rename from tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___2____2___2.snap
rename to tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___2____2.snap
diff --git a/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___3____3_.snap b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___3__.snap
similarity index 100%
rename from tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___3____3_.snap
rename to tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___3__.snap
diff --git a/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___3____3___2.snap b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___3____2.snap
similarity index 100%
rename from tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___3____3___2.snap
rename to tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___3____2.snap
diff --git a/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___4____4_.snap b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___4__.snap
similarity index 100%
rename from tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___4____4_.snap
rename to tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___4__.snap
diff --git a/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___4____4___2.snap b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___4____2.snap
similarity index 100%
rename from tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___4____4___2.snap
rename to tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___4____2.snap
diff --git a/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___5____5_.snap b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___5__.snap
similarity index 100%
rename from tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___5____5_.snap
rename to tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___5__.snap
diff --git a/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___5____5___2.snap b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___5____2.snap
similarity index 100%
rename from tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___5____5___2.snap
rename to tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___5____2.snap
diff --git a/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___6____6_.snap b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___6__.snap
similarity index 100%
rename from tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___6____6_.snap
rename to tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___6__.snap
diff --git a/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___6____6___2.snap b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___6____2.snap
similarity index 100%
rename from tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___6____6___2.snap
rename to tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___6____2.snap
diff --git a/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___7____7_.snap b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___7__.snap
similarity index 100%
rename from tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___7____7_.snap
rename to tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___7__.snap
diff --git a/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___7____7___2.snap b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___7____2.snap
similarity index 100%
rename from tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___7____7___2.snap
rename to tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___7____2.snap
diff --git a/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___8____8_.snap b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___8__.snap
similarity index 100%
rename from tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___8____8_.snap
rename to tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___8__.snap
diff --git a/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___8____8___2.snap b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___8____2.snap
similarity index 100%
rename from tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___8____8___2.snap
rename to tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___8____2.snap
diff --git a/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___9____9_.snap b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___9__.snap
similarity index 100%
rename from tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___9____9_.snap
rename to tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___9__.snap
diff --git a/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___9____9___2.snap b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___9____2.snap
similarity index 100%
rename from tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___9____9___2.snap
rename to tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___9____2.snap
diff --git a/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/pass_with_dataset_with_data_set____my_datas_set_value___.snap b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/pass_with_dataset_with_data_set____my_datas_set_value___.snap
new file mode 100644
index 000000000..c2b4dc0a1
--- /dev/null
+++ b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/pass_with_dataset_with_data_set____my_datas_set_value___.snap
@@ -0,0 +1,7 @@
+
\ No newline at end of file
diff --git a/tests/.pest/snapshots/Visual/Collision/collision_with_data_set________________.snap b/tests/.pest/snapshots/Visual/Collision/collision_with_data_set_________.snap
similarity index 100%
rename from tests/.pest/snapshots/Visual/Collision/collision_with_data_set________________.snap
rename to tests/.pest/snapshots/Visual/Collision/collision_with_data_set_________.snap
diff --git a/tests/.pest/snapshots/Visual/Help/visual_snapshot_of_help_command_output.snap b/tests/.pest/snapshots/Visual/Help/visual_snapshot_of_help_command_output.snap
index 27bb9dea7..ef023f6cf 100644
--- a/tests/.pest/snapshots/Visual/Help/visual_snapshot_of_help_command_output.snap
+++ b/tests/.pest/snapshots/Visual/Help/visual_snapshot_of_help_command_output.snap
@@ -1,5 +1,5 @@
- Pest Testing Framework 3.8.4.
+ Pest Testing Framework 4.3.2.
USAGE: pest [options]
@@ -27,6 +27,8 @@
--pr .... Output to standard output tests with the given pull request number
--pull-request Output to standard output tests with the given pull request number (alias for --pr)
--retry Run non-passing tests first and stop execution upon first error or failure
+ --dirty ...... Only run tests that have uncommitted changes according to Git
+ --all .................... Ignore test selection from XML configuration file
--list-suites ................................... List available test suites
--testsuite [name] ......... Only run tests from the specified test suite(s)
--exclude-testsuite [name] .. Exclude tests from the specified test suite(s)
@@ -68,6 +70,7 @@
--fail-on-risky Signal failure using shell exit code when a test was considered risky
--fail-on-deprecation Signal failure using shell exit code when a deprecation was triggered
--fail-on-phpunit-deprecation Signal failure using shell exit code when a PHPUnit deprecation was triggered
+ --fail-on-phpunit-notice Signal failure using shell exit code when a PHPUnit notice was triggered
--fail-on-phpunit-warning Signal failure using shell exit code when a PHPUnit warning was triggered
--fail-on-notice Signal failure using shell exit code when a notice was triggered
--fail-on-skipped Signal failure using shell exit code when a test was skipped
@@ -78,6 +81,7 @@
--do-not-fail-on-risky Do not signal failure using shell exit code when a test was considered risky
--do-not-fail-on-deprecation Do not signal failure using shell exit code when a deprecation was triggered
--do-not-fail-on-phpunit-deprecation Do not signal failure using shell exit code when a PHPUnit deprecation was triggered
+ --do-not-fail-on-phpunit-notice Do not signal failure using shell exit code when a PHPUnit notice was triggered
--do-not-fail-on-phpunit-warning Do not signal failure using shell exit code when a PHPUnit warning was triggered
--do-not-fail-on-notice Do not signal failure using shell exit code when a notice was triggered
--do-not-fail-on-skipped Do not signal failure using shell exit code when a test was skipped
@@ -99,6 +103,7 @@
--display-skipped ........................ Display details for skipped tests
--display-deprecations . Display details for deprecations triggered by tests
--display-phpunit-deprecations .... Display details for PHPUnit deprecations
+ --display-phpunit-notices .............. Display details for PHPUnit notices
--display-errors ............. Display details for errors triggered by tests
--display-notices ........... Display details for notices triggered by tests
--display-warnings ......... Display details for warnings triggered by tests
@@ -108,10 +113,13 @@
--testdox ................ Replace default result output with TestDox format
--testdox-summary Repeat TestDox output for tests with errors, failures, or issues
--debug Replace default progress and result output with debugging information
+ --with-telemetry Include telemetry information in debugging information output
--compact ................ Replace default result output with Compact format
LOGGING OPTIONS:
--log-junit [file] .......... Write test results in JUnit XML format to file
+ --log-otr [file] Write test results in Open Test Reporting XML format to file
+ --include-git-information Include Git information in Open Test Reporting XML logfile
--log-teamcity [file] ........ Write test results in TeamCity format to file
--testdox-html [file] .. Write test results in TestDox format (HTML) to file
--testdox-text [file] Write test results in TestDox format (plain text) to file
@@ -123,6 +131,7 @@
--coverage ..... Generate code coverage report and output to standard output
--coverage --min Set the minimum required coverage percentage, and fail if not met
--coverage-clover [file] Write code coverage report in Clover XML format to file
+ --coverage-openclover [file] Write code coverage report in OpenClover XML format to file
--coverage-cobertura [file] Write code coverage report in Cobertura XML format to file
--coverage-crap4j [file] Write code coverage report in Crap4J XML format to file
--coverage-html [dir] Write code coverage report in HTML format to directory
@@ -131,6 +140,7 @@
--only-summary-for-coverage-text Option for code coverage report in text format: only show summary
--show-uncovered-for-coverage-text Option for code coverage report in text format: show uncovered files
--coverage-xml [dir] . Write code coverage report in XML format to directory
+ --exclude-source-from-xml-coverage Exclude [source] element from code coverage report in XML format
--warm-coverage-cache ........................... Warm static analysis cache
--coverage-filter [dir] ........... Include [dir] in code coverage reporting
--path-coverage .......... Report path coverage in addition to line coverage
diff --git a/tests/.pest/snapshots/Visual/Todo/todo.snap b/tests/.pest/snapshots/Visual/Todo/todo.snap
index 5ab4f7fa6..c50794f7a 100644
--- a/tests/.pest/snapshots/Visual/Todo/todo.snap
+++ b/tests/.pest/snapshots/Visual/Todo/todo.snap
@@ -15,7 +15,7 @@
↓ todo on describe → should not fail
↓ todo on describe → should run
- TODO Tests\Features\Todo - 28 todos
+ TODO Tests\Features\Todo - 29 todos
↓ something todo later
↓ something todo later chained
↓ something todo later chained and with function body
@@ -45,6 +45,7 @@
// nested describe note
// test note
↓ todo on describe → todo block → it should not execute
+ ↓ todo on describe with matching name → describe block → it should not execute
↓ todo on test after describe block
↓ todo with note on test after describe block
// test note
@@ -80,6 +81,6 @@
PASS Tests\CustomTestCase\ParentTest
✓ override method
- Tests: 38 todos, 3 passed (20 assertions)
+ Tests: 39 todos, 3 passed (21 assertions)
Duration: x.xxs
diff --git a/tests/.pest/snapshots/Visual/Todo/todo_in_parallel.snap b/tests/.pest/snapshots/Visual/Todo/todo_in_parallel.snap
index 5ab4f7fa6..c50794f7a 100644
--- a/tests/.pest/snapshots/Visual/Todo/todo_in_parallel.snap
+++ b/tests/.pest/snapshots/Visual/Todo/todo_in_parallel.snap
@@ -15,7 +15,7 @@
↓ todo on describe → should not fail
↓ todo on describe → should run
- TODO Tests\Features\Todo - 28 todos
+ TODO Tests\Features\Todo - 29 todos
↓ something todo later
↓ something todo later chained
↓ something todo later chained and with function body
@@ -45,6 +45,7 @@
// nested describe note
// test note
↓ todo on describe → todo block → it should not execute
+ ↓ todo on describe with matching name → describe block → it should not execute
↓ todo on test after describe block
↓ todo with note on test after describe block
// test note
@@ -80,6 +81,6 @@
PASS Tests\CustomTestCase\ParentTest
✓ override method
- Tests: 38 todos, 3 passed (20 assertions)
+ Tests: 39 todos, 3 passed (21 assertions)
Duration: x.xxs
diff --git a/tests/.pest/snapshots/Visual/Todo/todos.snap b/tests/.pest/snapshots/Visual/Todo/todos.snap
index 5ab4f7fa6..c50794f7a 100644
--- a/tests/.pest/snapshots/Visual/Todo/todos.snap
+++ b/tests/.pest/snapshots/Visual/Todo/todos.snap
@@ -15,7 +15,7 @@
↓ todo on describe → should not fail
↓ todo on describe → should run
- TODO Tests\Features\Todo - 28 todos
+ TODO Tests\Features\Todo - 29 todos
↓ something todo later
↓ something todo later chained
↓ something todo later chained and with function body
@@ -45,6 +45,7 @@
// nested describe note
// test note
↓ todo on describe → todo block → it should not execute
+ ↓ todo on describe with matching name → describe block → it should not execute
↓ todo on test after describe block
↓ todo with note on test after describe block
// test note
@@ -80,6 +81,6 @@
PASS Tests\CustomTestCase\ParentTest
✓ override method
- Tests: 38 todos, 3 passed (20 assertions)
+ Tests: 39 todos, 3 passed (21 assertions)
Duration: x.xxs
diff --git a/tests/.pest/snapshots/Visual/Todo/todos_in_parallel.snap b/tests/.pest/snapshots/Visual/Todo/todos_in_parallel.snap
index 5ab4f7fa6..c50794f7a 100644
--- a/tests/.pest/snapshots/Visual/Todo/todos_in_parallel.snap
+++ b/tests/.pest/snapshots/Visual/Todo/todos_in_parallel.snap
@@ -15,7 +15,7 @@
↓ todo on describe → should not fail
↓ todo on describe → should run
- TODO Tests\Features\Todo - 28 todos
+ TODO Tests\Features\Todo - 29 todos
↓ something todo later
↓ something todo later chained
↓ something todo later chained and with function body
@@ -45,6 +45,7 @@
// nested describe note
// test note
↓ todo on describe → todo block → it should not execute
+ ↓ todo on describe with matching name → describe block → it should not execute
↓ todo on test after describe block
↓ todo with note on test after describe block
// test note
@@ -80,6 +81,6 @@
PASS Tests\CustomTestCase\ParentTest
✓ override method
- Tests: 38 todos, 3 passed (20 assertions)
+ Tests: 39 todos, 3 passed (21 assertions)
Duration: x.xxs
diff --git a/tests/.pest/snapshots/Visual/Version/visual_snapshot_of_help_command_output.snap b/tests/.pest/snapshots/Visual/Version/visual_snapshot_of_help_command_output.snap
index 557f198ed..c6a7cadfc 100644
--- a/tests/.pest/snapshots/Visual/Version/visual_snapshot_of_help_command_output.snap
+++ b/tests/.pest/snapshots/Visual/Version/visual_snapshot_of_help_command_output.snap
@@ -1,3 +1,3 @@
- Pest Testing Framework 3.8.4.
+ Pest Testing Framework 4.3.2.
diff --git a/tests/.snapshots/SuccessOnly.php.inc b/tests/.snapshots/SuccessOnly.php.inc
index b00a0e36c..b940b7b63 100644
--- a/tests/.snapshots/SuccessOnly.php.inc
+++ b/tests/.snapshots/SuccessOnly.php.inc
@@ -1,5 +1,5 @@
##teamcity[testSuiteStarted name='Tests/tests/SuccessOnly' locationHint='pest_qn://tests/.tests/SuccessOnly.php' flowId='1234']
-##teamcity[testCount count='3' 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[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']
@@ -8,8 +8,12 @@
##teamcity[testStarted name='can pass with dataset with data set "(true)"' locationHint='pest_qn://tests/.tests/SuccessOnly.php::can pass with dataset with data set "(true)"' flowId='1234']
##teamcity[testFinished name='can pass with dataset with data set "(true)"' duration='100000' flowId='1234']
##teamcity[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[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']
- [90mTests:[39m [32;1m3 passed[39;22m[90m (3 assertions)[39m
+ [90mTests:[39m [32;1m4 passed[39;22m[90m (4 assertions)[39m
[90mDuration:[39m [39m1.00s[39m
diff --git a/tests/.snapshots/success.txt b/tests/.snapshots/success.txt
index 513a804ea..7b3d6f9b8 100644
--- a/tests/.snapshots/success.txt
+++ b/tests/.snapshots/success.txt
@@ -4,7 +4,6 @@
✓ preset → strict → ignoring ['usleep']
✓ preset → security → ignoring ['eval', 'str_shuffle', 'exec', …]
✓ globals
- ✓ dependencies
✓ contracts
PASS Tests\Environments\Windows
@@ -29,6 +28,12 @@
✓ it gets executed after the test
✓ outer → inner → it does not get executed before the test
✓ outer → inner → it should call all parent afterEach functions
+ ✓ matching describe block names → outer → middle → inner → it does not get executed before the test
+ ✓ matching describe block names → outer → middle → inner → it should call all parent afterEach functions
+ ✓ matching describe block names → outer → middle → it does not get executed before the test
+ ✓ matching describe block names → outer → middle → it should not call afterEach functions for sibling describe blocks with the same name
+ ✓ matching describe block names → outer → inner → it does not get executed before the test
+ ✓ matching describe block names → outer → inner → it should not call afterEach functions for descendent of sibling describe blocks with the same name
PASS Tests\Features\Assignee
✓ it may be associated with an assignee [@nunomaduro, @taylorotwell]
@@ -45,6 +50,13 @@
✓ outer → inner → it should call all parent beforeEach functions
✓ with expectations → nested block → test
✓ with expectations → test
+ ✓ matching describe block names → outer → middle → inner → it should call all parent beforeEach functions
+ ✓ matching describe block names → outer → middle → it should not call beforeEach functions for sibling describe blocks with the same name
+ ✓ matching describe block names → outer → inner → it should not call beforeEach functions for descendent of sibling describe blocks with the same name
+ ✓ matching name → it should call the before each
+ ✓ matching name → it should not call the before each on the describe block with the same name
+ ✓ called on all tests → beforeEach should be called
+ ✓ called on all tests → beforeEach should be called for all tests
PASS Tests\Features\BeforeEachProxiesToTestCallWithExpectations
✓ runs 1
@@ -68,13 +80,20 @@
✓ it adds coverage if --min exist
✓ it generates coverage based on file input
- PASS Tests\Features\Covers
+ PASS Tests\Features\Covers\ClassCoverage
✓ it uses the correct PHPUnit attribute for class
+
+ PASS Tests\Features\Covers\ExceptionHandling
+ ✓ it throws exception if no class nor method has been found
+
+ PASS Tests\Features\Covers\FunctionCoverage
✓ it uses the correct PHPUnit attribute for function
+
+ PASS Tests\Features\Covers\GuessCoverage
✓ it guesses if the given argument is a class or function
+
+ PASS Tests\Features\Covers\TraitCoverage
✓ it uses the correct PHPUnit attribute for trait
- ✓ it uses the correct PHPUnit attribute for covers nothing
- ✓ it throws exception if no class nor method has been found
PASS Tests\Features\DatasetsTests - 1 todo
✓ it throws exception if dataset does not exist
@@ -187,6 +206,11 @@
✓ with on nested describe → nested → describe → it should include the with value from all parent describe blocks with (1) / (2)
✓ with on nested describe → nested → describe → should include the with value from all parent describe blocks and the test with (1) / (2) / (3)
✓ with on nested describe → nested → after inner describe block with (1)
+ ✓ matching describe block names → outer → before inner describe block with (1)
+ ✓ matching describe block names → outer → inner → it should include the with value from all parent describe blocks with (1) / (2)
+ ✓ matching describe block names → outer → inner → should include the with value from all parent describe blocks and the test with (1) / (2) / (3)
+ ✓ matching describe block names → outer → inner → it should not include the value from the other describe block with the same name with (1)
+ ✓ matching describe block names → outer → after inner describe block with (1)
✓ after describe block with (5)
✓ it may be used with high order after describe block with dataset "formal"
✓ it may be used with high order after describe block with dataset "informal"
@@ -619,6 +643,13 @@
✓ pass
✓ failures
✓ failures with custom message
+ ✓ not failures
+
+ PASS Tests\Features\Expect\toBeSlug
+ ✓ pass
+ ✓ failures
+ ✓ failures with custom message
+ ✓ failures with default message
✓ not failures
PASS Tests\Features\Expect\toBeSnakeCase
@@ -1030,6 +1061,10 @@
✓ it may fail
✓ it may fail with the given message
+ PASS Tests\Features\Fixture
+ ✓ it may return a file path
+ ✓ it may throw an exception if the file does not exist
+
WARN Tests\Features\Helpers
✓ it can set/get properties on $this
! it gets null if property do not exist → Undefined property Tests\Features\Helpers::$wqdwqdqw
@@ -1104,6 +1139,25 @@
// This is before each describe runtime note
// This is before each nested describe runtime note
// This is a runtime note within a nested describe
+ ✓ matching describe names → describe block → it may have a static note and runtime note
+ // This is before each static note
+ // This is before each matching describe static note
+ // This is a nested matching static note
+ // This is a static note within a matching describe
+ // This is before each runtime note
+ // This is before each matching describe runtime note
+ // This is before each matching describe runtime note
+ // This is a runtime note within a matching describe
+ ✓ matching describe names → describe block → it may have a static note and runtime note, that are different than the matching describe block
+ // This is before each static note
+ // This is before each matching describe static note
+ // This is a nested matching static note, and should not contain the matching describe notes
+ // This is before each matching describe static note, and should not contain the matching describe notes
+ // This is a static note within a matching describe, and should not contain the matching describe notes
+ // This is before each runtime note
+ // This is before each matching describe runtime note
+ // This is before each matching describe runtime note, and should not contain the matching describe notes
+ // This is a runtime note within a matching describe, and should not contain the matching describe notes
✓ multiple notes
// This is before each static note
// This is before each runtime note
@@ -1119,6 +1173,10 @@
✓ nested → it may be associated with an pr #1, #4, #5, #6, #3
// an note between an the pr
+ PASS Tests\Features\References
+ ✓ it can reference a specific class
+ ✓ it can reference a specific class method
+
PASS Tests\Features\Repeat
✓ once
✓ multiple times @ repetition 1 of 5
@@ -1250,6 +1308,10 @@
✓ describe blocks → describe with repeat → nested describe with repeat → test with no repeat should repeat the number of times specified in the parent describe block @ repetition 2 of 2
✓ describe blocks → describe with repeat → nested describe with repeat → test with repeat should repeat the number of times specified in the test @ repetition 1 of 2
✓ describe blocks → describe with repeat → nested describe with repeat → test with repeat should repeat the number of times specified in the test @ repetition 2 of 2
+ ✓ matching describe blocks → describe block → it should repeat the number of times specified in the parent describe block @ repetition 1 of 3
+ ✓ matching describe blocks → describe block → it should repeat the number of times specified in the parent describe block @ repetition 2 of 3
+ ✓ matching describe blocks → describe block → it should repeat the number of times specified in the parent describe block @ repetition 3 of 3
+ ✓ matching describe blocks → describe block → should not repeat the number of times of the describe block with the same name
PASS Tests\Features\ScopedDatasets\Directory\NestedDirectory1\TestFileInNestedDirectoryWithDatasetsFile
✓ uses dataset with (1)
@@ -1295,6 +1357,10 @@
✓ it can see datasets defined in Pest.php file with ('B')
✓ Pest.php dataset is taken
+ PASS Tests\Features\See
+ ✓ it can reference a specific class
+ ✓ it can reference a specific class method
+
WARN Tests\Features\Skip
✓ it do not skips
- it skips with truthy → 1
@@ -1312,6 +1378,12 @@
- skip on beforeEach → skipped tests → nested inside skipped block → it should not execute
- skip on beforeEach → skipped tests → it should not execute
✓ skip on beforeEach → it should execute
+ - matching describe with skip → describe block → it should not execute
+ ✓ matching describe with skip → describe block → it should execute a test in a describe block with the same name as a skipped describe block
+ ✓ matching describe with skip → it should execute
+ - matching describe with skip on beforeEach → describe block → it should not execute
+ ✓ matching describe with skip on beforeEach → describe block → it should execute a test in a describe block with the same name as a skipped describe block
+ ✓ matching describe with skip on beforeEach → it should execute
✓ it does not skip after the describe block
- it can skip after the describe block
@@ -1334,7 +1406,7 @@
✓ nested → it may be associated with an ticket #1, #4, #5, #6, #3
// an note between an the ticket
- PASS Tests\Features\Todo - 28 todos
+ PASS Tests\Features\Todo - 29 todos
↓ something todo later
↓ something todo later chained
↓ something todo later chained and with function body
@@ -1366,6 +1438,9 @@
// test note
↓ todo on describe → todo block → it should not execute
✓ todo on describe → it should execute
+ ↓ todo on describe with matching name → describe block → it should not execute
+ ✓ todo on describe with matching name → describe block → it should execute a test in a describe block with the same name as a todo describe block
+ ✓ todo on describe with matching name → it should execute
↓ todo on test after describe block
↓ todo with note on test after describe block
// test note
@@ -1425,6 +1500,11 @@
✓ nested → nested afterEach execution order
✓ global afterEach execution order
+ PASS Tests\Hooks\BeforeAllTest
+ ✓ it gets called before all tests 1 @ repetition 1 of 2
+ ✓ it gets called before all tests 1 @ repetition 2 of 2
+ ✓ it gets called before all tests 2
+
PASS Tests\Hooks\BeforeEachTest
✓ global beforeEach execution order
@@ -1698,4 +1778,8 @@
WARN Tests\Visual\Version
- visual snapshot of help command output
- Tests: 2 deprecated, 4 warnings, 5 incomplete, 2 notices, 38 todos, 33 skipped, 1144 passed (2736 assertions)
\ No newline at end of file
+ PASS Testsexternal\Features\Expect\toMatchSnapshot
+ ✓ 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, 39 todos, 35 skipped, 1188 passed (2813 assertions)
\ No newline at end of file
diff --git a/tests/.tests/SuccessOnly.php b/tests/.tests/SuccessOnly.php
index cb4009a68..4d231a8dd 100644
--- a/tests/.tests/SuccessOnly.php
+++ b/tests/.tests/SuccessOnly.php
@@ -13,3 +13,9 @@
test('can pass with dataset', function ($value) {
expect($value)->toEqual(true);
})->with([true]);
+
+describe('block', function () {
+ test('can pass with dataset in describe block', function ($number) {
+ expect($number)->toBeInt();
+ })->with([1]);
+});
diff --git a/tests/Arch.php b/tests/Arch.php
index d8deb4609..6348a0f31 100644
--- a/tests/Arch.php
+++ b/tests/Arch.php
@@ -27,26 +27,6 @@
->not->toBeUsed()
->ignoring(Expectation::class);
-arch('dependencies')
- ->expect('Pest')
- ->toOnlyUse([
- 'dd',
- 'dump',
- 'expect',
- 'uses',
- 'Termwind',
- 'ParaTest',
- 'Pest\Arch',
- 'Pest\Mutate\Contracts\Configuration',
- 'Pest\Mutate\Decorators\TestCallDecorator',
- 'Pest\Mutate\Repositories\ConfigurationRepository',
- 'Pest\Plugin',
- 'NunoMaduro\Collision',
- 'Whoops',
- 'Symfony\Component\Console',
- 'Symfony\Component\Process',
- ])->ignoring(['Composer', 'PHPUnit', 'SebastianBergmann']);
-
arch('contracts')
->expect('Pest\Contracts')
->toOnlyUse([
diff --git a/tests/Features/AfterEach.php b/tests/Features/AfterEach.php
index ea0dd7ea6..aff68283c 100644
--- a/tests/Features/AfterEach.php
+++ b/tests/Features/AfterEach.php
@@ -48,3 +48,55 @@
});
});
});
+
+describe('matching describe block names', function () {
+ afterEach(function () {
+ $this->state->foo = 1;
+ });
+
+ describe('outer', function () {
+ afterEach(function () {
+ $this->state->foo++;
+ });
+
+ describe('middle', function () {
+ afterEach(function () {
+ $this->state->foo++;
+ });
+
+ describe('inner', function () {
+ afterEach(function () {
+ $this->state->foo++;
+ });
+
+ it('does not get executed before the test', function () {
+ expect($this)->not->toHaveProperty('foo');
+ });
+
+ it('should call all parent afterEach functions', function () {
+ expect($this->state->foo)->toBe(4);
+ });
+ });
+ });
+
+ describe('middle', function () {
+ it('does not get executed before the test', function () {
+ expect($this)->not->toHaveProperty('foo');
+ });
+
+ it('should not call afterEach functions for sibling describe blocks with the same name', function () {
+ expect($this)->not->toHaveProperty('foo');
+ });
+ });
+
+ describe('inner', function () {
+ it('does not get executed before the test', function () {
+ expect($this)->not->toHaveProperty('foo');
+ });
+
+ it('should not call afterEach functions for descendent of sibling describe blocks with the same name', function () {
+ expect($this)->not->toHaveProperty('foo');
+ });
+ });
+ });
+});
diff --git a/tests/Features/BeforeEach.php b/tests/Features/BeforeEach.php
index 04f0dd13e..a7c7befe2 100644
--- a/tests/Features/BeforeEach.php
+++ b/tests/Features/BeforeEach.php
@@ -51,3 +51,78 @@
test('test', function () {});
});
+
+describe('matching describe block names', function () {
+ beforeEach(function () {
+ $this->foo = 1;
+ });
+
+ describe('outer', function () {
+ beforeEach(function () {
+ $this->foo++;
+ });
+
+ describe('middle', function () {
+ beforeEach(function () {
+ $this->foo++;
+ });
+
+ describe('inner', function () {
+ beforeEach(function () {
+ $this->foo++;
+ });
+
+ it('should call all parent beforeEach functions', function () {
+ expect($this->foo)->toBe(4);
+ });
+ });
+ });
+
+ describe('middle', function () {
+ it('should not call beforeEach functions for sibling describe blocks with the same name', function () {
+ expect($this->foo)->toBe(2);
+ });
+ });
+
+ describe('inner', function () {
+ it('should not call beforeEach functions for descendent of sibling describe blocks with the same name', function () {
+ expect($this->foo)->toBe(2);
+ });
+ });
+ });
+});
+
+$matchingNameCalls = 0;
+describe('matching name', function () use (&$matchingNameCalls) {
+ beforeEach(function () use (&$matchingNameCalls) {
+ $matchingNameCalls++;
+ });
+
+ it('should call the before each', function () use (&$matchingNameCalls) {
+ expect($matchingNameCalls)->toBe(1);
+ });
+});
+
+describe('matching name', function () use (&$matchingNameCalls) {
+ it('should not call the before each on the describe block with the same name', function () use (&$matchingNameCalls) {
+ expect($matchingNameCalls)->toBe(1);
+ });
+});
+
+beforeEach(function () {
+ $this->baz = 1;
+});
+
+describe('called on all tests', function () {
+ beforeEach(function () {
+ $this->baz++;
+ });
+
+ test('beforeEach should be called', function () {
+ expect($this->baz)->toBe(2);
+ });
+
+ test('beforeEach should be called for all tests', function () {
+ expect($this->baz)->toBe(2);
+ });
+});
diff --git a/tests/Features/Covers.php b/tests/Features/Covers.php
deleted file mode 100644
index 386d523f3..000000000
--- a/tests/Features/Covers.php
+++ /dev/null
@@ -1,59 +0,0 @@
-getAttributes();
-
- expect($attributes[1]->getName())->toBe('PHPUnit\Framework\Attributes\CoversClass');
- expect($attributes[1]->getArguments()[0])->toBe('Tests\Fixtures\Covers\CoversClass1');
-});
-
-it('uses the correct PHPUnit attribute for function', function () {
- $attributes = (new ReflectionClass($this))->getAttributes();
-
- expect($attributes[3]->getName())->toBe('PHPUnit\Framework\Attributes\CoversFunction');
- expect($attributes[3]->getArguments()[0])->toBe('testCoversFunction');
-})->coversFunction('testCoversFunction');
-
-it('guesses if the given argument is a class or function', function () {
- $attributes = (new ReflectionClass($this))->getAttributes();
-
- expect($attributes[5]->getName())->toBe(CoversClass::class);
- expect($attributes[5]->getArguments()[0])->toBe(CoversClass3::class);
-
- expect($attributes[6]->getName())->toBe(CoversFunction::class);
- expect($attributes[6]->getArguments()[0])->toBe('testCoversFunction');
-})->covers(CoversClass3::class, 'testCoversFunction');
-
-it('uses the correct PHPUnit attribute for trait', function () {
- $attributes = (new ReflectionClass($this))->getAttributes();
-
- expect($attributes[8]->getName())->toBe('PHPUnit\Framework\Attributes\CoversTrait');
- expect($attributes[8]->getArguments()[0])->toBe('Tests\Fixtures\Covers\CoversTrait');
-})->coversTrait(CoversTrait::class);
-
-it('uses the correct PHPUnit attribute for covers nothing', function () {
- $attributes = (new ReflectionMethod($this, $this->name()))->getAttributes();
-
- expect($attributes[3]->getName())->toBe('PHPUnit\Framework\Attributes\CoversNothing');
- expect($attributes[3]->getArguments())->toHaveCount(0);
-})->coversNothing();
-
-it('throws exception if no class nor method has been found', function () {
- $testCall = new TestCall(TestSuite::getInstance(), 'filename', 'description', fn () => 'closure');
-
- $testCall->covers('fakeName');
-})->throws(InvalidArgumentException::class, 'No class, trait or method named "fakeName" has been found.');
diff --git a/tests/Features/Covers/ClassCoverage.php b/tests/Features/Covers/ClassCoverage.php
new file mode 100644
index 000000000..ab805e170
--- /dev/null
+++ b/tests/Features/Covers/ClassCoverage.php
@@ -0,0 +1,13 @@
+getAttributes();
+
+ expect($attributes[1]->getName())->toBe(CoversClass::class);
+ expect($attributes[1]->getArguments()[0])->toBe('Tests\Fixtures\Covers\CoversClass1');
+});
diff --git a/tests/Features/Covers/ExceptionHandling.php b/tests/Features/Covers/ExceptionHandling.php
new file mode 100644
index 000000000..de86bb023
--- /dev/null
+++ b/tests/Features/Covers/ExceptionHandling.php
@@ -0,0 +1,10 @@
+ 'closure');
+
+ $testCall->covers('fakeName');
+})->throws(InvalidArgumentException::class, 'No class, trait or method named "fakeName" has been found.');
diff --git a/tests/Features/Covers/FunctionCoverage.php b/tests/Features/Covers/FunctionCoverage.php
new file mode 100644
index 000000000..fba97080f
--- /dev/null
+++ b/tests/Features/Covers/FunctionCoverage.php
@@ -0,0 +1,12 @@
+getAttributes();
+
+ expect($attributes[1]->getName())->toBe(CoversFunction::class);
+ expect($attributes[1]->getArguments()[0])->toBe('testCoversFunction');
+})->coversFunction('testCoversFunction');
diff --git a/tests/Features/Covers/GuessCoverage.php b/tests/Features/Covers/GuessCoverage.php
new file mode 100644
index 000000000..e8a84035b
--- /dev/null
+++ b/tests/Features/Covers/GuessCoverage.php
@@ -0,0 +1,17 @@
+getAttributes();
+
+ expect($attributes[1]->getName())->toBe(CoversClass::class);
+ expect($attributes[1]->getArguments()[0])->toBe(CoversClass3::class);
+
+ expect($attributes[2]->getName())->toBe(CoversFunction::class);
+ expect($attributes[2]->getArguments()[0])->toBe('testCoversFunction2');
+})->covers(CoversClass3::class, 'testCoversFunction2');
diff --git a/tests/Features/Covers/TraitCoverage.php b/tests/Features/Covers/TraitCoverage.php
new file mode 100644
index 000000000..57bc36806
--- /dev/null
+++ b/tests/Features/Covers/TraitCoverage.php
@@ -0,0 +1,11 @@
+getAttributes();
+
+ expect($attributes[1]->getName())->toBe(PHPUnitCoversTrait::class);
+ expect($attributes[1]->getArguments()[0])->toBe('Tests\Fixtures\Covers\CoversTrait');
+})->coversTrait(CoversTrait::class);
diff --git a/tests/Features/DatasetsTests.php b/tests/Features/DatasetsTests.php
index 54cb77a83..837b56f62 100644
--- a/tests/Features/DatasetsTests.php
+++ b/tests/Features/DatasetsTests.php
@@ -415,6 +415,34 @@ function () {
})->with([1]);
});
+describe('matching describe block names', function () {
+ describe('outer', function () {
+ test('before inner describe block', function (...$args) {
+ expect($args)->toBe([1]);
+ });
+
+ describe('inner', function () {
+ it('should include the with value from all parent describe blocks', function (...$args) {
+ expect($args)->toBe([1, 2]);
+ });
+
+ test('should include the with value from all parent describe blocks and the test', function (...$args) {
+ expect($args)->toBe([1, 2, 3]);
+ })->with([3]);
+ })->with([2]);
+
+ describe('inner', function () {
+ it('should not include the value from the other describe block with the same name', function (...$args) {
+ expect($args)->toBe([1]);
+ });
+ });
+
+ test('after inner describe block', function (...$args) {
+ expect($args)->toBe([1]);
+ });
+ })->with([1]);
+});
+
test('after describe block', function (...$args) {
expect($args)->toBe([5]);
})->with([5]);
diff --git a/tests/Features/Expect/toBeSlug.php b/tests/Features/Expect/toBeSlug.php
new file mode 100644
index 000000000..2d7c19f8a
--- /dev/null
+++ b/tests/Features/Expect/toBeSlug.php
@@ -0,0 +1,24 @@
+toBeSlug()
+ ->and('Another Test String')->toBeSlug();
+});
+
+test('failures', function () {
+ expect('')->toBeSlug();
+})->throws(ExpectationFailedException::class);
+
+test('failures with custom message', function () {
+ expect('')->toBeSlug('oh no!');
+})->throws(ExpectationFailedException::class, 'oh no!');
+
+test('failures with default message', function () {
+ expect('')->toBeSlug();
+})->throws(ExpectationFailedException::class, 'Failed asserting that can be converted to a slug.');
+
+test('not failures', function () {
+ expect('This is a Test String!')->not->toBeSlug();
+})->throws(ExpectationFailedException::class);
diff --git a/tests/Features/Expect/toMatchSnapshot.php b/tests/Features/Expect/toMatchSnapshot.php
index c3df8bace..0c09be936 100644
--- a/tests/Features/Expect/toMatchSnapshot.php
+++ b/tests/Features/Expect/toMatchSnapshot.php
@@ -74,7 +74,8 @@ public function toString()
TestSuite::getInstance()->snapshots->save($this->snapshotable);
[$filename] = TestSuite::getInstance()->snapshots->get();
- expect($filename)->toEndWith('pass_with_dataset_with_data_set____my_datas_set_value______my_datas_set_value__.snap')
+ expect($filename)->toStartWith('tests/.pest/snapshots/')
+ ->toEndWith('pass_with_dataset_with_data_set____my_datas_set_value___.snap')
->and($this->snapshotable)->toMatchSnapshot();
})->with(['my-datas-set-value']);
@@ -83,7 +84,8 @@ public function toString()
TestSuite::getInstance()->snapshots->save($this->snapshotable);
[$filename] = TestSuite::getInstance()->snapshots->get();
- expect($filename)->toEndWith('pass_with_dataset_with_data_set____my_datas_set_value______my_datas_set_value__.snap')
+ expect($filename)->toStartWith('tests/.pest/snapshots/')
+ ->toEndWith('pass_with_dataset_with_data_set____my_datas_set_value___.snap')
->and($this->snapshotable)->toMatchSnapshot();
});
})->with(['my-datas-set-value']);
diff --git a/tests/Features/Fixture.php b/tests/Features/Fixture.php
new file mode 100644
index 000000000..1ac538b84
--- /dev/null
+++ b/tests/Features/Fixture.php
@@ -0,0 +1,12 @@
+toBeString()
+ ->toBeFile();
+});
+
+it('may throw an exception if the file does not exist', function () {
+ fixture('file-that-does-not-exist.php');
+})->throws(InvalidArgumentException::class);
diff --git a/tests/Features/Note.php b/tests/Features/Note.php
index 571d7c851..22d73b4ff 100644
--- a/tests/Features/Note.php
+++ b/tests/Features/Note.php
@@ -44,6 +44,36 @@
})->note('This is a nested describe static note');
})->note('This is describe static note');
+describe('matching describe names', function () {
+ beforeEach(function () {
+ $this->note('This is before each matching describe runtime note');
+ })->note('This is before each matching describe static note');
+
+ describe('describe block', function () {
+ beforeEach(function () {
+ $this->note('This is before each matching describe runtime note');
+ })->note('This is before each matching describe static note');
+
+ it('may have a static note and runtime note', function () {
+ expect(true)->toBeTrue(true);
+
+ $this->note('This is a runtime note within a matching describe');
+ })->note('This is a static note within a matching describe');
+ })->note('This is a nested matching static note');
+
+ describe('describe block', function () {
+ beforeEach(function () {
+ $this->note('This is before each matching describe runtime note, and should not contain the matching describe notes');
+ })->note('This is before each matching describe static note, and should not contain the matching describe notes');
+
+ it('may have a static note and runtime note, that are different than the matching describe block', function () {
+ expect(true)->toBeTrue(true);
+
+ $this->note('This is a runtime note within a matching describe, and should not contain the matching describe notes');
+ })->note('This is a static note within a matching describe, and should not contain the matching describe notes');
+ })->note('This is a nested matching static note, and should not contain the matching describe notes');
+});
+
test('multiple notes', function () {
expect(true)->toBeTrue(true);
diff --git a/tests/Features/References.php b/tests/Features/References.php
new file mode 100644
index 000000000..a19db35a1
--- /dev/null
+++ b/tests/Features/References.php
@@ -0,0 +1,11 @@
+toBeString();
+})->references(Panic::class);
+
+it('can reference a specific class method', function () {
+ expect(Panic::with(...))->toBeCallable();
+})->references([Panic::class, 'with']);
diff --git a/tests/Features/Repeat.php b/tests/Features/Repeat.php
index 5a3370cd7..89d0c322e 100644
--- a/tests/Features/Repeat.php
+++ b/tests/Features/Repeat.php
@@ -79,3 +79,17 @@
})->repeat(times: 2);
})->repeat(times: 3);
});
+
+describe('matching describe blocks', function () {
+ describe('describe block', function () {
+ it('should repeat the number of times specified in the parent describe block', function () {
+ expect(true)->toBeTrue();
+ });
+ })->repeat(times: 3);
+
+ describe('describe block', function () {
+ test('should not repeat the number of times of the describe block with the same name', function () {
+ expect(true)->toBeTrue();
+ });
+ });
+});
diff --git a/tests/Features/See.php b/tests/Features/See.php
new file mode 100644
index 000000000..7c9393eeb
--- /dev/null
+++ b/tests/Features/See.php
@@ -0,0 +1,11 @@
+toBeString();
+})->see(Panic::class);
+
+it('can reference a specific class method', function () {
+ expect(Panic::with(...))->toBeCallable();
+})->see([Panic::class, 'with']);
diff --git a/tests/Features/Skip.php b/tests/Features/Skip.php
index 352189458..9c305d356 100644
--- a/tests/Features/Skip.php
+++ b/tests/Features/Skip.php
@@ -125,6 +125,74 @@
});
});
+describe('matching describe with skip', function () {
+ beforeEach(function () {
+ $this->ran = false;
+ });
+
+ afterEach(function () {
+ match ($this->name()) {
+ '__pest_evaluable__matching_describe_with_skip__→__describe_block__→_it_should_not_execute' => expect($this->ran)->toBe(false),
+ '__pest_evaluable__matching_describe_with_skip__→__describe_block__→_it_should_execute_a_test_in_a_describe_block_with_the_same_name_as_a_skipped_describe_block' => expect($this->ran)->toBe(true),
+ '__pest_evaluable__matching_describe_with_skip__→_it_should_execute' => expect($this->ran)->toBe(true),
+ default => $this->fail('Unexpected test name: '.$this->name()),
+ };
+ });
+
+ describe('describe block', function () {
+ it('should not execute', function () {
+ $this->ran = true;
+ $this->fail();
+ });
+ })->skip();
+
+ describe('describe block', function () {
+ it('should execute a test in a describe block with the same name as a skipped describe block', function () {
+ $this->ran = true;
+ });
+ });
+
+ it('should execute', function () {
+ $this->ran = true;
+ expect($this->ran)->toBe(true);
+ });
+});
+
+describe('matching describe with skip on beforeEach', function () {
+ beforeEach(function () {
+ $this->ran = false;
+ });
+
+ afterEach(function () {
+ match ($this->name()) {
+ '__pest_evaluable__matching_describe_with_skip_on_beforeEach__→__describe_block__→_it_should_not_execute' => expect($this->ran)->toBe(false),
+ '__pest_evaluable__matching_describe_with_skip_on_beforeEach__→__describe_block__→_it_should_execute_a_test_in_a_describe_block_with_the_same_name_as_a_skipped_describe_block' => expect($this->ran)->toBe(true),
+ '__pest_evaluable__matching_describe_with_skip_on_beforeEach__→_it_should_execute' => expect($this->ran)->toBe(true),
+ default => $this->fail('Unexpected test name: '.$this->name()),
+ };
+ });
+
+ describe('describe block', function () {
+ beforeEach()->skip();
+
+ it('should not execute', function () {
+ $this->ran = true;
+ $this->fail();
+ });
+ });
+
+ describe('describe block', function () {
+ it('should execute a test in a describe block with the same name as a skipped describe block', function () {
+ $this->ran = true;
+ });
+ });
+
+ it('should execute', function () {
+ $this->ran = true;
+ expect($this->ran)->toBe(true);
+ });
+});
+
it('does not skip after the describe block', function () {
expect(true)->toBeTrue();
});
diff --git a/tests/Features/Todo.php b/tests/Features/Todo.php
index f979a2ff0..d6e8d9781 100644
--- a/tests/Features/Todo.php
+++ b/tests/Features/Todo.php
@@ -108,6 +108,40 @@
});
});
+describe('todo on describe with matching name', function () {
+ beforeEach(function () {
+ $this->ran = false;
+ });
+
+ afterEach(function () {
+ match ($this->name()) {
+ '__pest_evaluable__todo_on_describe_with_matching_name__→__describe_block__→_it_should_not_execute' => expect($this->ran)->toBe(false),
+ '__pest_evaluable__todo_on_describe_with_matching_name__→__describe_block__→_it_should_execute_a_test_in_a_describe_block_with_the_same_name_as_a_todo_describe_block' => expect($this->ran)->toBe(true),
+ '__pest_evaluable__todo_on_describe_with_matching_name__→_it_should_execute' => expect($this->ran)->toBe(true),
+
+ default => $this->fail('Unexpected test name: '.$this->name()),
+ };
+ });
+
+ describe('describe block', function () {
+ it('should not execute', function () {
+ $this->ran = true;
+ $this->fail();
+ });
+ })->todo();
+
+ describe('describe block', function () {
+ it('should execute a test in a describe block with the same name as a todo describe block', function () {
+ $this->ran = true;
+ });
+ });
+
+ it('should execute', function () {
+ $this->ran = true;
+ expect($this->ran)->toBe(true);
+ });
+});
+
test('todo on test after describe block', function () {
$this->fail();
})->todo();
diff --git a/tests/Hooks/BeforeAllTest.php b/tests/Hooks/BeforeAllTest.php
new file mode 100644
index 000000000..d411d263e
--- /dev/null
+++ b/tests/Hooks/BeforeAllTest.php
@@ -0,0 +1,16 @@
+beforeAll(function () {
+ expect($_SERVER['globalHook']->calls->beforeAll)
+ ->toBe(0);
+
+ $_SERVER['globalHook']->calls->beforeAll++;
+});
+
+it('gets called before all tests 1', function () {
+ expect($_SERVER['globalHook']->calls->beforeAll)->toBe(1);
+})->repeat(2);
+
+it('gets called before all tests 2', function () {
+ expect($_SERVER['globalHook']->calls->beforeAll)->toBe(1);
+});
diff --git a/tests/Pest.php b/tests/Pest.php
index a938fc7e4..e498450cd 100644
--- a/tests/Pest.php
+++ b/tests/Pest.php
@@ -29,7 +29,6 @@
})
->beforeAll(function () {
$_SERVER['globalHook']->beforeAll = 0;
- $_SERVER['globalHook']->calls->beforeAll++;
})
->afterEach(function () {
if (! isset($this->ith)) {
diff --git a/tests/Unit/Support/StateGenerator.php b/tests/Unit/Support/StateGenerator.php
new file mode 100644
index 000000000..44b2ca43b
--- /dev/null
+++ b/tests/Unit/Support/StateGenerator.php
@@ -0,0 +1,106 @@
+fromPhpUnitTestResult(0, $phpunitResult);
+
+ // The AfterLastTestMethodErrored event should be processed and added to state
+ expect($state->suiteTests)->toHaveCount(1);
+});
+
+it('handles BeforeFirstTestMethodErrored correctly', function (): void {
+ $generator = new StateGenerator;
+
+ $throwable = ThrowableBuilder::from(new RuntimeException('Setup error'));
+
+ $beforeFirstEvent = new BeforeFirstTestMethodErrored(
+ makeTelemetryInfo(),
+ \PHPUnit\Framework\TestCase::class,
+ new ClassMethod(\PHPUnit\Framework\TestCase::class, 'setUp'),
+ $throwable,
+ );
+
+ $phpunitResult = makePHPUnitTestResult([$beforeFirstEvent]);
+ $state = $generator->fromPhpUnitTestResult(0, $phpunitResult);
+
+ expect($state->suiteTests)->toHaveCount(1);
+});
+
+it('handles mixed errored events without TypeError', function (): void {
+ $generator = new StateGenerator;
+
+ $throwable = ThrowableBuilder::from(new RuntimeException('Error'));
+
+ $beforeEvent = new BeforeFirstTestMethodErrored(
+ makeTelemetryInfo(),
+ \PHPUnit\Framework\TestCase::class,
+ new ClassMethod(\PHPUnit\Framework\TestCase::class, 'setUp'),
+ $throwable,
+ );
+
+ $afterEvent = new AfterLastTestMethodErrored(
+ makeTelemetryInfo(),
+ \PHPUnit\Framework\TestCase::class,
+ new ClassMethod(\PHPUnit\Framework\TestCase::class, 'tearDown'),
+ $throwable,
+ );
+
+ $phpunitResult = makePHPUnitTestResult([$beforeEvent, $afterEvent]);
+ $state = $generator->fromPhpUnitTestResult(0, $phpunitResult);
+
+ // Both events share the same testClassName key, so the second overwrites the first
+ expect($state->suiteTests)->toHaveCount(1);
+});
diff --git a/tests/Unit/TestSuite.php b/tests/Unit/TestSuite.php
index 5de7a9020..93c8a294e 100644
--- a/tests/Unit/TestSuite.php
+++ b/tests/Unit/TestSuite.php
@@ -27,7 +27,7 @@
$testSuite->tests->set($method);
})->throws(
TestClosureMustNotBeStatic::class,
- 'Test closure must not be static. Please remove the `static` keyword from the `bar` method in `foo`.',
+ 'Test closure must not be static. Please remove the [static] keyword from the [bar] method in [foo].',
);
it('alerts users about tests with arguments but no input', function () {
@@ -40,7 +40,7 @@
$testSuite->tests->set($method);
})->throws(
DatasetMissing::class,
- sprintf("A test with the description '%s' has %d argument(s) ([%s]) and no dataset(s) provided in %s", 'bar', 1, 'int $arg', 'foo'),
+ sprintf('A test with the description [%s] has [%d] argument(s) ([%s]) and no dataset(s) provided in [%s]', 'bar', 1, 'int $arg', 'foo'),
);
it('can return an array of all test suite filenames', function () {
diff --git a/tests/Visual/JUnit.php b/tests/Visual/JUnit.php
index 3523bdd6f..fd34ea7a9 100644
--- a/tests/Visual/JUnit.php
+++ b/tests/Visual/JUnit.php
@@ -36,8 +36,8 @@
expect($result['testsuite']['@attributes'])
->name->toBe('Tests\tests\SuccessOnly')
->file->toBe($normalizedPath('tests/.tests/SuccessOnly.php'))
- ->tests->toBe('3')
- ->assertions->toBe('3')
+ ->tests->toBe('4')
+ ->assertions->toBe('4')
->errors->toBe('0')
->failures->toBe('0')
->skipped->toBe('0');
diff --git a/tests/Visual/Parallel.php b/tests/Visual/Parallel.php
index 313f82088..1aced21dc 100644
--- a/tests/Visual/Parallel.php
+++ b/tests/Visual/Parallel.php
@@ -16,7 +16,7 @@
test('parallel', function () use ($run) {
expect($run('--exclude-group=integration'))
- ->toContain('Tests: 2 deprecated, 4 warnings, 5 incomplete, 2 notices, 38 todos, 24 skipped, 1134 passed (2712 assertions)')
+ ->toContain('Tests: 2 deprecated, 4 warnings, 5 incomplete, 2 notices, 39 todos, 26 skipped, 1177 passed (2789 assertions)')
->toContain('Parallel: 3 processes');
})->skipOnWindows();