Compare commits

...

46 Commits

Author SHA1 Message Date
nuno maduro 2e4f8ad56f chore: fixes snapshots 2026-08-07 15:43:17 +01:00
nuno maduro 8c554a81e2 chore: refactors exceptions 2026-08-07 15:26:55 +01:00
nuno maduro 42d9b777bf chore: coding style changes 2026-08-07 14:52:56 +01:00
nuno maduro eb88513baf fix: livewire 2026-08-07 14:27:26 +01:00
nuno maduro 7b6415ccd0 fix 2026-08-07 03:15:22 +01:00
nuno maduro a5d726b678 release: 5.0.4 2026-08-07 03:00:25 +01:00
nuno maduro 93223caa56 chore: fixes ci 2026-08-07 03:00:19 +01:00
nuno maduro d0572ce138 wip 2026-08-07 02:26:45 +01:00
nuno maduro b0d1fc589d refactor 2026-08-07 02:12:24 +01:00
nuno maduro 5a4f124199 wip 2026-08-07 02:06:28 +01:00
nuno maduro c3ceb7d0c7 qwd 2026-08-07 02:00:58 +01:00
nuno maduro 16d1208344 wip 2026-08-07 01:58:41 +01:00
nuno maduro 1cebc84c2d wip 2026-08-07 01:55:51 +01:00
nuno maduro 1f79660add wip 2026-08-07 01:55:34 +01:00
nuno maduro 37bdf60a1c wip 2026-08-07 01:41:32 +01:00
nuno maduro cc7acfe485 wdq 2026-08-07 01:11:53 +01:00
nuno maduro 7bfb2a6185 chore: upgrade snapshots 2026-08-06 22:24:26 +01:00
nuno maduro ca56ca5216 dw 2026-08-06 22:19:47 +01:00
nuno maduro b49ba062d5 wq 2026-08-06 22:05:15 +01:00
nuno maduro 09699847a2 wqd 2026-08-06 20:53:09 +01:00
nuno maduro 545d0c2784 wip 2026-08-06 20:48:22 +01:00
nuno maduro b795af3b1b wip 2026-08-06 17:52:04 +01:00
nuno maduro a40cc6bc0e more tests 2026-08-06 16:35:03 +01:00
nuno maduro 4d3d0105b7 chore: style 2026-08-06 16:02:36 +01:00
nuno maduro e90e4f70fc wip 2026-08-05 20:20:35 +01:00
nuno maduro d112582857 wip 2026-08-05 19:58:57 +01:00
nuno maduro 9f3c4e1e82 fix 2026-08-05 18:51:49 +01:00
nuno maduro 71f39366c2 wip 2026-08-05 18:21:34 +01:00
nuno maduro bfd5b75677 fix 2026-08-05 16:49:35 +01:00
nuno maduro 411b9954b5 wip 2026-08-05 16:18:03 +01:00
nuno maduro db70017cb2 wip 2026-08-05 03:30:50 +01:00
nuno maduro 86adaedbbd fix: tia filtered 2026-08-04 23:30:03 +01:00
nuno maduro 92c7677c6e fix: don't allow invalid class names 2026-08-04 20:46:21 +01:00
nuno maduro 872f0a50c2 fix(tia): replayable scenarios running afterEach 2026-08-04 20:19:12 +01:00
nuno maduro 668809bc75 chore: adds missing test 2026-08-04 19:28:58 +01:00
nuno maduro 8d8f45c843 chore: bump deps 2026-08-04 19:28:52 +01:00
nuno maduro 086b3e9107 chore: ai instructions 2026-08-04 19:01:16 +01:00
nuno maduro 43fe26f324 chore: bumps deps 2026-08-04 19:01:08 +01:00
nuno maduro 865c1e5113 ci: snapshots 2026-08-04 18:51:04 +01:00
stephanbouman 19eed8d581 Fixed typos (#1819)
* Fixed typo in message

* Fixed typo in message
2026-08-03 11:29:03 -04:00
Danny de Wit ad1850b110 fix: keep TableExtractor table names as strings (#1794)
PHP coerces numeric-string array keys to ints, so collecting table names in
$tables[$name] and returning array_keys() can yield ints. That breaks the
declared list<string> and throws a TypeError in Recorder::linkTable(string).

Standard SQL like substring(x FROM 1 FOR 10) is enough to trigger it, since
the numeric operand matches the FROM pattern.

Closes #1793

Co-authored-by: Danny de Wit <d.dewit@fenceweb.com>
2026-08-02 22:50:47 -04:00
nuno maduro 585de259a2 chore: style 2026-08-02 22:26:34 -04:00
nuno maduro 953664dce0 release: v5.0.3 2026-08-02 22:24:32 -04:00
nuno maduro 1dd959848c chore: updates snapshots 2026-08-02 22:24:24 -04:00
Jeffrey van Hees 047753c836 fix: key Tia results per dataset row (#1799)
The result cache was keyed `Class::method` on both sides, so every row of a
`->with()` test shared one entry and the last writer won. On replay that single
status was handed to every row.

`TestMethod::id()` already appends `#dataSetName`, and `TestCase` reaches the
same value object through `valueObjectForEvents()`, so both sides can use it.
2026-08-02 21:43:15 -04:00
nuno maduro b63626a94d docs: adjusts readme
Signed-off-by: nuno maduro <enunomaduro@gmail.com>
2026-07-30 11:53:41 -04:00
315 changed files with 6630 additions and 3309 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
.idea/* .idea/*
.idea/codeStyleSettings.xml .idea/codeStyleSettings.xml
.temp/* .temp/*
composer.lock /composer.lock
/vendor/ /vendor/
coverage.xml coverage.xml
.phpunit.result.cache .phpunit.result.cache
+34
View File
@@ -0,0 +1,34 @@
# CLAUDE.md
**Do not edit this file.** Agents must never add, remove, or reword anything here. If a change seems needed, say so and let a human do it.
## Ask before testing
When asked to review code or build a feature, do not run the test suite and do not write new tests. Make the change, report it, then ask the user whether tests should be added — describing the tests you have in mind — and wait for the user to confirm.
Two reasons this matters here: the suite takes minutes, and `tests/.snapshots/success.txt` plus the tally in `tests/Visual/Parallel.php` encode the whole suite's result, so a single added test breaks both.
Once the user confirms:
```bash
composer test:unit # fast, excludes the visual group
composer test:integration # visual and snapshot tests
composer test # everything CI runs, in CI's order
composer update:snapshots # only when a test was added or removed
```
## TIA scenario tests
`tests/Features/Tia/*` scaffold a throwaway git project, run a real `pest` subprocess against it, and diff the TIA graph it wrote. They exist because TIA's contract is about what a run *writes* — replay, branch keys, and the COMPLETE / RESULTS-ONLY / HARD-SUPPRESSED tiers are invisible to ordinary assertions, and every case used to be measured by hand against a playground app.
Add one whenever a change touches branch resolution, replay, filtered mode, or the write tiers. How:
- `Project::make('master')` scaffolds; `seed('master')` writes a graph and sentinels every cached result (`time=9.999`, `assertions=42`) so any rewrite shows up.
- `$project->pest('--tia', …)` runs it; `$project->delta()` compares against that snapshot. `writtenCount()` is the discriminator — `0` means "replayed", not "wrote the same values". `mutateGraph()` bends one entry; overlays in `tests/Fixtures/Tia/overlays/<name>/` supply a different `tests/Pest.php`.
- Keep expectations driver-independent: a cold recording run needs pcov/xdebug and behaves differently without one. Seed a graph instead of recording one.
Run them by file (a directory argument finds nothing) or by `--filter`:
```bash
php bin/pest tests/Features/Tia/PartialRunWriteTier.php
```
+2 -1
View File
@@ -35,6 +35,7 @@ We cannot thank our sponsors enough for their incredible support in funding Pest
- **[SerpApi](https://serpapi.com/?ref=nunomaduro)** - **[SerpApi](https://serpapi.com/?ref=nunomaduro)**
- **[Typesense](https://typesense.org/?ref=nunomaduro)** - **[Typesense](https://typesense.org/?ref=nunomaduro)**
- **[Bento](https://bentonow.com/?ref=nunomaduro)** - **[Bento](https://bentonow.com/?ref=nunomaduro)**
- **[Redberry](https://redberry.international/laravel-development/)** - **[Pixel](https://wearepixel.com.au/?ref=nunomaduro)**
- **[Redberry](https://redberry.international/laravel-development/?ref=nunomaduro)**
Pest is an open-sourced software licensed under the **[MIT license](https://opensource.org/licenses/MIT)**. Pest is an open-sourced software licensed under the **[MIT license](https://opensource.org/licenses/MIT)**.
-1
View File
@@ -111,7 +111,6 @@ $bootPest = (static function (): void {
exit; exit;
} }
// It must be a 1 byte string to ensure filesize() is equal to the number of tests executed
$exitCode = $application->runTest(realpath(trim($testPath))); $exitCode = $application->runTest(realpath(trim($testPath)));
fwrite($statusFile, (string) $exitCode); fwrite($statusFile, (string) $exitCode);
+6 -4
View File
@@ -23,7 +23,7 @@
"nunomaduro/termwind": "^2.4.0", "nunomaduro/termwind": "^2.4.0",
"pestphp/pest-plugin": "^5.0.0", "pestphp/pest-plugin": "^5.0.0",
"pestphp/pest-plugin-arch": "^5.0.0", "pestphp/pest-plugin-arch": "^5.0.0",
"pestphp/pest-plugin-mutate": "^5.0.0", "pestphp/pest-plugin-mutate": "^5.0.1",
"pestphp/pest-plugin-profanity": "^5.0.0", "pestphp/pest-plugin-profanity": "^5.0.0",
"phpunit/phpunit": "^13.2.6", "phpunit/phpunit": "^13.2.6",
"symfony/process": "^8.1.0" "symfony/process": "^8.1.0"
@@ -48,6 +48,7 @@
"Tests\\Fixtures\\Covers\\": "tests/Fixtures/Covers", "Tests\\Fixtures\\Covers\\": "tests/Fixtures/Covers",
"Tests\\Fixtures\\Inheritance\\": "tests/Fixtures/Inheritance", "Tests\\Fixtures\\Inheritance\\": "tests/Fixtures/Inheritance",
"Tests\\Fixtures\\Arch\\": "tests/Fixtures/Arch", "Tests\\Fixtures\\Arch\\": "tests/Fixtures/Arch",
"Tests\\Fixtures\\Tia\\": "tests/Fixtures/Tia",
"Tests\\": "tests/PHPUnit/" "Tests\\": "tests/PHPUnit/"
}, },
"classmap": [ "classmap": [
@@ -58,12 +59,11 @@
] ]
}, },
"require-dev": { "require-dev": {
"laravel/pao": "^1.1.3",
"pestphp/pest-dev-tools": "^5.0.0", "pestphp/pest-dev-tools": "^5.0.0",
"pestphp/pest-plugin-browser": "^5.0.0", "pestphp/pest-plugin-browser": "^5.0.0",
"pestphp/pest-plugin-phpstan": "^5.0.0", "pestphp/pest-plugin-phpstan": "^5.0.0",
"pestphp/pest-plugin-rector": "^5.0.0", "pestphp/pest-plugin-rector": "^5.0.3",
"pestphp/pest-plugin-type-coverage": "^5.0.0", "pestphp/pest-plugin-type-coverage": "^5.0.2",
"psy/psysh": "^0.12.24" "psy/psysh": "^0.12.24"
}, },
"minimum-stability": "dev", "minimum-stability": "dev",
@@ -71,6 +71,7 @@
"config": { "config": {
"sort-packages": true, "sort-packages": true,
"preferred-install": "dist", "preferred-install": "dist",
"process-timeout": 0,
"allow-plugins": { "allow-plugins": {
"pestphp/pest-plugin": true "pestphp/pest-plugin": true
} }
@@ -94,6 +95,7 @@
"test:inline": "php bin/pest --configuration=phpunit.inline.xml", "test:inline": "php bin/pest --configuration=phpunit.inline.xml",
"test:parallel": "php bin/pest --exclude-group=integration --parallel --processes=3", "test:parallel": "php bin/pest --exclude-group=integration --parallel --processes=3",
"test:integration": "php bin/pest --group=integration -v", "test:integration": "php bin/pest --group=integration -v",
"test:tia": "php bin/pest --group=tia -v",
"update:snapshots": "REBUILD_SNAPSHOTS=true php bin/pest --update-snapshots", "update:snapshots": "REBUILD_SNAPSHOTS=true php bin/pest --update-snapshots",
"test": [ "test": [
"@test:lint", "@test:lint",
@@ -1,37 +1,5 @@
<?php <?php
/*
* BSD 3-Clause License
*
* Copyright (c) 2001-2023, Sebastian Bergmann
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
declare(strict_types=1); declare(strict_types=1);
/* /*
+13 -51
View File
@@ -1,37 +1,5 @@
<?php <?php
/*
* BSD 3-Clause License
*
* Copyright (c) 2001-2023, Sebastian Bergmann
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
declare(strict_types=1); declare(strict_types=1);
/* /*
@@ -86,7 +54,7 @@ final class JunitXmlLogger
{ {
private readonly Printer $printer; private readonly Printer $printer;
private readonly Converter $converter; // pest-added private readonly Converter $converter;
private DOMDocument $document; private DOMDocument $document;
@@ -144,8 +112,7 @@ final class JunitXmlLogger
public function __construct(Printer $printer, Facade $facade) public function __construct(Printer $printer, Facade $facade)
{ {
$this->printer = $printer; $this->printer = $printer;
$this->converter = new Converter(Container::getInstance()->get(TestSuite::class)->rootPath); // pest-added $this->converter = new Converter(Container::getInstance()->get(TestSuite::class)->rootPath);
$this->registerSubscribers($facade); $this->registerSubscribers($facade);
$this->createDocument(); $this->createDocument();
} }
@@ -160,10 +127,9 @@ final class JunitXmlLogger
public function testSuiteStarted(Started $event): void public function testSuiteStarted(Started $event): void
{ {
$testSuite = $this->document->createElement('testsuite'); $testSuite = $this->document->createElement('testsuite');
$testSuite->setAttribute('name', $this->converter->getTestSuiteName($event->testSuite())); // pest-changed $testSuite->setAttribute('name', $this->converter->getTestSuiteName($event->testSuite()));
if ($event->testSuite()->isForTestClass()) { if ($event->testSuite()->isForTestClass()) {
$testSuite->setAttribute('file', $this->converter->getTestSuiteLocation($event->testSuite()) ?? ''); // pest-changed $testSuite->setAttribute('file', $this->converter->getTestSuiteLocation($event->testSuite()) ?? '');
} }
if ($this->testSuiteLevel > 0) { if ($this->testSuiteLevel > 0) {
@@ -380,12 +346,11 @@ final class JunitXmlLogger
assert($this->currentTestCase !== null); assert($this->currentTestCase !== null);
$buffer = $this->converter->getTestCaseMethodName($event->test()); // pest-changed $buffer = $this->converter->getTestCaseMethodName($event->test());
$throwable = $event->throwable(); $throwable = $event->throwable();
$buffer .= trim( $buffer .= trim(
$this->converter->getExceptionMessage($throwable).PHP_EOL. // pest-changed $this->converter->getExceptionMessage($throwable).PHP_EOL.
$this->converter->getExceptionDetails($throwable), // pest-changed $this->converter->getExceptionDetails($throwable),
); );
$fault = $this->document->createElement( $fault = $this->document->createElement(
@@ -485,18 +450,15 @@ final class JunitXmlLogger
$testCase = $this->document->createElement('testcase'); $testCase = $this->document->createElement('testcase');
$test = $event->test(); $test = $event->test();
$file = $this->converter->getTestCaseLocation($test); // pest-added $file = $this->converter->getTestCaseLocation($test);
$testCase->setAttribute('name', $this->converter->getTestCaseMethodName($test));
$testCase->setAttribute('name', $this->converter->getTestCaseMethodName($test)); // pest-changed $testCase->setAttribute('file', $file);
$testCase->setAttribute('file', $file); // pest-changed
if ($test->isTestMethod()) { if ($test->isTestMethod()) {
assert($test instanceof TestMethod); assert($test instanceof TestMethod);
// $testCase->setAttribute('line', (string) $test->line()); // pest-removed $className = $this->converter->getTrimmedTestClassName($test);
$className = $this->converter->getTrimmedTestClassName($test); // pest-added $testCase->setAttribute('class', $className);
$testCase->setAttribute('class', $className); // pest-changed $testCase->setAttribute('classname', str_replace('\\', '.', $className));
$testCase->setAttribute('classname', str_replace('\\', '.', $className)); // pest-changed
} }
$this->currentTestCase = $testCase; $this->currentTestCase = $testCase;
@@ -1,29 +1,5 @@
<?php <?php
/*
* The MIT License (MIT)
*
* Copyright (c) 2011 Brian Scaturro
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
declare(strict_types=1); declare(strict_types=1);
namespace ParaTest\WrapperRunner; namespace ParaTest\WrapperRunner;
@@ -34,10 +10,6 @@ use function preg_match;
/** /**
* @internal * @internal
*
* This file is overridden so the "T" progress character — emitted by Pest for
* "todo" tests — is routed to the progress file next to the regular characters,
* instead of being treated as unexpected output.
*/ */
final readonly class ProgressPrinterOutput implements Printer final readonly class ProgressPrinterOutput implements Printer
{ {
@@ -48,7 +20,6 @@ final readonly class ProgressPrinterOutput implements Printer
public function print(string $buffer): void public function print(string $buffer): void
{ {
// Skip anything in \PHPUnit\TextUI\Output\Default\ProgressPrinter\ProgressPrinter::printProgress except $progress
if ( if (
$buffer === "\n" $buffer === "\n"
|| preg_match('/^ +$/', $buffer) === 1 || preg_match('/^ +$/', $buffer) === 1
+1 -38
View File
@@ -1,37 +1,5 @@
<?php <?php
/*
* BSD 3-Clause License
*
* Copyright (c) 2001-2023, Sebastian Bergmann
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
declare(strict_types=1); declare(strict_types=1);
/* /*
@@ -129,9 +97,6 @@ abstract class NameFilterIterator extends RecursiveFilterIterator
$dataSetMaximum = null; $dataSetMaximum = null;
if (@preg_match($filter, '') === false) { if (@preg_match($filter, '') === false) {
// Handles:
// * testAssertEqualsSucceeds#4
// * testAssertEqualsSucceeds#4-8
if (preg_match('/^(.*?)#(\d+)(?:-(\d+))?$/', $filter, $matches)) { if (preg_match('/^(.*?)#(\d+)(?:-(\d+))?$/', $filter, $matches)) {
if (isset($matches[3]) && $matches[2] < $matches[3]) { if (isset($matches[3]) && $matches[2] < $matches[3]) {
$filter = sprintf( $filter = sprintf(
@@ -148,7 +113,7 @@ abstract class NameFilterIterator extends RecursiveFilterIterator
$matches[2], $matches[2],
); );
} }
} // Handles: }
// * testDetermineJsonError@JSON_ERROR_NONE // * testDetermineJsonError@JSON_ERROR_NONE
// * testDetermineJsonError@JSON.* // * testDetermineJsonError@JSON.*
elseif (preg_match('/^(.*?)@(.+)$/', $filter, $matches)) { elseif (preg_match('/^(.*?)@(.+)$/', $filter, $matches)) {
@@ -159,8 +124,6 @@ abstract class NameFilterIterator extends RecursiveFilterIterator
); );
} }
// Escape delimiters in regular expression. Do NOT use preg_quote,
// to keep magic characters.
$filter = sprintf( $filter = sprintf(
'/%s/i', '/%s/i',
str_replace( str_replace(
@@ -1,37 +1,5 @@
<?php <?php
/*
* BSD 3-Clause License
*
* Copyright (c) 2001-2023, Sebastian Bergmann
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
declare(strict_types=1); declare(strict_types=1);
/* /*
@@ -196,9 +164,6 @@ final class DefaultResultCache implements ResultCache
); );
} }
/**
* Returns the cache version.
*/
private function cacheVersion(): string private function cacheVersion(): string
{ {
return 'pest_'.version(); return 'pest_'.version();
+2 -34
View File
@@ -1,37 +1,5 @@
<?php <?php
/*
* BSD 3-Clause License
*
* Copyright (c) 2001-2023, Sebastian Bergmann
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
declare(strict_types=1); declare(strict_types=1);
namespace PHPUnit\Runner; namespace PHPUnit\Runner;
@@ -90,11 +58,11 @@ final class TestSuiteLoader
(static function () use ($suiteClassFile) { (static function () use ($suiteClassFile) {
try { try {
include_once $suiteClassFile; include_once $suiteClassFile;
TestSuite::getInstance()->tests->makeIfNeeded($suiteClassFile);
} catch (Throwable $e) { } catch (Throwable $e) {
Panic::with($e); Panic::with($e);
} }
TestSuite::getInstance()->tests->makeIfNeeded($suiteClassFile);
})(); })();
$loadedClasses = array_values( $loadedClasses = array_values(
-70
View File
@@ -1,37 +1,5 @@
<?php <?php
/*
* BSD 3-Clause License
*
* Copyright (c) 2001-2023, Sebastian Bergmann
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
declare(strict_types=1); declare(strict_types=1);
/* /*
@@ -256,13 +224,6 @@ final class TestSuiteSorter
return $tests; return $tests;
} }
/**
* Comparator callback function to sort tests for "reach failure as fast as possible".
*
* 1. sort tests by defect weight defined in self::DEFECT_SORT_WEIGHT
* 2. when tests are equally defective, sort the fastest to the front
* 3. do not reorder successful tests
*/
private function cmpDefectPriorityAndTime(Test $a, Test $b): int private function cmpDefectPriorityAndTime(Test $a, Test $b): int
{ {
assert($a instanceof Reorderable); assert($a instanceof Reorderable);
@@ -272,7 +233,6 @@ final class TestSuiteSorter
$priorityB = $this->defectSortOrder[$b->sortId()] ?? 0; $priorityB = $this->defectSortOrder[$b->sortId()] ?? 0;
if ($priorityA !== $priorityB) { if ($priorityA !== $priorityB) {
// Sort defect weight descending
return $priorityB <=> $priorityA; return $priorityB <=> $priorityA;
} }
@@ -280,13 +240,9 @@ final class TestSuiteSorter
return $this->cmpDuration($a, $b); return $this->cmpDuration($a, $b);
} }
// do not change execution order
return 0; return 0;
} }
/**
* Compares test duration for sorting tests by duration ascending.
*/
private function cmpDuration(Test $a, Test $b): int private function cmpDuration(Test $a, Test $b): int
{ {
if (! ($a instanceof Reorderable && $b instanceof Reorderable)) { if (! ($a instanceof Reorderable && $b instanceof Reorderable)) {
@@ -296,9 +252,6 @@ final class TestSuiteSorter
return $this->cache->time(ResultCacheId::fromReorderable($a)) <=> $this->cache->time(ResultCacheId::fromReorderable($b)); return $this->cache->time(ResultCacheId::fromReorderable($a)) <=> $this->cache->time(ResultCacheId::fromReorderable($b));
} }
/**
* Compares test size for sorting tests small->medium->large->unknown.
*/
private function cmpSize(Test $a, Test $b): int private function cmpSize(Test $a, Test $b): int
{ {
$sizeA = ($a instanceof TestCase || $a instanceof DataProviderTestSuite) $sizeA = ($a instanceof TestCase || $a instanceof DataProviderTestSuite)
@@ -312,30 +265,11 @@ final class TestSuiteSorter
} }
/** /**
* Reorder Tests within a TestCase in such a way as to resolve as many dependencies as possible.
* The algorithm will leave the tests in original running order when it can.
* For more details see the documentation for test dependencies.
*
* Short description of algorithm:
* 1. Pick the next Test from remaining tests to be checked for dependencies.
* 2. If the test has no dependencies: mark done, start again from the top
* 3. If the test has dependencies but none left to do: mark done, start again from the top
* 4. When we reach the end add any leftover tests to the end. These will be marked 'skipped' during execution.
*
* @param array<TestCase> $tests * @param array<TestCase> $tests
* @return array<TestCase> * @return array<TestCase>
*/ */
private function resolveDependencies(array $tests): array private function resolveDependencies(array $tests): array
{ {
// Pest: Fast-path. If no test in this suite declares dependencies, the
// original O(N^2) algorithm is wasted work — it would splice each test
// one-by-one back into the same order. The check deliberately walks
// TestCase instances directly instead of calling TestSuite::requires(),
// because the latter lazily builds TestSuite::provides() via
// ExecutionOrderDependency::mergeUnique, which is O(N^2) in the total
// number of tests. With thousands of tests that single call alone can
// burn several seconds before the sort even begins. Reading the
// cached TestCase::$dependencies property stays O(N) and costs nothing
// when no test uses `->depends()` / PHPUnit `@depends`. // when no test uses `->depends()` / PHPUnit `@depends`.
if (! $this->anyTestHasDependencies($tests)) { if (! $this->anyTestHasDependencies($tests)) {
return $tests; return $tests;
@@ -361,10 +295,6 @@ final class TestSuiteSorter
/** /**
* Cheaply determines whether any test in the tree declares @depends. * Cheaply determines whether any test in the tree declares @depends.
* *
* Walks `TestSuite` containers recursively and inspects each `TestCase`
* directly so it never triggers `TestSuite::provides()`, which is O(N^2)
* in the total number of aggregated tests.
*
* @param iterable<Test> $tests * @param iterable<Test> $tests
*/ */
private function anyTestHasDependencies(iterable $tests): bool private function anyTestHasDependencies(iterable $tests): bool
@@ -1,37 +1,5 @@
<?php <?php
/*
* BSD 3-Clause License
*
* Copyright (c) 2001-2023, Sebastian Bergmann
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
declare(strict_types=1); declare(strict_types=1);
/* /*
@@ -1,37 +1,5 @@
<?php <?php
/*
* BSD 3-Clause License
*
* Copyright (c) 2001-2023, Sebastian Bergmann
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
declare(strict_types=1); declare(strict_types=1);
/* /*
@@ -51,10 +19,6 @@ use ReflectionClass;
/** /**
* @internal This class is not covered by the backward compatibility promise for PHPUnit * @internal This class is not covered by the backward compatibility promise for PHPUnit
*
* This file is overridden so PHPUnit's progress output emits a "T" before the
* regular "S" for "todo" tests — Pest's parallel result printer consumes the
* "T" and swallows the "S" that follows it.
*/ */
final readonly class TestSkippedSubscriber extends Subscriber implements SkippedSubscriber final readonly class TestSkippedSubscriber extends Subscriber implements SkippedSubscriber
{ {
@@ -67,9 +31,6 @@ final readonly class TestSkippedSubscriber extends Subscriber implements Skipped
$this->printer()->testSkipped(); $this->printer()->testSkipped();
} }
/**
* Prints a "T" to the standard PHPUnit output to indicate a todo item.
*/
private function printTodoItem(): void private function printTodoItem(): void
{ {
$mirror = new ReflectionClass($this->printer()); $mirror = new ReflectionClass($this->printer());
@@ -1,37 +1,5 @@
<?php <?php
/*
* BSD 3-Clause License
*
* Copyright (c) 2001-2023, Sebastian Bergmann
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
declare(strict_types=1); declare(strict_types=1);
/* /*
* This file is part of PHPUnit. * This file is part of PHPUnit.
+2 -1
View File
@@ -18,8 +18,9 @@
<directory suffix=".php">./tests</directory> <directory suffix=".php">./tests</directory>
<directory suffix=".php">./tests-external</directory> <directory suffix=".php">./tests-external</directory>
<exclude>./tests/.snapshots</exclude> <exclude>./tests/.snapshots</exclude>
<exclude>./tests/.tests</exclude>
<exclude>./tests/Fixtures/Inheritance</exclude> <exclude>./tests/Fixtures/Inheritance</exclude>
<exclude>./tests/Fixtures/Suites</exclude>
<exclude>./tests/Fixtures/Tia</exclude>
</testsuite> </testsuite>
</testsuites> </testsuites>
<source> <source>
+8
View File
@@ -0,0 +1,8 @@
{
"exclude": [
"tests/Fixtures/Suites"
],
"rules": {
"Pint/phpdoc_type_annotations_only": true
}
}
+1 -6
View File
@@ -28,6 +28,7 @@ return RectorConfig::configure()
->withSkip([ ->withSkip([
__DIR__.'/src/Plugins/Parallel/Paratest/WrapperRunner.php', __DIR__.'/src/Plugins/Parallel/Paratest/WrapperRunner.php',
__DIR__.'/tests/Fixtures/Arch', __DIR__.'/tests/Fixtures/Arch',
__DIR__.'/tests/Fixtures/Suites',
ReturnNeverTypeRector::class, ReturnNeverTypeRector::class,
ArrowFunctionDelegatingCallToFirstClassCallableRector::class, ArrowFunctionDelegatingCallToFirstClassCallableRector::class,
NarrowObjectReturnTypeRector::class, NarrowObjectReturnTypeRector::class,
@@ -37,13 +38,7 @@ return RectorConfig::configure()
RemoveReturnTagIncompatibleWithNativeTypeRector::class => [ RemoveReturnTagIncompatibleWithNativeTypeRector::class => [
__DIR__.'/src/Expectations/HigherOrderExpectation.php', __DIR__.'/src/Expectations/HigherOrderExpectation.php',
], ],
// Merges unrelated expectations into a single `toMatchArray()`, turning
// `toContain()` into exact matches, dropping `->not`, and mistaking a
// `toBeTrue()` failure message for an expected value. Unsafe here.
UseToMatchArrayRector::class, UseToMatchArrayRector::class,
// Test fixtures rely on "unused" constructors, params and properties
// (resolved via the container or read through reflection), so the
// dead-code and return-type rules below must not touch the test suite.
RemoveEmptyClassMethodRector::class => [ RemoveEmptyClassMethodRector::class => [
__DIR__.'/tests', __DIR__.'/tests',
], ],
-13
View File
@@ -13,15 +13,11 @@ use Pest\Expectation;
abstract class AbstractPreset // @pest-arch-ignore-line abstract class AbstractPreset // @pest-arch-ignore-line
{ {
/** /**
* The expectations.
*
* @var array<int, Expectation<mixed>|ArchExpectation> * @var array<int, Expectation<mixed>|ArchExpectation>
*/ */
protected array $expectations = []; protected array $expectations = [];
/** /**
* Creates a new preset instance.
*
* @param array<int, string> $userNamespaces * @param array<int, string> $userNamespaces
*/ */
public function __construct( public function __construct(
@@ -31,15 +27,11 @@ abstract class AbstractPreset // @pest-arch-ignore-line
} }
/** /**
* Executes the arch preset.
*
* @internal * @internal
*/ */
abstract public function execute(): void; abstract public function execute(): void;
/** /**
* Ignores the given "targets" or "dependencies".
*
* @param array<int, string>|string $targetsOrDependencies * @param array<int, string>|string $targetsOrDependencies
*/ */
final public function ignoring(array|string $targetsOrDependencies): void final public function ignoring(array|string $targetsOrDependencies): void
@@ -51,8 +43,6 @@ abstract class AbstractPreset // @pest-arch-ignore-line
} }
/** /**
* Runs the given callback for each namespace.
*
* @param callable(Expectation<string>): ArchExpectation ...$callbacks * @param callable(Expectation<string>): ArchExpectation ...$callbacks
*/ */
final public function eachUserNamespace(callable ...$callbacks): void final public function eachUserNamespace(callable ...$callbacks): void
@@ -64,9 +54,6 @@ abstract class AbstractPreset // @pest-arch-ignore-line
} }
} }
/**
* Flushes the expectations.
*/
final public function flush(): void final public function flush(): void
{ {
$this->expectations = []; $this->expectations = [];
-8
View File
@@ -14,8 +14,6 @@ use Pest\Expectation;
final class Custom extends AbstractPreset final class Custom extends AbstractPreset
{ {
/** /**
* Creates a new preset instance.
*
* @param array<int, string> $userNamespaces * @param array<int, string> $userNamespaces
* @param Closure(array<int, string>): array<Expectation<mixed>|ArchExpectation> $execute * @param Closure(array<int, string>): array<Expectation<mixed>|ArchExpectation> $execute
*/ */
@@ -27,17 +25,11 @@ final class Custom extends AbstractPreset
parent::__construct($userNamespaces); parent::__construct($userNamespaces);
} }
/**
* Returns the name of the preset.
*/
public function name(): string public function name(): string
{ {
return $this->name; return $this->name;
} }
/**
* Executes the arch preset.
*/
public function execute(): void public function execute(): void
{ {
$this->expectations = ($this->execute)($this->userNamespaces); $this->expectations = ($this->execute)($this->userNamespaces);
-3
View File
@@ -11,9 +11,6 @@ use Throwable;
*/ */
final class Laravel extends AbstractPreset final class Laravel extends AbstractPreset
{ {
/**
* Executes the arch preset.
*/
public function execute(): void public function execute(): void
{ {
$this->expectations[] = expect('App\Traits') $this->expectations[] = expect('App\Traits')
-3
View File
@@ -9,9 +9,6 @@ namespace Pest\ArchPresets;
*/ */
final class Php extends AbstractPreset final class Php extends AbstractPreset
{ {
/**
* Executes the arch preset.
*/
public function execute(): void public function execute(): void
{ {
$this->expectations[] = expect([ $this->expectations[] = expect([
-3
View File
@@ -12,9 +12,6 @@ use Pest\Expectation;
*/ */
final class Relaxed extends AbstractPreset final class Relaxed extends AbstractPreset
{ {
/**
* Executes the arch preset.
*/
public function execute(): void public function execute(): void
{ {
$this->eachUserNamespace( $this->eachUserNamespace(
-3
View File
@@ -9,9 +9,6 @@ namespace Pest\ArchPresets;
*/ */
final class Security extends AbstractPreset final class Security extends AbstractPreset
{ {
/**
* Executes the arch preset.
*/
public function execute(): void public function execute(): void
{ {
$this->expectations[] = expect([ $this->expectations[] = expect([
-3
View File
@@ -12,9 +12,6 @@ use Pest\Expectation;
*/ */
final class Strict extends AbstractPreset final class Strict extends AbstractPreset
{ {
/**
* Executes the arch preset.
*/
public function execute(): void public function execute(): void
{ {
$this->eachUserNamespace( $this->eachUserNamespace(
-5
View File
@@ -13,8 +13,6 @@ use PHPUnit\Util\ExcludeList;
final class BootExcludeList implements Bootstrapper final class BootExcludeList implements Bootstrapper
{ {
/** /**
* The directories to exclude.
*
* @var array<int, non-empty-string> * @var array<int, non-empty-string>
*/ */
private const array EXCLUDE_LIST = [ private const array EXCLUDE_LIST = [
@@ -25,9 +23,6 @@ final class BootExcludeList implements Bootstrapper
'stubs', 'stubs',
]; ];
/**
* Boots the "exclude list" for PHPUnit to ignore Pest files.
*/
public function boot(): void public function boot(): void
{ {
$baseDirectory = dirname(__DIR__, 2); $baseDirectory = dirname(__DIR__, 2);
-8
View File
@@ -21,8 +21,6 @@ use function Pest\testDirectory;
final class BootFiles implements Bootstrapper final class BootFiles implements Bootstrapper
{ {
/** /**
* The structure of the tests directory.
*
* @var array<int, string> * @var array<int, string>
*/ */
private const array STRUCTURE = [ private const array STRUCTURE = [
@@ -33,9 +31,6 @@ final class BootFiles implements Bootstrapper
'Pest.php', 'Pest.php',
]; ];
/**
* Boots the structure of the tests directory.
*/
public function boot(): void public function boot(): void
{ {
$rootPath = TestSuite::getInstance()->rootPath; $rootPath = TestSuite::getInstance()->rootPath;
@@ -67,9 +62,6 @@ final class BootFiles implements Bootstrapper
$this->bootDatasets($testsPath); $this->bootDatasets($testsPath);
} }
/**
* Loads, if possible, the given file.
*/
private function load(string $filename): void private function load(string $filename): void
{ {
if (! Str::endsWith($filename, '.php')) { if (! Str::endsWith($filename, '.php')) {
+1 -7
View File
@@ -14,18 +14,12 @@ use Symfony\Component\Console\Output\OutputInterface;
*/ */
final readonly class BootKernelDump implements Bootstrapper final readonly class BootKernelDump implements Bootstrapper
{ {
/**
* Creates a new Boot Kernel Dump instance.
*/
public function __construct( public function __construct(
private OutputInterface $output, private OutputInterface $output,
) { ) {
// ... //
} }
/**
* Boots the kernel dump.
*/
public function boot(): void public function boot(): void
{ {
Container::getInstance()->add(KernelDump::class, $kernelDump = new KernelDump( Container::getInstance()->add(KernelDump::class, $kernelDump = new KernelDump(
-5
View File
@@ -13,8 +13,6 @@ use Pest\Exceptions\ShouldNotHappen;
final class BootOverrides implements Bootstrapper final class BootOverrides implements Bootstrapper
{ {
/** /**
* The list of files to be overridden.
*
* @var array<int, string> * @var array<int, string>
*/ */
public const array FILES = [ public const array FILES = [
@@ -30,9 +28,6 @@ final class BootOverrides implements Bootstrapper
'Logging/JUnit/JunitXmlLogger.php', 'Logging/JUnit/JunitXmlLogger.php',
]; ];
/**
* Boots the list of files to be overridden.
*/
public function boot(): void public function boot(): void
{ {
foreach (self::FILES as $file) { foreach (self::FILES as $file) {
+6 -8
View File
@@ -16,8 +16,6 @@ use PHPUnit\Event\Subscriber;
final readonly class BootSubscribers implements Bootstrapper final readonly class BootSubscribers implements Bootstrapper
{ {
/** /**
* The list of Subscribers.
*
* @var array<int, class-string<Subscriber>> * @var array<int, class-string<Subscriber>>
*/ */
private const array SUBSCRIBERS = [ private const array SUBSCRIBERS = [
@@ -35,19 +33,19 @@ final readonly class BootSubscribers implements Bootstrapper
Subscribers\EnsureTiaResultIsRecordedOnSkipped::class, Subscribers\EnsureTiaResultIsRecordedOnSkipped::class,
Subscribers\EnsureTiaResultIsRecordedOnIncomplete::class, Subscribers\EnsureTiaResultIsRecordedOnIncomplete::class,
Subscribers\EnsureTiaResultIsRecordedOnRisky::class, Subscribers\EnsureTiaResultIsRecordedOnRisky::class,
Subscribers\EnsureTiaResultIsRecordedOnNoticeTriggered::class,
Subscribers\EnsureTiaResultIsRecordedOnPhpNoticeTriggered::class,
Subscribers\EnsureTiaResultIsRecordedOnDeprecationTriggered::class,
Subscribers\EnsureTiaResultIsRecordedOnPhpDeprecationTriggered::class,
Subscribers\EnsureTiaResultIsRecordedOnWarningTriggered::class,
Subscribers\EnsureTiaResultIsRecordedOnPhpWarningTriggered::class,
Subscribers\EnsureTiaAssertionsAreRecordedOnFinished::class, Subscribers\EnsureTiaAssertionsAreRecordedOnFinished::class,
]; ];
/**
* Creates a new instance of the Boot Subscribers.
*/
public function __construct( public function __construct(
private Container $container, private Container $container,
) {} ) {}
/**
* Boots the list of Subscribers.
*/
public function boot(): void public function boot(): void
{ {
foreach (self::SUBSCRIBERS as $subscriber) { foreach (self::SUBSCRIBERS as $subscriber) {
+1 -7
View File
@@ -13,18 +13,12 @@ use Symfony\Component\Console\Output\OutputInterface;
*/ */
final readonly class BootView implements Bootstrapper final readonly class BootView implements Bootstrapper
{ {
/**
* Creates a new instance of the Boot View.
*/
public function __construct( public function __construct(
private OutputInterface $output private OutputInterface $output
) { ) {
// .. //
} }
/**
* Boots the view renderer.
*/
public function boot(): void public function boot(): void
{ {
View::renderUsing($this->output); View::renderUsing($this->output);
-12
View File
@@ -16,22 +16,13 @@ use function Termwind\renderUsing;
*/ */
final class Events final class Events
{ {
/**
* Sets the output.
*/
private static ?OutputInterface $output = null; private static ?OutputInterface $output = null;
/**
* Sets the output.
*/
public static function setOutput(OutputInterface $output): void public static function setOutput(OutputInterface $output): void
{ {
self::$output = $output; self::$output = $output;
} }
/**
* Fires before the test method description is printed.
*/
public static function beforeTestMethodDescription(TestResult $result, string $description): string public static function beforeTestMethodDescription(TestResult $result, string $description): string
{ {
if (($context = $result->context) === []) { if (($context = $result->context) === []) {
@@ -73,9 +64,6 @@ final class Events
return $description; return $description;
} }
/**
* Fires after the test method description is printed.
*/
public static function afterTestMethodDescription(TestResult $result): void public static function afterTestMethodDescription(TestResult $result): void
{ {
if (($context = $result->context) === []) { if (($context = $result->context) === []) {
-2
View File
@@ -14,8 +14,6 @@ trait Expectable
/** /**
* @template TValue * @template TValue
* *
* Creates a new Expectation.
*
* @param TValue $value * @param TValue $value
* @return Expectation<TValue> * @return Expectation<TValue>
*/ */
-7
View File
@@ -14,15 +14,11 @@ use Closure;
trait Extendable trait Extendable
{ {
/** /**
* The list of extends.
*
* @var array<string, Closure> * @var array<string, Closure>
*/ */
private static array $extends = []; private static array $extends = [];
/** /**
* Register a new extend.
*
* @param-closure-this T $extend * @param-closure-this T $extend
*/ */
public function extend(string $name, Closure $extend): void public function extend(string $name, Closure $extend): void
@@ -30,9 +26,6 @@ trait Extendable
static::$extends[$name] = $extend; static::$extends[$name] = $extend;
} }
/**
* Checks if given extend name is registered.
*/
public static function hasExtend(string $name): bool public static function hasExtend(string $name): bool
{ {
return array_key_exists($name, static::$extends); return array_key_exists($name, static::$extends);
-12
View File
@@ -9,33 +9,21 @@ namespace Pest\Concerns\Logging;
*/ */
trait WritesToConsole trait WritesToConsole
{ {
/**
* Writes the given success message to the console.
*/
private function writeSuccess(string $message): void private function writeSuccess(string $message): void
{ {
$this->writePestTestOutput($message, 'fg-green, bold', '✓'); $this->writePestTestOutput($message, 'fg-green, bold', '✓');
} }
/**
* Writes the given error message to the console.
*/
private function writeError(string $message): void private function writeError(string $message): void
{ {
$this->writePestTestOutput($message, 'fg-red, bold', ''); $this->writePestTestOutput($message, 'fg-red, bold', '');
} }
/**
* Writes the given warning message to the console.
*/
private function writeWarning(string $message): void private function writeWarning(string $message): void
{ {
$this->writePestTestOutput($message, 'fg-yellow, bold', '-'); $this->writePestTestOutput($message, 'fg-yellow, bold', '-');
} }
/**
* Writes the give message to the console.
*/
private function writePestTestOutput(string $message, string $color, string $symbol): void private function writePestTestOutput(string $message, string $color, string $symbol): void
{ {
$this->writeWithColor($color, "$symbol ", false); $this->writeWithColor($color, "$symbol ", false);
-11
View File
@@ -12,30 +12,21 @@ use Closure;
trait Pipeable trait Pipeable
{ {
/** /**
* The list of pipes.
*
* @var array<string, array<Closure(Closure, mixed ...$arguments): void>> * @var array<string, array<Closure(Closure, mixed ...$arguments): void>>
*/ */
private static array $pipes = []; private static array $pipes = [];
/** /**
* The list of interceptors.
*
* @var array<string, array<Closure(Closure, mixed ...$arguments): void>> * @var array<string, array<Closure(Closure, mixed ...$arguments): void>>
*/ */
private static array $interceptors = []; private static array $interceptors = [];
/**
* Register a pipe to be applied before an expectation is checked.
*/
public function pipe(string $name, Closure $pipe): void public function pipe(string $name, Closure $pipe): void
{ {
self::$pipes[$name][] = $pipe; self::$pipes[$name][] = $pipe;
} }
/** /**
* Register an interceptor that should replace an existing expectation.
*
* @param string|Closure(mixed $value, mixed ...$arguments):bool $filter * @param string|Closure(mixed $value, mixed ...$arguments):bool $filter
*/ */
public function intercept(string $name, string|Closure $filter, Closure $handler): void public function intercept(string $name, string|Closure $filter, Closure $handler): void
@@ -60,8 +51,6 @@ trait Pipeable
} }
/** /**
* Get the list of pipes by the given name.
*
* @return array<int, Closure> * @return array<int, Closure>
*/ */
private function pipes(string $name, object $context, string $scope): array private function pipes(string $name, object $context, string $scope): array
-2
View File
@@ -11,8 +11,6 @@ trait Retrievable
{ {
/** /**
* @template TRetrievableValue * @template TRetrievableValue
*
* Safely retrieve the value at the given key from an object or array.
* @template TRetrievableValue * @template TRetrievableValue
* *
* @param array<string, TRetrievableValue>|object $value * @param array<string, TRetrievableValue>|object $value
+4 -116
View File
@@ -37,105 +37,53 @@ use Throwable;
*/ */
trait Testable trait Testable
{ {
/**
* The test's description.
*/
private string $__description; private string $__description;
/**
* The test's latest description.
*/
private static string $__latestDescription; private static string $__latestDescription;
/**
* The test's assignees.
*/
private static array $__latestAssignees = []; private static array $__latestAssignees = [];
/**
* The test's notes.
*/
private static array $__latestNotes = []; private static array $__latestNotes = [];
/** /**
* The test's issues.
*
* @var array<int, int> * @var array<int, int>
*/ */
private static array $__latestIssues = []; private static array $__latestIssues = [];
/** /**
* The test's PRs.
*
* @var array<int, int> * @var array<int, int>
*/ */
private static array $__latestPrs = []; private static array $__latestPrs = [];
/** /**
* The test's describing, if any.
*
* @var array<int, string> * @var array<int, string>
*/ */
public array $__describing = []; public array $__describing = [];
/**
* Whether the test has ran or not.
*/
public bool $__ran = false; public bool $__ran = false;
/**
* The active replay mode for this test, set in `setUp()` and checked
* in `__runTest()` / `tearDown()` to skip the body and after-each.
*/
private ReplayType $__replay = ReplayType::None; private ReplayType $__replay = ReplayType::None;
/**
* The cached assertion count to replay, captured when entering replay mode.
*/
private int $__replayAssertions = 0; private int $__replayAssertions = 0;
/**
* The test's test closure.
*/
private Closure $__test; private Closure $__test;
/**
* The test's before each closure.
*/
private ?Closure $__beforeEach = null; private ?Closure $__beforeEach = null;
/**
* The test's after each closure.
*/
private ?Closure $__afterEach = null; private ?Closure $__afterEach = null;
/**
* The test's before all closure.
*/
private static ?Closure $__beforeAll = null; private static ?Closure $__beforeAll = null;
/**
* The test's after all closure.
*/
private static ?Closure $__afterAll = null; private static ?Closure $__afterAll = null;
/**
* The list of snapshot changes, if any.
*/
private array $__snapshotChanges = []; private array $__snapshotChanges = [];
/**
* Resets the test case static properties.
*/
public static function flush(): void public static function flush(): void
{ {
self::$__beforeAll = null; self::$__beforeAll = null;
self::$__afterAll = null; self::$__afterAll = null;
} }
/**
* Adds a new "note" to the Test Case.
*/
public function note(array|string $note): self public function note(array|string $note): self
{ {
$note = is_array($note) ? $note : [$note]; $note = is_array($note) ? $note : [$note];
@@ -145,9 +93,6 @@ trait Testable
return $this; return $this;
} }
/**
* Adds a new "setUpBeforeClass" to the Test Case.
*/
public function __addBeforeAll(?Closure $hook): void public function __addBeforeAll(?Closure $hook): void
{ {
if (! $hook instanceof Closure) { if (! $hook instanceof Closure) {
@@ -159,9 +104,6 @@ trait Testable
: $hook; : $hook;
} }
/**
* Adds a new "tearDownAfterClass" to the Test Case.
*/
public function __addAfterAll(?Closure $hook): void public function __addAfterAll(?Closure $hook): void
{ {
if (! $hook instanceof Closure) { if (! $hook instanceof Closure) {
@@ -173,25 +115,16 @@ trait Testable
: $hook; : $hook;
} }
/**
* Adds a new "setUp" to the Test Case.
*/
public function __addBeforeEach(?Closure $hook): void public function __addBeforeEach(?Closure $hook): void
{ {
$this->__addHook('__beforeEach', $hook); $this->__addHook('__beforeEach', $hook);
} }
/**
* Adds a new "tearDown" to the Test Case.
*/
public function __addAfterEach(?Closure $hook): void public function __addAfterEach(?Closure $hook): void
{ {
$this->__addHook('__afterEach', $hook); $this->__addHook('__afterEach', $hook);
} }
/**
* Adds a new "hook" to the Test Case.
*/
private function __addHook(string $property, ?Closure $hook): void private function __addHook(string $property, ?Closure $hook): void
{ {
if (! $hook instanceof Closure) { if (! $hook instanceof Closure) {
@@ -203,9 +136,6 @@ trait Testable
: $hook; : $hook;
} }
/**
* This method is called before the first test of this Test Case is run.
*/
public static function setUpBeforeClass(): void public static function setUpBeforeClass(): void
{ {
parent::setUpBeforeClass(); parent::setUpBeforeClass();
@@ -223,9 +153,6 @@ trait Testable
} }
} }
/**
* This method is called after the last test of this Test Case is run.
*/
public static function tearDownAfterClass(): void public static function tearDownAfterClass(): void
{ {
$afterAll = TestSuite::getInstance()->afterAll->get(self::$__filename); $afterAll = TestSuite::getInstance()->afterAll->get(self::$__filename);
@@ -239,9 +166,6 @@ trait Testable
parent::tearDownAfterClass(); parent::tearDownAfterClass();
} }
/**
* Gets executed before the Test Case.
*/
protected function setUp(...$arguments): void protected function setUp(...$arguments): void
{ {
TestSuite::getInstance()->test = $this; TestSuite::getInstance()->test = $this;
@@ -280,12 +204,14 @@ trait Testable
/** @var Tia $tia */ /** @var Tia $tia */
$tia = Container::getInstance()->get(Tia::class); $tia = Container::getInstance()->get(Tia::class);
$status = $tia->getStatus(self::$__filename, $this::class.'::'.$this->name()); $status = $tia->getStatus(self::$__filename, $this->valueObjectForEvents()->id());
$replay = ReplayType::fromStatus($status); $replay = ReplayType::fromStatus($status);
if ($replay !== ReplayType::None) { if ($replay !== ReplayType::None) {
assert($status !== null); assert($status !== null);
$this->__replay = $replay;
match ($replay) { match ($replay) {
ReplayType::Pass, ReplayType::Risky => $this->__beginReplay($replay, $tia), ReplayType::Pass, ReplayType::Risky => $this->__beginReplay($replay, $tia),
ReplayType::Skipped => $this->markTestSkipped($status->message()), ReplayType::Skipped => $this->markTestSkipped($status->message()),
@@ -319,16 +245,12 @@ trait Testable
private function __beginReplay(ReplayType $replay, Tia $tia): void private function __beginReplay(ReplayType $replay, Tia $tia): void
{ {
$this->__replay = $replay; $this->__replay = $replay;
$this->__replayAssertions = $tia->getAssertionCount($this::class.'::'.$this->name()); $this->__replayAssertions = $tia->getAssertionCount($this->valueObjectForEvents()->id());
$this->__ran = true; $this->__ran = true;
} }
/**
* Initialize test case properties from TestSuite.
*/
public function __initializeTestCase(): void public function __initializeTestCase(): void
{ {
// Return if the test case has already been initialized
if (isset($this->__test)) { if (isset($this->__test)) {
return; return;
} }
@@ -350,9 +272,6 @@ trait Testable
} }
} }
/**
* Gets executed after the Test Case.
*/
protected function tearDown(...$arguments): void protected function tearDown(...$arguments): void
{ {
if ($this->__replay !== ReplayType::None) { if ($this->__replay !== ReplayType::None) {
@@ -380,8 +299,6 @@ trait Testable
} }
/** /**
* Executes the Test Case current test.
*
* @throws Throwable * @throws Throwable
*/ */
private function __runTest(Closure $closure, ...$args): mixed private function __runTest(Closure $closure, ...$args): mixed
@@ -452,9 +369,6 @@ trait Testable
throw $lastException; throw $lastException;
} }
/**
* Determines if the given exception matches PHPUnit's expected exception.
*/
private function __isExpectedException(Throwable $e): bool private function __isExpectedException(Throwable $e): bool
{ {
$expectation = Closure::bind(fn () => $this->exceptionExpectation, $this, TestCase::class)(); $expectation = Closure::bind(fn () => $this->exceptionExpectation, $this, TestCase::class)();
@@ -489,8 +403,6 @@ trait Testable
} }
/** /**
* Resolve the passed arguments. Any Closures will be bound to the testcase and resolved.
*
* @throws Throwable * @throws Throwable
*/ */
private function __resolveTestArguments(array $arguments): array private function __resolveTestArguments(array $arguments): array
@@ -498,10 +410,6 @@ trait Testable
$method = TestSuite::getInstance()->tests->get(self::$__filename)->getMethod($this->name()); $method = TestSuite::getInstance()->tests->get(self::$__filename)->getMethod($this->name());
if ($method->repetitions > 1) { if ($method->repetitions > 1) {
// If the test is repeated, the first argument is the iteration number
// we need to move it to the end of the arguments list
// so that the datasets are the first n arguments
// and the iteration number is the last argument
$firstArgument = array_shift($arguments); $firstArgument = array_shift($arguments);
$arguments[] = $firstArgument; $arguments[] = $firstArgument;
} }
@@ -550,8 +458,6 @@ trait Testable
} }
/** /**
* Ensures dataset items count matches underlying test case required parameters
*
* @throws ReflectionException * @throws ReflectionException
* @throws DatasetArgumentsMismatch * @throws DatasetArgumentsMismatch
*/ */
@@ -591,9 +497,6 @@ trait Testable
return ExceptionTrace::ensure(fn (): mixed => call_user_func_array(Closure::bind($closure, $this, $this::class), $arguments)); return ExceptionTrace::ensure(fn (): mixed => call_user_func_array(Closure::bind($closure, $this, $this::class), $arguments));
} }
/**
* Uses the given preset on the test.
*/
public function preset(): Preset public function preset(): Preset
{ {
return new Preset; return new Preset;
@@ -609,33 +512,21 @@ trait Testable
$this->markTestIncomplete(implode('. ', $this->__snapshotChanges)); $this->markTestIncomplete(implode('. ', $this->__snapshotChanges));
} }
/**
* The printable test case name.
*/
public static function getPrintableTestCaseName(): string public static function getPrintableTestCaseName(): string
{ {
return preg_replace('/P\\\/', '', self::class, 1); return preg_replace('/P\\\/', '', self::class, 1);
} }
/**
* The printable test case method name.
*/
public function getPrintableTestCaseMethodName(): string public function getPrintableTestCaseMethodName(): string
{ {
return $this->__description; return $this->__description;
} }
/**
* The latest printable test case method name.
*/
public static function getLatestPrintableTestCaseMethodName(): string public static function getLatestPrintableTestCaseMethodName(): string
{ {
return self::$__latestDescription ?? ''; return self::$__latestDescription ?? '';
} }
/**
* The printable test case method context.
*/
public static function getPrintableContext(): array public static function getPrintableContext(): array
{ {
return [ return [
@@ -646,9 +537,6 @@ trait Testable
]; ];
} }
/**
* Opens a shell for the test case.
*/
public function shell(): void public function shell(): void
{ {
Shell::open(); Shell::open();
-47
View File
@@ -14,31 +14,19 @@ use Pest\PendingCalls\UsesCall;
*/ */
final readonly class Configuration final readonly class Configuration
{ {
/**
* The filename of the configuration.
*/
private string $filename; private string $filename;
/**
* Creates a new configuration instance.
*/
public function __construct( public function __construct(
string $filename, string $filename,
) { ) {
$this->filename = str_ends_with($filename, DIRECTORY_SEPARATOR.'Pest.php') ? dirname($filename) : $filename; $this->filename = str_ends_with($filename, DIRECTORY_SEPARATOR.'Pest.php') ? dirname($filename) : $filename;
} }
/**
* Use the given classes and traits in the given targets.
*/
public function in(string ...$targets): UsesCall public function in(string ...$targets): UsesCall
{ {
return new UsesCall($this->filename, [])->in(...$targets); return new UsesCall($this->filename, [])->in(...$targets);
} }
/**
* Depending on where is called, it will extend the given classes and traits globally or locally.
*/
public function extend(string ...$classAndTraits): UsesCall public function extend(string ...$classAndTraits): UsesCall
{ {
return new UsesCall( return new UsesCall(
@@ -47,97 +35,62 @@ final readonly class Configuration
); );
} }
/**
* Depending on where is called, it will extend the given classes and traits globally or locally.
*/
public function extends(string ...$classAndTraits): UsesCall public function extends(string ...$classAndTraits): UsesCall
{ {
return $this->extend(...$classAndTraits); return $this->extend(...$classAndTraits);
} }
/**
* Depending on where is called, it will add the given groups globally or locally.
*/
public function group(string ...$groups): UsesCall public function group(string ...$groups): UsesCall
{ {
return new UsesCall($this->filename, [])->group(...$groups); return new UsesCall($this->filename, [])->group(...$groups);
} }
/**
* Marks all tests in the current file to be run exclusively.
*/
public function only(): void public function only(): void
{ {
new BeforeEachCall(TestSuite::getInstance(), $this->filename)->only(); new BeforeEachCall(TestSuite::getInstance(), $this->filename)->only();
} }
/**
* Depending on where is called, it will extend the given classes and traits globally or locally.
*/
public function use(string ...$classAndTraits): UsesCall public function use(string ...$classAndTraits): UsesCall
{ {
return $this->extend(...$classAndTraits); return $this->extend(...$classAndTraits);
} }
/**
* Depending on where is called, it will extend the given classes and traits globally or locally.
*/
public function uses(string ...$classAndTraits): UsesCall public function uses(string ...$classAndTraits): UsesCall
{ {
return $this->extends(...$classAndTraits); return $this->extends(...$classAndTraits);
} }
/**
* Gets the printer configuration.
*/
public function printer(): Configuration\Printer public function printer(): Configuration\Printer
{ {
return new Configuration\Printer; return new Configuration\Printer;
} }
/**
* Gets the presets configuration.
*/
public function presets(): Configuration\Presets public function presets(): Configuration\Presets
{ {
return new Configuration\Presets; return new Configuration\Presets;
} }
/**
* Gets the project configuration.
*/
public function project(): Configuration\Project public function project(): Configuration\Project
{ {
return Configuration\Project::getInstance(); return Configuration\Project::getInstance();
} }
/**
* Gets the browser configuration.
*/
public function browser(): Browser\Configuration public function browser(): Browser\Configuration
{ {
return new Browser\Configuration; return new Browser\Configuration;
} }
/**
* Gets the evals configuration.
*/
public function evals(): Evals\Configuration // @phpstan-ignore-line public function evals(): Evals\Configuration // @phpstan-ignore-line
{ {
return new Evals\Configuration; // @phpstan-ignore-line return new Evals\Configuration; // @phpstan-ignore-line
} }
/**
* Gets the TIA (Test Impact Analysis) configuration.
*/
public function tia(): Plugins\Tia\Configuration public function tia(): Plugins\Tia\Configuration
{ {
return new Plugins\Tia\Configuration; return new Plugins\Tia\Configuration;
} }
/** /**
* Proxies calls to the uses method.
*
* @param array<array-key, mixed> $arguments * @param array<array-key, mixed> $arguments
*/ */
public function __call(string $name, array $arguments): mixed public function __call(string $name, array $arguments): mixed
-3
View File
@@ -9,9 +9,6 @@ use Pest\Preset;
final class Presets final class Presets
{ {
/**
* Creates a custom preset instance, and adds it to the list of presets.
*/
public function custom(string $name, Closure $execute): void public function custom(string $name, Closure $execute): void
{ {
Preset::custom($name, $execute); Preset::custom($name, $execute);
-3
View File
@@ -11,9 +11,6 @@ use NunoMaduro\Collision\Adapters\Phpunit\Printers\DefaultPrinter;
*/ */
final readonly class Printer final readonly class Printer
{ {
/**
* Sets the theme to compact.
*/
public function compact(): self public function compact(): self
{ {
DefaultPrinter::compact(true); DefaultPrinter::compact(true);
-28
View File
@@ -10,42 +10,27 @@ namespace Pest\Configuration;
final class Project final class Project
{ {
/** /**
* The assignees link.
*
* @internal * @internal
*/ */
public string $assignees = ''; public string $assignees = '';
/** /**
* The issues link.
*
* @internal * @internal
*/ */
public string $issues = ''; public string $issues = '';
/** /**
* The PRs link.
*
* @internal * @internal
*/ */
public string $prs = ''; public string $prs = '';
/**
* The singleton instance.
*/
private static ?self $instance = null; private static ?self $instance = null;
/**
* Creates a new instance of the project.
*/
public static function getInstance(): self public static function getInstance(): self
{ {
return self::$instance ??= new self; return self::$instance ??= new self;
} }
/**
* Sets the test project to GitHub.
*/
public function github(string $project): self public function github(string $project): self
{ {
$this->issues = "https://github.com/{$project}/issues/%s"; $this->issues = "https://github.com/{$project}/issues/%s";
@@ -56,12 +41,8 @@ final class Project
return $this; return $this;
} }
/**
* Sets the test project to GitLab.
*/
public function gitlab(string $project, string $hostname = 'gitlab.com'): self public function gitlab(string $project, string $hostname = 'gitlab.com'): self
{ {
// Simple way to ensure only the host is used
$hostname = parse_url($hostname, PHP_URL_HOST) ?? $hostname; $hostname = parse_url($hostname, PHP_URL_HOST) ?? $hostname;
$this->issues = "https://{$hostname}/{$project}/-/work_items/%s"; $this->issues = "https://{$hostname}/{$project}/-/work_items/%s";
@@ -72,9 +53,6 @@ final class Project
return $this; return $this;
} }
/**
* Sets the test project to Bitbucket.
*/
public function bitbucket(string $project): self public function bitbucket(string $project): self
{ {
$this->issues = "https://bitbucket.org/{$project}/issues/%s"; $this->issues = "https://bitbucket.org/{$project}/issues/%s";
@@ -85,9 +63,6 @@ final class Project
return $this; return $this;
} }
/**
* Sets the test project to Jira.
*/
public function jira(string $namespace, string $project): self public function jira(string $namespace, string $project): self
{ {
$this->issues = "https://{$namespace}.atlassian.net/browse/{$project}-%s"; $this->issues = "https://{$namespace}.atlassian.net/browse/{$project}-%s";
@@ -97,9 +72,6 @@ final class Project
return $this; return $this;
} }
/**
* Sets the test project to custom.
*/
public function custom(string $issues, string $prs, string $assignees): self public function custom(string $issues, string $prs, string $assignees): self
{ {
$this->issues = $issues; $this->issues = $issues;
+1 -9
View File
@@ -12,8 +12,6 @@ use Symfony\Component\Console\Output\OutputInterface;
final readonly class Help final readonly class Help
{ {
/** /**
* The Command messages.
*
* @var array<int, string> * @var array<int, string>
*/ */
private const array HELP_MESSAGES = [ private const array HELP_MESSAGES = [
@@ -24,17 +22,11 @@ final readonly class Help
' <info>--group=<fg=cyan><name></></info> Only runs tests from the specified group(s)', ' <info>--group=<fg=cyan><name></></info> Only runs tests from the specified group(s)',
]; ];
/**
* Creates a new Console Command instance.
*/
public function __construct(private OutputInterface $output) public function __construct(private OutputInterface $output)
{ {
// .. //
} }
/**
* Executes the Console Command.
*/
public function __invoke(): void public function __invoke(): void
{ {
foreach (self::HELP_MESSAGES as $message) { foreach (self::HELP_MESSAGES as $message) {
+1 -9
View File
@@ -18,8 +18,6 @@ use Symfony\Component\Console\Question\ConfirmationQuestion;
final readonly class Thanks final readonly class Thanks
{ {
/** /**
* The support options.
*
* @var array<string, string> * @var array<string, string>
*/ */
private const array FUNDING_MESSAGES = [ private const array FUNDING_MESSAGES = [
@@ -33,19 +31,13 @@ final readonly class Thanks
'Sponsor' => 'https://github.com/sponsors/nunomaduro', 'Sponsor' => 'https://github.com/sponsors/nunomaduro',
]; ];
/**
* Creates a new Console Command instance.
*/
public function __construct( public function __construct(
private InputInterface $input, private InputInterface $input,
private OutputInterface $output private OutputInterface $output
) { ) {
// .. //
} }
/**
* Executes the Console Command.
*/
public function __invoke(): void public function __invoke(): void
{ {
$bootstrapper = new BootView($this->output); $bootstrapper = new BootView($this->output);
-3
View File
@@ -9,8 +9,5 @@ namespace Pest\Contracts;
*/ */
interface Bootstrapper interface Bootstrapper
{ {
/**
* Boots the bootstrapper.
*/
public function boot(): void; public function boot(): void;
} }
+1 -1
View File
@@ -11,5 +11,5 @@ use NunoMaduro\Collision\Contracts\Adapters\Phpunit\HasPrintableTestCaseName as
*/ */
interface HasPrintableTestCaseName extends BaseHasPrintableTestCaseName interface HasPrintableTestCaseName extends BaseHasPrintableTestCaseName
{ {
// .. //
} }
-6
View File
@@ -11,13 +11,7 @@ use Symfony\Component\Console\Output\OutputInterface;
*/ */
interface Panicable interface Panicable
{ {
/**
* Renders the panic on the given output.
*/
public function render(OutputInterface $output): void; public function render(OutputInterface $output): void;
/**
* The exit code to be used.
*/
public function exitCode(): int; public function exitCode(): int;
} }
-3
View File
@@ -9,8 +9,5 @@ namespace Pest\Contracts\Plugins;
*/ */
interface AddsOutput interface AddsOutput
{ {
/**
* Adds output after the Test Suite execution.
*/
public function addOutput(int $exitCode): int; public function addOutput(int $exitCode): int;
} }
-3
View File
@@ -9,8 +9,5 @@ namespace Pest\Contracts\Plugins;
*/ */
interface Bootable interface Bootable
{ {
/**
* Boots the plugin.
*/
public function boot(): void; public function boot(): void;
} }
@@ -10,8 +10,6 @@ namespace Pest\Contracts\Plugins;
interface HandlesArguments interface HandlesArguments
{ {
/** /**
* Adds arguments before the Test Suite execution.
*
* @param array<int, string> $arguments * @param array<int, string> $arguments
* @return array<int, string> * @return array<int, string>
*/ */
@@ -10,8 +10,6 @@ namespace Pest\Contracts\Plugins;
interface HandlesOriginalArguments interface HandlesOriginalArguments
{ {
/** /**
* Adds original arguments before the Test Suite execution.
*
* @param array<int, string> $arguments * @param array<int, string> $arguments
*/ */
public function handleOriginalArguments(array $arguments): void; public function handleOriginalArguments(array $arguments): void;
-3
View File
@@ -9,8 +9,5 @@ namespace Pest\Contracts\Plugins;
*/ */
interface Terminable interface Terminable
{ {
/**
* Terminates the plugin.
*/
public function terminate(): void; public function terminate(): void;
} }
-3
View File
@@ -6,8 +6,5 @@ namespace Pest\Contracts;
interface TestCaseFilter interface TestCaseFilter
{ {
/**
* Whether the test case is accepted.
*/
public function accept(string $testCaseFilename): bool; public function accept(string $testCaseFilename): bool;
} }
-3
View File
@@ -8,8 +8,5 @@ use Pest\Factories\TestCaseMethodFactory;
interface TestCaseMethodFilter interface TestCaseMethodFilter
{ {
/**
* Whether the test case method is accepted.
*/
public function accept(TestCaseMethodFactory $factory): bool; public function accept(TestCaseMethodFactory $factory): bool;
} }
-2
View File
@@ -12,8 +12,6 @@ use Pest\Factories\Attribute;
final class Attributes final class Attributes
{ {
/** /**
* Evaluates the given attributes and returns the code.
*
* @param iterable<int, Attribute> $attributes * @param iterable<int, Attribute> $attributes
*/ */
public static function code(iterable $attributes): string public static function code(iterable $attributes): string
+1 -4
View File
@@ -14,11 +14,8 @@ use Symfony\Component\Console\Exception\ExceptionInterface;
*/ */
final class AfterAllAlreadyExist extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace final class AfterAllAlreadyExist extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
{ {
/**
* Creates a new Exception instance.
*/
public function __construct(string $filename) public function __construct(string $filename)
{ {
parent::__construct(sprintf('The afterAll already exists in the filename `%s`.', $filename)); parent::__construct(sprintf('The [afterAll] hook is already defined in [%s]. Each test file may only define it once.', $filename));
} }
} }
+1 -4
View File
@@ -14,11 +14,8 @@ use Symfony\Component\Console\Exception\ExceptionInterface;
*/ */
final class AfterAllWithinDescribe extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace final class AfterAllWithinDescribe extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
{ {
/**
* Creates a new Exception instance.
*/
public function __construct(string $filename) public function __construct(string $filename)
{ {
parent::__construct(sprintf('The afterAll method can not be used within describe functions. Filename `%s`.', $filename)); parent::__construct(sprintf('The [afterAll] hook may not be used inside a [describe] block. Please move it to the top level of [%s].', $filename));
} }
} }
+1 -4
View File
@@ -14,11 +14,8 @@ use Symfony\Component\Console\Exception\ExceptionInterface;
*/ */
final class AfterBeforeTestFunction extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace final class AfterBeforeTestFunction extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
{ {
/**
* Creates a new Exception instance.
*/
public function __construct(string $filename) public function __construct(string $filename)
{ {
parent::__construct('After method cannot be used with before the [test|it] functions in the filename `['.$filename.']`.'); parent::__construct(sprintf('The [after] hook may only be chained onto [beforeEach] inside a [describe] block. Please move it inside one in [%s].', $filename));
} }
} }
+1 -4
View File
@@ -14,11 +14,8 @@ use Symfony\Component\Console\Exception\ExceptionInterface;
*/ */
final class BeforeAllAlreadyExist extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace final class BeforeAllAlreadyExist extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
{ {
/**
* Creates a new Exception instance.
*/
public function __construct(string $filename) public function __construct(string $filename)
{ {
parent::__construct(sprintf('The beforeAll already exists in the filename `%s`.', $filename)); parent::__construct(sprintf('The [beforeAll] hook is already defined in [%s]. Each test file may only define it once.', $filename));
} }
} }
+1 -4
View File
@@ -14,11 +14,8 @@ use Symfony\Component\Console\Exception\ExceptionInterface;
*/ */
final class BeforeAllWithinDescribe extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace final class BeforeAllWithinDescribe extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
{ {
/**
* Creates a new Exception instance.
*/
public function __construct(string $filename) public function __construct(string $filename)
{ {
parent::__construct(sprintf('The beforeAll method can not be used within describe functions. Filename `%s`.', $filename)); parent::__construct(sprintf('The [beforeAll] hook may not be used inside a [describe] block. Please move it to the top level of [%s].', $filename));
} }
} }
+1 -4
View File
@@ -14,11 +14,8 @@ use Symfony\Component\Console\Exception\ExceptionInterface;
*/ */
final class DatasetAlreadyExists extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace final class DatasetAlreadyExists extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
{ {
/**
* Creates a new Exception instance.
*/
public function __construct(string $name, string $scope) public function __construct(string $name, string $scope)
{ {
parent::__construct(sprintf('A dataset with the name `%s` already exists in scope [%s].', $name, $scope)); parent::__construct(sprintf('A dataset named [%s] is already registered in [%s]. Please choose a different name.', $name, $scope));
} }
} }
+2 -4
View File
@@ -11,11 +11,9 @@ final class DatasetArgumentsMismatch extends Exception
public function __construct(int $requiredCount, int $suppliedCount) public function __construct(int $requiredCount, int $suppliedCount)
{ {
if ($requiredCount <= $suppliedCount) { if ($requiredCount <= $suppliedCount) {
parent::__construct('Test argument names and dataset keys do not match'); parent::__construct('The test arguments do not match the dataset keys. Please make sure each argument is named after a key in the dataset.');
} else { } else {
parent::__construct(sprintf('Test expects %d arguments but dataset only provides %d', $requiredCount, $suppliedCount)); parent::__construct(sprintf('The test expects [%d] argument(s), but the dataset only provides [%d].', $requiredCount, $suppliedCount));
} }
} }
//
} }
+1 -4
View File
@@ -14,11 +14,8 @@ use Symfony\Component\Console\Exception\ExceptionInterface;
*/ */
final class DatasetDoesNotExist extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace final class DatasetDoesNotExist extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
{ {
/**
* Creates a new Exception instance.
*/
public function __construct(string $name) public function __construct(string $name)
{ {
parent::__construct(sprintf("A dataset with the name `%s` does not exist. You can create it using `dataset('%s', ['a', 'b']);`.", $name, $name)); parent::__construct(sprintf("A dataset named [%s] does not exist. You may create one using `dataset('%s', ['a', 'b']);`.", $name, $name));
} }
} }
+2 -4
View File
@@ -15,18 +15,16 @@ use Symfony\Component\Console\Exception\ExceptionInterface;
final class DatasetMissing extends BadFunctionCallException implements ExceptionInterface, RenderlessEditor, RenderlessTrace final class DatasetMissing extends BadFunctionCallException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
{ {
/** /**
* Creates a new Exception instance.
*
* @param array<string, string> $arguments * @param array<string, string> $arguments
*/ */
public function __construct(string $file, string $name, array $arguments) public function __construct(string $file, string $name, array $arguments)
{ {
parent::__construct(sprintf( parent::__construct(sprintf(
'A test with the description [%s] has [%d] argument(s) ([%s]) and no dataset(s) provided in [%s]', 'The test [%s] in [%s] expects [%d] argument(s) ([%s]), but no dataset was provided. Please chain [with()] onto the test to supply one.',
$name, $name,
$file,
count($arguments), count($arguments),
implode(', ', array_map(static fn (string $arg, string $type): string => sprintf('%s $%s', $type, $arg), array_keys($arguments), $arguments)), implode(', ', array_map(static fn (string $arg, string $type): string => sprintf('%s $%s', $type, $arg), array_keys($arguments), $arguments)),
$file,
)); ));
} }
} }
+1 -4
View File
@@ -11,11 +11,8 @@ use Exception;
*/ */
final class ExpectationNotFound extends Exception final class ExpectationNotFound extends Exception
{ {
/**
* Creates a new ExpectationNotFound instance from the given name.
*/
public static function fromName(string $name): ExpectationNotFound public static function fromName(string $name): ExpectationNotFound
{ {
return new self("Expectation [$name] does not exist."); return new self("The expectation [$name] does not exist. You may register it using [expect()->extend()].");
} }
} }
+1 -4
View File
@@ -14,11 +14,8 @@ use Symfony\Component\Console\Exception\ExceptionInterface;
*/ */
final class FileOrFolderNotFound extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace final class FileOrFolderNotFound extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
{ {
/**
* Creates a new Exception instance.
*/
public function __construct(string $filename) public function __construct(string $filename)
{ {
parent::__construct(sprintf('The file or folder with the name `%s` could not be found.', $filename)); parent::__construct(sprintf('The file or folder [%s] could not be found. Please check the path and try again.', $filename));
} }
} }
@@ -14,9 +14,6 @@ use Symfony\Component\Console\Exception\ExceptionInterface;
*/ */
final class InvalidArgumentException extends BaseInvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace final class InvalidArgumentException extends BaseInvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
{ {
/**
* Creates a new Exception instance.
*/
public function __construct(string $message) public function __construct(string $message)
{ {
parent::__construct($message, 1); parent::__construct($message, 1);
+1 -1
View File
@@ -21,6 +21,6 @@ final class InvalidExpectation extends LogicException implements ExceptionInterf
*/ */
public static function fromMethods(array $methods): never public static function fromMethods(array $methods): never
{ {
throw new self(sprintf('Expectation [%s] is not valid.', implode('->', $methods))); throw new self(sprintf('The expectation [%s] does not exist. Please check the spelling, or register it using [expect()->extend()].', implode('->', $methods)));
} }
} }
+1 -1
View File
@@ -16,6 +16,6 @@ final class InvalidExpectationValue extends InvalidArgumentException
*/ */
public static function expected(string $type): never public static function expected(string $type): never
{ {
throw new self(sprintf('Invalid expectation value type. Expected [%s].', $type)); throw new self(sprintf('This expectation may only be used on a value of type [%s].', $type));
} }
} }
-3
View File
@@ -14,9 +14,6 @@ use Symfony\Component\Console\Exception\ExceptionInterface;
*/ */
final class InvalidOption extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace final class InvalidOption extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
{ {
/**
* Creates a new Exception instance.
*/
public function __construct(string $message) public function __construct(string $message)
{ {
parent::__construct($message, 1); parent::__construct($message, 1);
+1 -4
View File
@@ -14,11 +14,8 @@ use Symfony\Component\Console\Exception\ExceptionInterface;
*/ */
final class InvalidPestCommand extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace final class InvalidPestCommand extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
{ {
/**
* Creates a new Exception instance.
*/
public function __construct() public function __construct()
{ {
parent::__construct('Please run [./vendor/bin/pest] instead.'); parent::__construct('Pest must be run through its own binary. Please run [./vendor/bin/pest] instead.');
} }
} }
+35
View File
@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace Pest\Exceptions;
use InvalidArgumentException;
use NunoMaduro\Collision\Contracts\RenderlessEditor;
use NunoMaduro\Collision\Contracts\RenderlessTrace;
use Symfony\Component\Console\Exception\ExceptionInterface;
/**
* @internal
*/
final class InvalidTestClassName extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
{
public static function fromClassName(string $filename, string $className): self
{
return new self(sprintf(
'The test file [%s] would create the class [%s], which is not a valid PHP class name. Please rename the file.',
$filename,
$className,
));
}
public static function fromNamespace(string $filename, string $namespace, string $part): self
{
return new self(sprintf(
'The test file [%s] would create the namespace [%s], which is not a valid PHP namespace, because [%s] may not be used as a namespace name. Please rename that folder.',
$filename,
$namespace,
$part,
));
}
}
+1 -4
View File
@@ -14,11 +14,8 @@ use Symfony\Component\Console\Exception\ExceptionInterface;
*/ */
final class MissingDependency extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace final class MissingDependency extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
{ {
/**
* Creates a new Exception instance.
*/
public function __construct(string $feature, string $dependency) public function __construct(string $feature, string $dependency)
{ {
parent::__construct(sprintf('The feature "%s" requires "%s".', $feature, $dependency)); parent::__construct(sprintf('The [%s] feature requires [%s]. Please install it and try again.', $feature, $dependency));
} }
} }
-6
View File
@@ -16,9 +16,6 @@ use Symfony\Component\Console\Output\OutputInterface;
*/ */
final class NoDirtyTestsFound extends InvalidArgumentException implements ExceptionInterface, Panicable, RenderlessEditor, RenderlessTrace final class NoDirtyTestsFound extends InvalidArgumentException implements ExceptionInterface, Panicable, RenderlessEditor, RenderlessTrace
{ {
/**
* Renders the panic on the given output.
*/
public function render(OutputInterface $output): void public function render(OutputInterface $output): void
{ {
$output->writeln([ $output->writeln([
@@ -28,9 +25,6 @@ final class NoDirtyTestsFound extends InvalidArgumentException implements Except
]); ]);
} }
/**
* The exit code to be used.
*/
public function exitCode(): int public function exitCode(): int
{ {
return 0; return 0;
+1 -7
View File
@@ -12,15 +12,12 @@ use RuntimeException;
*/ */
final class ShouldNotHappen extends RuntimeException final class ShouldNotHappen extends RuntimeException
{ {
/**
* Creates a new Exception instance.
*/
public function __construct(Exception $exception) public function __construct(Exception $exception)
{ {
$message = $exception->getMessage(); $message = $exception->getMessage();
parent::__construct(sprintf(<<<'EOF' parent::__construct(sprintf(<<<'EOF'
This should not happen - please create an new issue here: https://github.com/pestphp/pest/issues This should not have happened. Please report it here: https://github.com/pestphp/pest/issues
Issue: %s Issue: %s
PHP version: %s PHP version: %s
@@ -29,9 +26,6 @@ EOF
, $message, phpversion(), PHP_OS), 1, $exception); , $message, phpversion(), PHP_OS), 1, $exception);
} }
/**
* Creates a new instance of should not happen without a specific exception.
*/
public static function fromMessage(string $message): ShouldNotHappen public static function fromMessage(string $message): ShouldNotHappen
{ {
return new ShouldNotHappen(new Exception($message)); return new ShouldNotHappen(new Exception($message));
+1 -4
View File
@@ -14,11 +14,8 @@ use Symfony\Component\Console\Exception\ExceptionInterface;
*/ */
final class TestAlreadyExist extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace final class TestAlreadyExist extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
{ {
/**
* Creates a new Exception instance.
*/
public function __construct(string $fileName, string $description) public function __construct(string $fileName, string $description)
{ {
parent::__construct(sprintf('A test with the description `%s` already exists in the filename `%s`.', $description, $fileName)); parent::__construct(sprintf('A test named [%s] already exists in [%s]. Please give this test a different description.', $description, $fileName));
} }
} }
+1 -4
View File
@@ -14,13 +14,10 @@ use Symfony\Component\Console\Exception\ExceptionInterface;
*/ */
final class TestCaseAlreadyInUse extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace final class TestCaseAlreadyInUse extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
{ {
/**
* Creates a new Exception instance.
*/
public function __construct(string $inUse, string $newOne, string $folder) public function __construct(string $inUse, string $newOne, string $folder)
{ {
parent::__construct(sprintf( parent::__construct(sprintf(
'Test case [%s] can not be used. The folder [%s] already uses the test case [%s].', 'The test case [%s] may not be used here. The folder [%s] is already bound to the test case [%s].',
$newOne, $newOne,
$folder, $folder,
$inUse, $inUse,
@@ -14,11 +14,8 @@ use Symfony\Component\Console\Exception\ExceptionInterface;
*/ */
final class TestCaseClassOrTraitNotFound extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace final class TestCaseClassOrTraitNotFound extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
{ {
/**
* Creates a new Exception instance.
*/
public function __construct(string $testCaseClass) public function __construct(string $testCaseClass)
{ {
parent::__construct(sprintf('The class `%s` was not found.', $testCaseClass)); parent::__construct(sprintf('The class or trait [%s] could not be found. Please check the name, and make sure it is autoloadable.', $testCaseClass));
} }
} }
@@ -15,14 +15,11 @@ use Symfony\Component\Console\Exception\ExceptionInterface;
*/ */
final class TestClosureMustNotBeStatic extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace final class TestClosureMustNotBeStatic extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
{ {
/**
* Creates a new Exception instance.
*/
public function __construct(TestCaseMethodFactory $method) public function __construct(TestCaseMethodFactory $method)
{ {
parent::__construct( parent::__construct(
sprintf( sprintf(
'Test closure must not be static. Please remove the [static] keyword from the [%s] method in [%s].', 'Test closures may not be static. Please remove the [static] keyword from the test [%s] in [%s].',
$method->description, $method->description,
$method->filename $method->filename
) )
+1 -4
View File
@@ -14,11 +14,8 @@ use Symfony\Component\Console\Exception\ExceptionInterface;
*/ */
final class TestDescriptionMissing extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace final class TestDescriptionMissing extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
{ {
/**
* Creates a new Exception instance.
*/
public function __construct(string $fileName) public function __construct(string $fileName)
{ {
parent::__construct(sprintf('Test description is missing in the filename `%s`.', $fileName)); parent::__construct(sprintf('A test in [%s] is missing its description. Please give every test a description.', $fileName));
} }
} }
+49
View File
@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace Pest\Exceptions;
use NunoMaduro\Collision\Contracts\RenderlessEditor;
use NunoMaduro\Collision\Contracts\RenderlessTrace;
use Pest\Contracts\Panicable;
use RuntimeException;
use Symfony\Component\Console\Exception\ExceptionInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* @internal
*/
final class TiaRequiresCommit extends RuntimeException implements ExceptionInterface, Panicable, RenderlessEditor, RenderlessTrace
{
public function __construct()
{
parent::__construct(
'Tia mode requires a repository with at least one commit, so the baseline it records can be anchored to a revision.',
);
}
public function render(OutputInterface $output): void
{
$output->writeln([
'',
' <fg=white;options=bold;bg=red> ERROR </> Tia mode requires at least one commit.',
'',
' A baseline is anchored to the revision it was recorded at, and this repository',
' has none yet, so there is nothing to record against and nothing to compare a',
' later run to.',
'',
' Commit once, then run again:',
'',
' <fg=yellow>git add . && git commit -m "Initial commit"</>',
'',
' Runs without <fg=yellow>--tia</> are unaffected.',
'',
]);
}
public function exitCode(): int
{
return 1;
}
}
@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace Pest\Exceptions;
use NunoMaduro\Collision\Contracts\RenderlessEditor;
use NunoMaduro\Collision\Contracts\RenderlessTrace;
use Pest\Contracts\Panicable;
use RuntimeException;
use Symfony\Component\Console\Exception\ExceptionInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* @internal
*/
final class TiaRequiresDefaultBranch extends RuntimeException implements ExceptionInterface, Panicable, RenderlessEditor, RenderlessTrace
{
public function __construct()
{
parent::__construct(
'Tia mode could not determine the default branch every other branch falls back to reading.',
);
}
public function render(OutputInterface $output): void
{
$output->writeln([
'',
' <fg=white;options=bold;bg=red> ERROR </> Tia mode could not determine the default branch.',
'',
' It is the branch whose baseline every other branch falls back to reading, and',
' nothing in this checkout names it: the repository has a remote, but no',
' <fg=yellow>origin/HEAD</>, and no CI provider stated it either. Guessing would re-run the',
' whole suite on every new branch while reporting it as a cache hit.',
'',
' Name the branch in <fg=yellow>tests/Pest.php</>:',
'',
' <fg=yellow>pest()->tia()->defaultBranch(\'master\');</>',
'',
' Or let git answer, once per clone:',
'',
' <fg=yellow>git remote set-head origin --auto</>',
'',
]);
}
public function exitCode(): int
{
return 1;
}
}
+1 -1
View File
@@ -19,7 +19,7 @@ final class TiaRequiresPestTests extends RuntimeException implements ExceptionIn
public function __construct(private readonly string $className, string $filename) public function __construct(private readonly string $className, string $filename)
{ {
parent::__construct(sprintf( parent::__construct(sprintf(
'Tia mode requires only functional based Pest tests, but encountered PHPUnit class [%s] in [%s].', 'Tia mode supports functional Pest tests only, but found the PHPUnit class [%s] in [%s]. Please convert it to a Pest test, or run without Tia.',
$className, $className,
$filename, $filename,
)); ));
+47
View File
@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace Pest\Exceptions;
use NunoMaduro\Collision\Contracts\RenderlessEditor;
use NunoMaduro\Collision\Contracts\RenderlessTrace;
use Pest\Contracts\Panicable;
use RuntimeException;
use Symfony\Component\Console\Exception\ExceptionInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* @internal
*/
final class TiaRequiresRemote extends RuntimeException implements ExceptionInterface, Panicable, RenderlessEditor, RenderlessTrace
{
public function __construct()
{
parent::__construct(
'Tia mode requires a repository with a remote, so the default branch every other branch falls back to reading can be resolved.',
);
}
public function render(OutputInterface $output): void
{
$output->writeln([
'',
' <fg=white;options=bold;bg=red> ERROR </> Tia mode requires a repository with a remote.',
'',
' Without one there is no way to tell which branch is the default — the branch',
' whose baseline every other branch falls back to reading — so the first run on',
' each new branch would silently re-run the whole suite.',
'',
' Add a remote, or name the branch yourself in <fg=yellow>tests/Pest.php</>:',
'',
' <fg=yellow>pest()->tia()->defaultBranch(\'master\');</>',
'',
]);
}
public function exitCode(): int
{
return 1;
}
}
+1 -1
View File
@@ -19,7 +19,7 @@ final class TiaRequiresRepositoryRoot extends RuntimeException implements Except
public function __construct(private readonly string $subdirectoryPrefix) public function __construct(private readonly string $subdirectoryPrefix)
{ {
parent::__construct(sprintf( parent::__construct(sprintf(
'Tia mode requires the project root to be the git repository root, but it sits in the subdirectory [%s] of a larger repo.', 'Tia mode requires the project root to be the git repository root, but this project sits in the subdirectory [%s] of a larger repository. Please give it its own repository to use Tia.',
$this->subdirectoryPrefix, $this->subdirectoryPrefix,
)); ));
} }
+7 -204
View File
@@ -4,7 +4,6 @@ declare(strict_types=1);
namespace Pest; namespace Pest;
use Attribute;
use BadMethodCallException; use BadMethodCallException;
use Closure; use Closure;
use InvalidArgumentException; use InvalidArgumentException;
@@ -60,19 +59,15 @@ final class Expectation
use Retrievable; use Retrievable;
/** /**
* Creates a new expectation.
*
* @param TValue $value * @param TValue $value
*/ */
public function __construct( public function __construct(
public mixed $value public mixed $value
) { ) {
// .. //
} }
/** /**
* Creates a new expectation.
*
* @template TAndValue * @template TAndValue
* *
* @param TAndValue $value * @param TAndValue $value
@@ -84,8 +79,6 @@ final class Expectation
} }
/** /**
* Creates a new expectation with the decoded JSON value.
*
* @return self<array<int|string, mixed>|bool> * @return self<array<int|string, mixed>|bool>
*/ */
public function json(): Expectation public function json(): Expectation
@@ -103,8 +96,6 @@ final class Expectation
} }
/** /**
* Dump the expectation value.
*
* @return self<TValue> * @return self<TValue>
*/ */
public function dump(mixed ...$arguments): self public function dump(mixed ...$arguments): self
@@ -118,9 +109,6 @@ final class Expectation
return $this; return $this;
} }
/**
* Dump the expectation value and end the script.
*/
public function dd(mixed ...$arguments): never public function dd(mixed ...$arguments): never
{ {
if (function_exists('dd')) { if (function_exists('dd')) {
@@ -141,8 +129,6 @@ final class Expectation
} }
/** /**
* Dump the expectation value when the result of the condition is truthy.
*
* @param (Closure(TValue): bool)|bool $condition * @param (Closure(TValue): bool)|bool $condition
* @return self<TValue> * @return self<TValue>
*/ */
@@ -158,8 +144,6 @@ final class Expectation
} }
/** /**
* Dump the expectation value when the result of the condition is falsy.
*
* @param (Closure(TValue): bool)|bool $condition * @param (Closure(TValue): bool)|bool $condition
* @return self<TValue> * @return self<TValue>
*/ */
@@ -175,8 +159,6 @@ final class Expectation
} }
/** /**
* Send the expectation value to Ray along with all given arguments.
*
* @return self<TValue> * @return self<TValue>
*/ */
public function ray(mixed ...$arguments): self public function ray(mixed ...$arguments): self
@@ -189,8 +171,6 @@ final class Expectation
} }
/** /**
* Creates the opposite expectation for the value.
*
* @return OppositeExpectation<TValue> * @return OppositeExpectation<TValue>
*/ */
public function not(): OppositeExpectation public function not(): OppositeExpectation
@@ -199,8 +179,6 @@ final class Expectation
} }
/** /**
* Creates an expectation on each item of the iterable "value".
*
* @return EachExpectation<TValue> * @return EachExpectation<TValue>
*/ */
public function each(?callable $callback = null): EachExpectation public function each(?callable $callback = null): EachExpectation
@@ -219,8 +197,6 @@ final class Expectation
} }
/** /**
* Allows you to specify a sequential set of expectations for each item in a iterable "value".
*
* @template TSequenceValue * @template TSequenceValue
* *
* @param (callable(self<TValue>, self<string|int>): void)|TSequenceValue ...$callbacks * @param (callable(self<TValue>, self<string|int>): void)|TSequenceValue ...$callbacks
@@ -258,8 +234,6 @@ final class Expectation
} }
/** /**
* If the subject matches one of the given "expressions", the expression callback will run.
*
* @template TMatchSubject of array-key * @template TMatchSubject of array-key
* *
* @param (callable(): TMatchSubject)|TMatchSubject $subject * @param (callable(): TMatchSubject)|TMatchSubject $subject
@@ -298,8 +272,6 @@ final class Expectation
} }
/** /**
* Apply the callback if the given "condition" is falsy.
*
* @param (callable(): bool)|bool $condition * @param (callable(): bool)|bool $condition
* @param callable(Expectation<TValue>): mixed $callback * @param callable(Expectation<TValue>): mixed $callback
* @return self<TValue> * @return self<TValue>
@@ -314,8 +286,6 @@ final class Expectation
} }
/** /**
* Apply the callback if the given "condition" is truthy.
*
* @param (callable(): bool)|bool $condition * @param (callable(): bool)|bool $condition
* @param callable(self<TValue>): mixed $callback * @param callable(self<TValue>): mixed $callback
* @return self<TValue> * @return self<TValue>
@@ -334,8 +304,6 @@ final class Expectation
} }
/** /**
* Dynamically calls methods on the class or creates a new higher order expectation.
*
* @param array<int, mixed> $parameters * @param array<int, mixed> $parameters
* @return Expectation<TValue>|HigherOrderExpectation<Expectation<TValue>, TValue> * @return Expectation<TValue>|HigherOrderExpectation<Expectation<TValue>, TValue>
*/ */
@@ -350,7 +318,7 @@ final class Expectation
if (! is_object($this->value)) { if (! is_object($this->value)) {
throw new BadMethodCallException(sprintf( throw new BadMethodCallException(sprintf(
'Method "%s" does not exist in %s.', 'Method [%s] does not exist in [%s].',
$method, $method,
gettype($this->value) gettype($this->value)
)); ));
@@ -379,8 +347,6 @@ final class Expectation
} }
/** /**
* Creates a new expectation closure from the given name.
*
* @throws ExpectationNotFound * @throws ExpectationNotFound
*/ */
private function getExpectationClosure(string $name): Closure private function getExpectationClosure(string $name): Closure
@@ -402,8 +368,6 @@ final class Expectation
} }
/** /**
* Dynamically calls methods on the class without any arguments or creates a new higher order expectation.
*
* @return Expectation<TValue>|OppositeExpectation<TValue>|EachExpectation<TValue>|HigherOrderExpectation<Expectation<TValue>, TValue|null>|TValue * @return Expectation<TValue>|OppositeExpectation<TValue>|EachExpectation<TValue>|HigherOrderExpectation<Expectation<TValue>, TValue|null>|TValue
*/ */
public function __get(string $name): mixed public function __get(string $name): mixed
@@ -422,9 +386,6 @@ final class Expectation
return $this->{$name}(); return $this->{$name}();
} }
/**
* Checks if the given expectation method exists.
*/
public static function hasMethod(string $name): bool public static function hasMethod(string $name): bool
{ {
return method_exists(self::class, $name) return method_exists(self::class, $name)
@@ -432,17 +393,12 @@ final class Expectation
|| self::hasExtend($name); || self::hasExtend($name);
} }
/**
* Matches any value.
*/
public function any(): Any public function any(): Any
{ {
return new Any; return new Any;
} }
/** /**
* Asserts that the given expectation target use the given dependencies.
*
* @param array<int, string>|string $targets * @param array<int, string>|string $targets
*/ */
public function toUse(array|string $targets): ArchExpectation public function toUse(array|string $targets): ArchExpectation
@@ -450,9 +406,6 @@ final class Expectation
return ToUse::make($this, $targets); return ToUse::make($this, $targets);
} }
/**
* Asserts that the given expectation target does have the given permissions
*/
public function toHaveFileSystemPermissions(string $permissions): ArchExpectation public function toHaveFileSystemPermissions(string $permissions): ArchExpectation
{ {
return Targeted::make( return Targeted::make(
@@ -463,9 +416,6 @@ final class Expectation
); );
} }
/**
* Asserts that the given expectation target to have line count less than the given number.
*/
public function toHaveLineCountLessThan(int $lines): ArchExpectation public function toHaveLineCountLessThan(int $lines): ArchExpectation
{ {
return Targeted::make( return Targeted::make(
@@ -476,9 +426,6 @@ final class Expectation
); );
} }
/**
* Asserts that the given expectation target have all methods documented.
*/
public function toHaveMethodsDocumented(): ArchExpectation public function toHaveMethodsDocumented(): ArchExpectation
{ {
return Targeted::make( return Targeted::make(
@@ -495,9 +442,6 @@ final class Expectation
); );
} }
/**
* Asserts that the given expectation target have all properties documented.
*/
public function toHavePropertiesDocumented(): ArchExpectation public function toHavePropertiesDocumented(): ArchExpectation
{ {
return Targeted::make( return Targeted::make(
@@ -515,9 +459,6 @@ final class Expectation
); );
} }
/**
* Asserts that the given expectation target use the "declare(strict_types=1)" declaration.
*/
public function toUseStrictTypes(): ArchExpectation public function toUseStrictTypes(): ArchExpectation
{ {
return Targeted::make( return Targeted::make(
@@ -528,9 +469,6 @@ final class Expectation
); );
} }
/**
* Asserts that the given expectation target uses strict equality.
*/
public function toUseStrictEquality(): ArchExpectation public function toUseStrictEquality(): ArchExpectation
{ {
return Targeted::make( return Targeted::make(
@@ -541,9 +479,6 @@ final class Expectation
); );
} }
/**
* Asserts that the given expectation target is final.
*/
public function toBeFinal(): ArchExpectation public function toBeFinal(): ArchExpectation
{ {
return Targeted::make( return Targeted::make(
@@ -554,9 +489,6 @@ final class Expectation
); );
} }
/**
* Asserts that the given expectation target is readonly.
*/
public function toBeReadonly(): ArchExpectation public function toBeReadonly(): ArchExpectation
{ {
return Targeted::make( return Targeted::make(
@@ -567,9 +499,6 @@ final class Expectation
); );
} }
/**
* Asserts that the given expectation target is trait.
*/
public function toBeTrait(): ArchExpectation public function toBeTrait(): ArchExpectation
{ {
return Targeted::make( return Targeted::make(
@@ -580,17 +509,11 @@ final class Expectation
); );
} }
/**
* Asserts that the given expectation targets are traits.
*/
public function toBeTraits(): ArchExpectation public function toBeTraits(): ArchExpectation
{ {
return $this->toBeTrait(); return $this->toBeTrait();
} }
/**
* Asserts that the given expectation target is abstract.
*/
public function toBeAbstract(): ArchExpectation public function toBeAbstract(): ArchExpectation
{ {
return Targeted::make( return Targeted::make(
@@ -602,8 +525,6 @@ final class Expectation
} }
/** /**
* Asserts that the given expectation target has a specific method.
*
* @param array<int, string>|string $method * @param array<int, string>|string $method
*/ */
public function toHaveMethod(array|string $method): ArchExpectation public function toHaveMethod(array|string $method): ArchExpectation
@@ -613,14 +534,12 @@ final class Expectation
return Targeted::make( return Targeted::make(
$this, $this,
fn (ObjectDescription $object): bool => count(array_filter($methods, fn (string $method): bool => isset($object->reflectionClass) && $object->reflectionClass->hasMethod($method))) === count($methods), fn (ObjectDescription $object): bool => count(array_filter($methods, fn (string $method): bool => isset($object->reflectionClass) && $object->reflectionClass->hasMethod($method))) === count($methods),
sprintf("to have method '%s'", implode("', '", $methods)), sprintf('to have method [%s]', implode('], [', $methods)),
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
); );
} }
/** /**
* Asserts that the given expectation target has a specific methods.
*
* @param array<int, string> $methods * @param array<int, string> $methods
*/ */
public function toHaveMethods(array $methods): ArchExpectation public function toHaveMethods(array $methods): ArchExpectation
@@ -628,57 +547,36 @@ final class Expectation
return $this->toHaveMethod($methods); return $this->toHaveMethod($methods);
} }
/**
* Not supported.
*/
public function toHavePublicMethodsBesides(): void public function toHavePublicMethodsBesides(): void
{ {
throw InvalidExpectation::fromMethods(['toHavePublicMethodsBesides']); throw InvalidExpectation::fromMethods(['toHavePublicMethodsBesides']);
} }
/**
* Not supported.
*/
public function toHavePublicMethods(): void public function toHavePublicMethods(): void
{ {
throw InvalidExpectation::fromMethods(['toHavePublicMethods']); throw InvalidExpectation::fromMethods(['toHavePublicMethods']);
} }
/**
* Not supported.
*/
public function toHaveProtectedMethodsBesides(): void public function toHaveProtectedMethodsBesides(): void
{ {
throw InvalidExpectation::fromMethods(['toHaveProtectedMethodsBesides']); throw InvalidExpectation::fromMethods(['toHaveProtectedMethodsBesides']);
} }
/**
* Not supported.
*/
public function toHaveProtectedMethods(): void public function toHaveProtectedMethods(): void
{ {
throw InvalidExpectation::fromMethods(['toHaveProtectedMethods']); throw InvalidExpectation::fromMethods(['toHaveProtectedMethods']);
} }
/**
* Not supported.
*/
public function toHavePrivateMethodsBesides(): void public function toHavePrivateMethodsBesides(): void
{ {
throw InvalidExpectation::fromMethods(['toHavePrivateMethodsBesides']); throw InvalidExpectation::fromMethods(['toHavePrivateMethodsBesides']);
} }
/**
* Not supported.
*/
public function toHavePrivateMethods(): void public function toHavePrivateMethods(): void
{ {
throw InvalidExpectation::fromMethods(['toHavePrivateMethods']); throw InvalidExpectation::fromMethods(['toHavePrivateMethods']);
} }
/**
* Asserts that the given expectation target is cased correctly.
*/
public function toBeCasedCorrectly(): ArchExpectation public function toBeCasedCorrectly(): ArchExpectation
{ {
return Targeted::make( return Targeted::make(
@@ -711,9 +609,6 @@ final class Expectation
); );
} }
/**
* Asserts that the given expectation target is enum.
*/
public function toBeEnum(): ArchExpectation public function toBeEnum(): ArchExpectation
{ {
return Targeted::make( return Targeted::make(
@@ -724,17 +619,11 @@ final class Expectation
); );
} }
/**
* Asserts that the given expectation targets are enums.
*/
public function toBeEnums(): ArchExpectation public function toBeEnums(): ArchExpectation
{ {
return $this->toBeEnum(); return $this->toBeEnum();
} }
/**
* Asserts that the given expectation target is a class.
*/
public function toBeClass(): ArchExpectation public function toBeClass(): ArchExpectation
{ {
return Targeted::make( return Targeted::make(
@@ -745,17 +634,11 @@ final class Expectation
); );
} }
/**
* Asserts that the given expectation targets are classes.
*/
public function toBeClasses(): ArchExpectation public function toBeClasses(): ArchExpectation
{ {
return $this->toBeClass(); return $this->toBeClass();
} }
/**
* Asserts that the given expectation target is interface.
*/
public function toBeInterface(): ArchExpectation public function toBeInterface(): ArchExpectation
{ {
return Targeted::make( return Targeted::make(
@@ -766,30 +649,21 @@ final class Expectation
); );
} }
/**
* Asserts that the given expectation targets are interfaces.
*/
public function toBeInterfaces(): ArchExpectation public function toBeInterfaces(): ArchExpectation
{ {
return $this->toBeInterface(); return $this->toBeInterface();
} }
/**
* Asserts that the given expectation target to be subclass of the given class.
*/
public function toExtend(string $class): ArchExpectation public function toExtend(string $class): ArchExpectation
{ {
return Targeted::make( return Targeted::make(
$this, $this,
fn (ObjectDescription $object): bool => isset($object->reflectionClass) && ($class === $object->reflectionClass->getName() || $object->reflectionClass->isSubclassOf($class)), fn (ObjectDescription $object): bool => isset($object->reflectionClass) && ($class === $object->reflectionClass->getName() || $object->reflectionClass->isSubclassOf($class)),
sprintf("to extend '%s'", $class), sprintf('to extend [%s]', $class),
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
); );
} }
/**
* Asserts that the given expectation target to be have a parent class.
*/
public function toExtendNothing(): ArchExpectation public function toExtendNothing(): ArchExpectation
{ {
return Targeted::make( return Targeted::make(
@@ -800,17 +674,12 @@ final class Expectation
); );
} }
/**
* Asserts that the given expectation target to use the given trait.
*/
public function toUseTrait(string $trait): ArchExpectation public function toUseTrait(string $trait): ArchExpectation
{ {
return $this->toUseTraits($trait); return $this->toUseTraits($trait);
} }
/** /**
* Asserts that the given expectation target to use the given traits.
*
* @param array<int, string>|string $traits * @param array<int, string>|string $traits
*/ */
public function toUseTraits(array|string $traits): ArchExpectation public function toUseTraits(array|string $traits): ArchExpectation
@@ -852,9 +721,6 @@ final class Expectation
); );
} }
/**
* Asserts that the given expectation target to not implement any interfaces.
*/
public function toImplementNothing(): ArchExpectation public function toImplementNothing(): ArchExpectation
{ {
return Targeted::make( return Targeted::make(
@@ -866,8 +732,6 @@ final class Expectation
} }
/** /**
* Asserts that the given expectation target to only implement the given interfaces.
*
* @param array<int, string>|string $interfaces * @param array<int, string>|string $interfaces
*/ */
public function toOnlyImplement(array|string $interfaces): ArchExpectation public function toOnlyImplement(array|string $interfaces): ArchExpectation
@@ -884,35 +748,27 @@ final class Expectation
); );
} }
/**
* Asserts that the given expectation target to have the given prefix.
*/
public function toHavePrefix(string $prefix): ArchExpectation public function toHavePrefix(string $prefix): ArchExpectation
{ {
return Targeted::make( return Targeted::make(
$this, $this,
fn (ObjectDescription $object): bool => isset($object->reflectionClass) && str_starts_with($object->reflectionClass->getShortName(), $prefix), fn (ObjectDescription $object): bool => isset($object->reflectionClass) && str_starts_with($object->reflectionClass->getShortName(), $prefix),
"to have prefix '{$prefix}'", "to have prefix [{$prefix}]",
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
); );
} }
/**
* Asserts that the given expectation target to have the given suffix.
*/
public function toHaveSuffix(string $suffix): ArchExpectation public function toHaveSuffix(string $suffix): ArchExpectation
{ {
return Targeted::make( return Targeted::make(
$this, $this,
fn (ObjectDescription $object): bool => isset($object->reflectionClass) && str_ends_with($object->reflectionClass->getName(), $suffix), fn (ObjectDescription $object): bool => isset($object->reflectionClass) && str_ends_with($object->reflectionClass->getName(), $suffix),
"to have suffix '{$suffix}'", "to have suffix [{$suffix}]",
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
); );
} }
/** /**
* Asserts that the given expectation target to implement the given interfaces.
*
* @param array<int, string>|string $interfaces * @param array<int, string>|string $interfaces
*/ */
public function toImplement(array|string $interfaces): ArchExpectation public function toImplement(array|string $interfaces): ArchExpectation
@@ -928,8 +784,6 @@ final class Expectation
} }
/** /**
* Asserts that the given expectation target "only" use on the given dependencies.
*
* @param array<int, string>|string $targets * @param array<int, string>|string $targets
*/ */
public function toOnlyUse(array|string $targets): ArchExpectation public function toOnlyUse(array|string $targets): ArchExpectation
@@ -937,33 +791,22 @@ final class Expectation
return ToOnlyUse::make($this, $targets); return ToOnlyUse::make($this, $targets);
} }
/**
* Asserts that the given expectation target does not use any dependencies.
*/
public function toUseNothing(): ArchExpectation public function toUseNothing(): ArchExpectation
{ {
return ToUseNothing::make($this); return ToUseNothing::make($this);
} }
/**
* Asserts that the source code of the given expectation target does not include suspicious characters.
*/
public function toHaveSuspiciousCharacters(): ArchExpectation public function toHaveSuspiciousCharacters(): ArchExpectation
{ {
throw InvalidExpectation::fromMethods(['toHaveSuspiciousCharacters']); throw InvalidExpectation::fromMethods(['toHaveSuspiciousCharacters']);
} }
/**
* Not supported.
*/
public function toBeUsed(): void public function toBeUsed(): void
{ {
throw InvalidExpectation::fromMethods(['toBeUsed']); throw InvalidExpectation::fromMethods(['toBeUsed']);
} }
/** /**
* Asserts that the given expectation dependency is used by the given targets.
*
* @param array<int, string>|string $targets * @param array<int, string>|string $targets
*/ */
public function toBeUsedIn(array|string $targets): ArchExpectation public function toBeUsedIn(array|string $targets): ArchExpectation
@@ -972,8 +815,6 @@ final class Expectation
} }
/** /**
* Asserts that the given expectation dependency is "only" used by the given targets.
*
* @param array<int, string>|string $targets * @param array<int, string>|string $targets
*/ */
public function toOnlyBeUsedIn(array|string $targets): ArchExpectation public function toOnlyBeUsedIn(array|string $targets): ArchExpectation
@@ -981,17 +822,11 @@ final class Expectation
return ToOnlyBeUsedIn::make($this, $targets); return ToOnlyBeUsedIn::make($this, $targets);
} }
/**
* Asserts that the given expectation dependency is not used.
*/
public function toBeUsedInNothing(): ArchExpectation public function toBeUsedInNothing(): ArchExpectation
{ {
return ToBeUsedInNothing::make($this); return ToBeUsedInNothing::make($this);
} }
/**
* Asserts that the given expectation dependency is an invokable class.
*/
public function toBeInvokable(): ArchExpectation public function toBeInvokable(): ArchExpectation
{ {
return Targeted::make( return Targeted::make(
@@ -1003,8 +838,6 @@ final class Expectation
} }
/** /**
* Asserts that the given expectation is iterable and contains snake_case keys.
*
* @return self<TValue> * @return self<TValue>
*/ */
public function toHaveSnakeCaseKeys(string $message = ''): self public function toHaveSnakeCaseKeys(string $message = ''): self
@@ -1027,8 +860,6 @@ final class Expectation
} }
/** /**
* Asserts that the given expectation is iterable and contains kebab-case keys.
*
* @return self<TValue> * @return self<TValue>
*/ */
public function toHaveKebabCaseKeys(string $message = ''): self public function toHaveKebabCaseKeys(string $message = ''): self
@@ -1051,8 +882,6 @@ final class Expectation
} }
/** /**
* Asserts that the given expectation is iterable and contains camelCase keys.
*
* @return self<TValue> * @return self<TValue>
*/ */
public function toHaveCamelCaseKeys(string $message = ''): self public function toHaveCamelCaseKeys(string $message = ''): self
@@ -1075,8 +904,6 @@ final class Expectation
} }
/** /**
* Asserts that the given expectation is iterable and contains StudlyCase keys.
*
* @return self<TValue> * @return self<TValue>
*/ */
public function toHaveStudlyCaseKeys(string $message = ''): self public function toHaveStudlyCaseKeys(string $message = ''): self
@@ -1098,38 +925,26 @@ final class Expectation
return $this; return $this;
} }
/**
* Asserts that the given expectation target to have the given attribute.
*/
public function toHaveAttribute(string $attribute): ArchExpectation public function toHaveAttribute(string $attribute): ArchExpectation
{ {
return Targeted::make( return Targeted::make(
$this, $this,
fn (ObjectDescription $object): bool => isset($object->reflectionClass) && $object->reflectionClass->getAttributes($attribute) !== [], fn (ObjectDescription $object): bool => isset($object->reflectionClass) && $object->reflectionClass->getAttributes($attribute) !== [],
"to have attribute '{$attribute}'", "to have attribute [{$attribute}]",
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
); );
} }
/**
* Asserts that the given expectation target has a constructor method.
*/
public function toHaveConstructor(): ArchExpectation public function toHaveConstructor(): ArchExpectation
{ {
return $this->toHaveMethod('__construct'); return $this->toHaveMethod('__construct');
} }
/**
* Asserts that the given expectation target has a destructor method.
*/
public function toHaveDestructor(): ArchExpectation public function toHaveDestructor(): ArchExpectation
{ {
return $this->toHaveMethod('__destruct'); return $this->toHaveMethod('__destruct');
} }
/**
* Asserts that the given expectation target is a backed enum of given type.
*/
private function toBeBackedEnum(string $backingType): ArchExpectation private function toBeBackedEnum(string $backingType): ArchExpectation
{ {
return Targeted::make( return Targeted::make(
@@ -1143,33 +958,21 @@ final class Expectation
); );
} }
/**
* Asserts that the given expectation targets are string backed enums.
*/
public function toBeStringBackedEnums(): ArchExpectation public function toBeStringBackedEnums(): ArchExpectation
{ {
return $this->toBeStringBackedEnum(); return $this->toBeStringBackedEnum();
} }
/**
* Asserts that the given expectation targets are int backed enums.
*/
public function toBeIntBackedEnums(): ArchExpectation public function toBeIntBackedEnums(): ArchExpectation
{ {
return $this->toBeIntBackedEnum(); return $this->toBeIntBackedEnum();
} }
/**
* Asserts that the given expectation target is a string backed enum.
*/
public function toBeStringBackedEnum(): ArchExpectation public function toBeStringBackedEnum(): ArchExpectation
{ {
return $this->toBeBackedEnum('string'); return $this->toBeBackedEnum('string');
} }
/**
* Asserts that the given expectation target is an int backed enum.
*/
public function toBeIntBackedEnum(): ArchExpectation public function toBeIntBackedEnum(): ArchExpectation
{ {
return $this->toBeBackedEnum('int'); return $this->toBeBackedEnum('int');
-13
View File
@@ -17,21 +17,14 @@ use function expect;
*/ */
final class EachExpectation final class EachExpectation
{ {
/**
* Indicates if the expectation is the opposite.
*/
private bool $opposite = false; private bool $opposite = false;
/** /**
* Creates an expectation on each item of the iterable "value".
*
* @param Expectation<TValue> $original * @param Expectation<TValue> $original
*/ */
public function __construct(private readonly Expectation $original) {} public function __construct(private readonly Expectation $original) {}
/** /**
* Creates a new expectation.
*
* @template TAndValue * @template TAndValue
* *
* @param TAndValue $value * @param TAndValue $value
@@ -43,8 +36,6 @@ final class EachExpectation
} }
/** /**
* Creates the opposite expectation for the value.
*
* @return self<TValue> * @return self<TValue>
*/ */
public function not(): self public function not(): self
@@ -55,8 +46,6 @@ final class EachExpectation
} }
/** /**
* Dynamically calls methods on the class with the given arguments on each item.
*
* @param array<int|string, mixed> $arguments * @param array<int|string, mixed> $arguments
* @return self<TValue> * @return self<TValue>
*/ */
@@ -73,8 +62,6 @@ final class EachExpectation
} }
/** /**
* Dynamically calls methods on the class without any arguments on each item.
*
* @return self<TValue> * @return self<TValue>
*/ */
public function __get(string $name): self public function __get(string $name): self
@@ -25,19 +25,11 @@ final class HigherOrderExpectation
*/ */
private Expectation|EachExpectation $expectation; private Expectation|EachExpectation $expectation;
/**
* Indicates if the expectation is the opposite.
*/
private bool $opposite = false; private bool $opposite = false;
/**
* Indicates if the expectation should reset the value.
*/
private bool $shouldReset = false; private bool $shouldReset = false;
/** /**
* Creates a new higher order expectation.
*
* @param Expectation<TOriginalValue> $original * @param Expectation<TOriginalValue> $original
* @param TValue $value * @param TValue $value
*/ */
@@ -47,8 +39,6 @@ final class HigherOrderExpectation
} }
/** /**
* Creates the opposite expectation for the value.
*
* @return self<TOriginalValue, TValue> * @return self<TOriginalValue, TValue>
*/ */
public function not(): self public function not(): self
@@ -59,8 +49,6 @@ final class HigherOrderExpectation
} }
/** /**
* Creates a new Expectation.
*
* @template TExpectValue * @template TExpectValue
* *
* @param TExpectValue $value * @param TExpectValue $value
@@ -72,8 +60,6 @@ final class HigherOrderExpectation
} }
/** /**
* Creates a new expectation.
*
* @template TExpectValue * @template TExpectValue
* *
* @param TExpectValue $value * @param TExpectValue $value
@@ -85,9 +71,6 @@ final class HigherOrderExpectation
} }
/** /**
* Scope an expectation callback to the current value in
* the HigherOrderExpectation chain.
*
* @param Closure(Expectation<TValue>): void $expectation * @param Closure(Expectation<TValue>): void $expectation
* @return HigherOrderExpectation<TOriginalValue, TOriginalValue> * @return HigherOrderExpectation<TOriginalValue, TOriginalValue>
*/ */
@@ -99,8 +82,6 @@ final class HigherOrderExpectation
} }
/** /**
* Creates a new expectation with the decoded JSON value.
*
* @return self<TOriginalValue, array<string|int, mixed>|bool> * @return self<TOriginalValue, array<string|int, mixed>|bool>
*/ */
public function json(): self public function json(): self
@@ -109,8 +90,6 @@ final class HigherOrderExpectation
} }
/** /**
* Dynamically calls methods on the class with the given arguments.
*
* @param array<int, mixed> $arguments * @param array<int, mixed> $arguments
* @return self<TOriginalValue, mixed> * @return self<TOriginalValue, mixed>
*/ */
@@ -125,8 +104,6 @@ final class HigherOrderExpectation
} }
/** /**
* Accesses properties in the value or in the expectation.
*
* @return self<TOriginalValue, mixed> * @return self<TOriginalValue, mixed>
*/ */
public function __get(string $name): self public function __get(string $name): self
@@ -145,9 +122,6 @@ final class HigherOrderExpectation
return $this->performAssertion($name, []); return $this->performAssertion($name, []);
} }
/**
* Determines if the original expectation has the given method name.
*/
private function expectationHasMethod(string $name): bool private function expectationHasMethod(string $name): bool
{ {
if (method_exists($this->original, $name)) { if (method_exists($this->original, $name)) {
@@ -161,8 +135,6 @@ final class HigherOrderExpectation
} }
/** /**
* Retrieve the applicable value based on the current reset condition.
*
* @return TOriginalValue|TValue * @return TOriginalValue|TValue
*/ */
private function getValue(): mixed private function getValue(): mixed
@@ -171,8 +143,6 @@ final class HigherOrderExpectation
} }
/** /**
* Performs the given assertion with the current expectation.
*
* @param array<int, mixed> $arguments * @param array<int, mixed> $arguments
* @return self<TOriginalValue, TValue> * @return self<TOriginalValue, TValue>
*/ */
+7 -159
View File
@@ -4,7 +4,6 @@ declare(strict_types=1);
namespace Pest\Expectations; namespace Pest\Expectations;
use Attribute;
use Pest\Arch\Contracts\ArchExpectation; use Pest\Arch\Contracts\ArchExpectation;
use Pest\Arch\Expectations\Targeted; use Pest\Arch\Expectations\Targeted;
use Pest\Arch\Expectations\ToBeUsedIn; use Pest\Arch\Expectations\ToBeUsedIn;
@@ -38,15 +37,11 @@ use stdClass;
final readonly class OppositeExpectation final readonly class OppositeExpectation
{ {
/** /**
* Creates a new opposite expectation.
*
* @param Expectation<TValue> $original * @param Expectation<TValue> $original
*/ */
public function __construct(private Expectation $original) {} public function __construct(private Expectation $original) {}
/** /**
* Asserts that the value array not has the provided $keys.
*
* @param array<int, int|string|array<int-string, mixed>> $keys * @param array<int, int|string|array<int-string, mixed>> $keys
* @return Expectation<TValue> * @return Expectation<TValue>
*/ */
@@ -70,8 +65,6 @@ final readonly class OppositeExpectation
} }
/** /**
* Asserts that the given expectation target does not use any of the given dependencies.
*
* @param array<int, string>|string $targets * @param array<int, string>|string $targets
*/ */
public function toUse(array|string $targets): ArchExpectation public function toUse(array|string $targets): ArchExpectation
@@ -84,9 +77,6 @@ final readonly class OppositeExpectation
), is_string($targets) ? [$targets] : $targets)); ), is_string($targets) ? [$targets] : $targets));
} }
/**
* Asserts that the given expectation target does not have the given permissions
*/
public function toHaveFileSystemPermissions(string $permissions): ArchExpectation public function toHaveFileSystemPermissions(string $permissions): ArchExpectation
{ {
/** @var Expectation<array<int, string>|string> $original */ /** @var Expectation<array<int, string>|string> $original */
@@ -100,17 +90,11 @@ final readonly class OppositeExpectation
); );
} }
/**
* Not supported.
*/
public function toHaveLineCountLessThan(): ArchExpectation public function toHaveLineCountLessThan(): ArchExpectation
{ {
throw InvalidExpectation::fromMethods(['not', 'toHaveLineCountLessThan']); throw InvalidExpectation::fromMethods(['not', 'toHaveLineCountLessThan']);
} }
/**
* Not supported.
*/
public function toHaveMethodsDocumented(): ArchExpectation public function toHaveMethodsDocumented(): ArchExpectation
{ {
/** @var Expectation<array<int, string>|string> $original */ /** @var Expectation<array<int, string>|string> $original */
@@ -130,9 +114,6 @@ final readonly class OppositeExpectation
); );
} }
/**
* Not supported.
*/
public function toHavePropertiesDocumented(): ArchExpectation public function toHavePropertiesDocumented(): ArchExpectation
{ {
/** @var Expectation<array<int, string>|string> $original */ /** @var Expectation<array<int, string>|string> $original */
@@ -153,9 +134,6 @@ final readonly class OppositeExpectation
); );
} }
/**
* Asserts that the given expectation target does not use the "declare(strict_types=1)" declaration.
*/
public function toUseStrictTypes(): ArchExpectation public function toUseStrictTypes(): ArchExpectation
{ {
/** @var Expectation<array<int, string>|string> $original */ /** @var Expectation<array<int, string>|string> $original */
@@ -169,9 +147,6 @@ final readonly class OppositeExpectation
); );
} }
/**
* Asserts that the given expectation target does not use the strict equality operator.
*/
public function toUseStrictEquality(): ArchExpectation public function toUseStrictEquality(): ArchExpectation
{ {
/** @var Expectation<array<int, string>|string> $original */ /** @var Expectation<array<int, string>|string> $original */
@@ -185,9 +160,6 @@ final readonly class OppositeExpectation
); );
} }
/**
* Asserts that the given expectation target is not final.
*/
public function toBeFinal(): ArchExpectation public function toBeFinal(): ArchExpectation
{ {
/** @var Expectation<array<int, string>|string> $original */ /** @var Expectation<array<int, string>|string> $original */
@@ -201,9 +173,6 @@ final readonly class OppositeExpectation
); );
} }
/**
* Asserts that the given expectation target is not readonly.
*/
public function toBeReadonly(): ArchExpectation public function toBeReadonly(): ArchExpectation
{ {
/** @var Expectation<array<int, string>|string> $original */ /** @var Expectation<array<int, string>|string> $original */
@@ -217,9 +186,6 @@ final readonly class OppositeExpectation
); );
} }
/**
* Asserts that the given expectation target is not trait.
*/
public function toBeTrait(): ArchExpectation public function toBeTrait(): ArchExpectation
{ {
/** @var Expectation<array<int, string>|string> $original */ /** @var Expectation<array<int, string>|string> $original */
@@ -233,17 +199,11 @@ final readonly class OppositeExpectation
); );
} }
/**
* Asserts that the given expectation targets are not traits.
*/
public function toBeTraits(): ArchExpectation public function toBeTraits(): ArchExpectation
{ {
return $this->toBeTrait(); return $this->toBeTrait();
} }
/**
* Asserts that the given expectation target is not abstract.
*/
public function toBeAbstract(): ArchExpectation public function toBeAbstract(): ArchExpectation
{ {
/** @var Expectation<array<int, string>|string> $original */ /** @var Expectation<array<int, string>|string> $original */
@@ -258,8 +218,6 @@ final readonly class OppositeExpectation
} }
/** /**
* Asserts that the given expectation target does not have a specific method.
*
* @param array<int, string>|string $method * @param array<int, string>|string $method
*/ */
public function toHaveMethod(array|string $method): ArchExpectation public function toHaveMethod(array|string $method): ArchExpectation
@@ -280,9 +238,6 @@ final readonly class OppositeExpectation
); );
} }
/**
* Asserts that the given expectation target does not have suspicious characters.
*/
public function toHaveSuspiciousCharacters(): ArchExpectation public function toHaveSuspiciousCharacters(): ArchExpectation
{ {
if (! class_exists(Spoofchecker::class)) { if (! class_exists(Spoofchecker::class)) {
@@ -303,8 +258,6 @@ final readonly class OppositeExpectation
} }
/** /**
* Asserts that the given expectation target does not have the given methods.
*
* @param array<int, string> $methods * @param array<int, string> $methods
*/ */
public function toHaveMethods(array $methods): ArchExpectation public function toHaveMethods(array $methods): ArchExpectation
@@ -313,8 +266,6 @@ final readonly class OppositeExpectation
} }
/** /**
* Asserts that the given expectation target not to have the public methods besides the given methods.
*
* @param array<int, string>|string $methods * @param array<int, string>|string $methods
*/ */
public function toHavePublicMethodsBesides(array|string $methods): ArchExpectation public function toHavePublicMethodsBesides(array|string $methods): ArchExpectation
@@ -345,22 +296,17 @@ final readonly class OppositeExpectation
}, },
$methods === [] $methods === []
? 'not to have public methods' ? 'not to have public methods'
: sprintf("not to have public methods besides '%s'", implode("', '", $methods)), : sprintf('not to have public methods besides [%s]', implode('], [', $methods)),
FileLineFinder::where(fn (string $line): bool => str_contains($line, (string) $state->contains)), FileLineFinder::where(fn (string $line): bool => str_contains($line, (string) $state->contains)),
); );
} }
/**
* Asserts that the given expectation target not to have the public methods.
*/
public function toHavePublicMethods(): ArchExpectation public function toHavePublicMethods(): ArchExpectation
{ {
return $this->toHavePublicMethodsBesides([]); return $this->toHavePublicMethodsBesides([]);
} }
/** /**
* Asserts that the given expectation target not to have the protected methods besides the given methods.
*
* @param array<int, string>|string $methods * @param array<int, string>|string $methods
*/ */
public function toHaveProtectedMethodsBesides(array|string $methods): ArchExpectation public function toHaveProtectedMethodsBesides(array|string $methods): ArchExpectation
@@ -391,22 +337,17 @@ final readonly class OppositeExpectation
}, },
$methods === [] $methods === []
? 'not to have protected methods' ? 'not to have protected methods'
: sprintf("not to have protected methods besides '%s'", implode("', '", $methods)), : sprintf('not to have protected methods besides [%s]', implode('], [', $methods)),
FileLineFinder::where(fn (string $line): bool => str_contains($line, (string) $state->contains)), FileLineFinder::where(fn (string $line): bool => str_contains($line, (string) $state->contains)),
); );
} }
/**
* Asserts that the given expectation target not to have the protected methods.
*/
public function toHaveProtectedMethods(): ArchExpectation public function toHaveProtectedMethods(): ArchExpectation
{ {
return $this->toHaveProtectedMethodsBesides([]); return $this->toHaveProtectedMethodsBesides([]);
} }
/** /**
* Asserts that the given expectation target not to have the private methods besides the given methods.
*
* @param array<int, string>|string $methods * @param array<int, string>|string $methods
*/ */
public function toHavePrivateMethodsBesides(array|string $methods): ArchExpectation public function toHavePrivateMethodsBesides(array|string $methods): ArchExpectation
@@ -437,22 +378,16 @@ final readonly class OppositeExpectation
}, },
$methods === [] $methods === []
? 'not to have private methods' ? 'not to have private methods'
: sprintf("not to have private methods besides '%s'", implode("', '", $methods)), : sprintf('not to have private methods besides [%s]', implode('], [', $methods)),
FileLineFinder::where(fn (string $line): bool => str_contains($line, (string) $state->contains)), FileLineFinder::where(fn (string $line): bool => str_contains($line, (string) $state->contains)),
); );
} }
/**
* Asserts that the given expectation target not to have the private methods.
*/
public function toHavePrivateMethods(): ArchExpectation public function toHavePrivateMethods(): ArchExpectation
{ {
return $this->toHavePrivateMethodsBesides([]); return $this->toHavePrivateMethodsBesides([]);
} }
/**
* Asserts that the given expectation target is not enum.
*/
public function toBeEnum(): ArchExpectation public function toBeEnum(): ArchExpectation
{ {
/** @var Expectation<array<int, string>|string> $original */ /** @var Expectation<array<int, string>|string> $original */
@@ -466,17 +401,11 @@ final readonly class OppositeExpectation
); );
} }
/**
* Asserts that the given expectation targets are not enums.
*/
public function toBeEnums(): ArchExpectation public function toBeEnums(): ArchExpectation
{ {
return $this->toBeEnum(); return $this->toBeEnum();
} }
/**
* Asserts that the given expectation targets is not class.
*/
public function toBeClass(): ArchExpectation public function toBeClass(): ArchExpectation
{ {
/** @var Expectation<array<int, string>|string> $original */ /** @var Expectation<array<int, string>|string> $original */
@@ -490,17 +419,11 @@ final readonly class OppositeExpectation
); );
} }
/**
* Asserts that the given expectation targets are not classes.
*/
public function toBeClasses(): ArchExpectation public function toBeClasses(): ArchExpectation
{ {
return $this->toBeClass(); return $this->toBeClass();
} }
/**
* Asserts that the given expectation target is not interface.
*/
public function toBeInterface(): ArchExpectation public function toBeInterface(): ArchExpectation
{ {
/** @var Expectation<array<int, string>|string> $original */ /** @var Expectation<array<int, string>|string> $original */
@@ -514,17 +437,11 @@ final readonly class OppositeExpectation
); );
} }
/**
* Asserts that the given expectation targets are not interfaces.
*/
public function toBeInterfaces(): ArchExpectation public function toBeInterfaces(): ArchExpectation
{ {
return $this->toBeInterface(); return $this->toBeInterface();
} }
/**
* Asserts that the given expectation target to be not subclass of the given class.
*/
public function toExtend(string $class): ArchExpectation public function toExtend(string $class): ArchExpectation
{ {
/** @var Expectation<array<int, string>|string> $original */ /** @var Expectation<array<int, string>|string> $original */
@@ -533,14 +450,11 @@ final readonly class OppositeExpectation
return Targeted::make( return Targeted::make(
$original, $original,
fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false || ! $object->reflectionClass->isSubclassOf($class), fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false || ! $object->reflectionClass->isSubclassOf($class),
sprintf("not to extend '%s'", $class), sprintf('not to extend [%s]', $class),
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
); );
} }
/**
* Asserts that the given expectation target to be not have any parent class.
*/
public function toExtendNothing(): ArchExpectation public function toExtendNothing(): ArchExpectation
{ {
/** @var Expectation<array<int, string>|string> $original */ /** @var Expectation<array<int, string>|string> $original */
@@ -554,17 +468,12 @@ final readonly class OppositeExpectation
); );
} }
/**
* Asserts that the given expectation target not to use the given trait.
*/
public function toUseTrait(string $trait): ArchExpectation public function toUseTrait(string $trait): ArchExpectation
{ {
return $this->toUseTraits($trait); return $this->toUseTraits($trait);
} }
/** /**
* Asserts that the given expectation target not to use the given traits.
*
* @param array<int, string>|string $traits * @param array<int, string>|string $traits
*/ */
public function toUseTraits(array|string $traits): ArchExpectation public function toUseTraits(array|string $traits): ArchExpectation
@@ -583,8 +492,6 @@ final readonly class OppositeExpectation
} }
/** /**
* Asserts that the given expectation target not to implement the given interfaces.
*
* @param array<int, string>|string $interfaces * @param array<int, string>|string $interfaces
*/ */
public function toImplement(array|string $interfaces): ArchExpectation public function toImplement(array|string $interfaces): ArchExpectation
@@ -602,9 +509,6 @@ final readonly class OppositeExpectation
); );
} }
/**
* Asserts that the given expectation target to not implement any interfaces.
*/
public function toImplementNothing(): ArchExpectation public function toImplementNothing(): ArchExpectation
{ {
/** @var Expectation<array<int, string>|string> $original */ /** @var Expectation<array<int, string>|string> $original */
@@ -618,17 +522,11 @@ final readonly class OppositeExpectation
); );
} }
/**
* Not supported.
*/
public function toOnlyImplement(): void public function toOnlyImplement(): void
{ {
throw InvalidExpectation::fromMethods(['not', 'toOnlyImplement']); throw InvalidExpectation::fromMethods(['not', 'toOnlyImplement']);
} }
/**
* Asserts that the given expectation target to not have the given prefix.
*/
public function toHavePrefix(string $prefix): ArchExpectation public function toHavePrefix(string $prefix): ArchExpectation
{ {
/** @var Expectation<array<int, string>|string> $original */ /** @var Expectation<array<int, string>|string> $original */
@@ -637,14 +535,11 @@ final readonly class OppositeExpectation
return Targeted::make( return Targeted::make(
$original, $original,
fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false || ! str_starts_with($object->reflectionClass->getShortName(), $prefix), fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false || ! str_starts_with($object->reflectionClass->getShortName(), $prefix),
"not to have prefix '{$prefix}'", "not to have prefix [{$prefix}]",
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
); );
} }
/**
* Asserts that the given expectation target to not have the given suffix.
*/
public function toHaveSuffix(string $suffix): ArchExpectation public function toHaveSuffix(string $suffix): ArchExpectation
{ {
/** @var Expectation<array<int, string>|string> $original */ /** @var Expectation<array<int, string>|string> $original */
@@ -653,30 +548,21 @@ final readonly class OppositeExpectation
return Targeted::make( return Targeted::make(
$original, $original,
fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false || ! str_ends_with($object->reflectionClass->getName(), $suffix), fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false || ! str_ends_with($object->reflectionClass->getName(), $suffix),
"not to have suffix '{$suffix}'", "not to have suffix [{$suffix}]",
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
); );
} }
/**
* Not supported.
*/
public function toOnlyUse(): void public function toOnlyUse(): void
{ {
throw InvalidExpectation::fromMethods(['not', 'toOnlyUse']); throw InvalidExpectation::fromMethods(['not', 'toOnlyUse']);
} }
/**
* Not supported.
*/
public function toUseNothing(): void public function toUseNothing(): void
{ {
throw InvalidExpectation::fromMethods(['not', 'toUseNothing']); throw InvalidExpectation::fromMethods(['not', 'toUseNothing']);
} }
/**
* Asserts that the given expectation dependency is not used.
*/
public function toBeUsed(): ArchExpectation public function toBeUsed(): ArchExpectation
{ {
/** @var Expectation<array<int, string>|string> $original */ /** @var Expectation<array<int, string>|string> $original */
@@ -686,8 +572,6 @@ final readonly class OppositeExpectation
} }
/** /**
* Asserts that the given expectation dependency is not used by any of the given targets.
*
* @param array<int, string>|string $targets * @param array<int, string>|string $targets
*/ */
public function toBeUsedIn(array|string $targets): ArchExpectation public function toBeUsedIn(array|string $targets): ArchExpectation
@@ -705,17 +589,11 @@ final readonly class OppositeExpectation
throw InvalidExpectation::fromMethods(['not', 'toOnlyBeUsedIn']); throw InvalidExpectation::fromMethods(['not', 'toOnlyBeUsedIn']);
} }
/**
* Asserts that the given expectation dependency is not used.
*/
public function toBeUsedInNothing(): void public function toBeUsedInNothing(): void
{ {
throw InvalidExpectation::fromMethods(['not', 'toBeUsedInNothing']); throw InvalidExpectation::fromMethods(['not', 'toBeUsedInNothing']);
} }
/**
* Asserts that the given expectation dependency is not an invokable class.
*/
public function toBeInvokable(): ArchExpectation public function toBeInvokable(): ArchExpectation
{ {
/** @var Expectation<array<int, string>|string> $original */ /** @var Expectation<array<int, string>|string> $original */
@@ -729,9 +607,6 @@ final readonly class OppositeExpectation
); );
} }
/**
* Asserts that the given expectation target not to have the given attribute.
*/
public function toHaveAttribute(string $attribute): ArchExpectation public function toHaveAttribute(string $attribute): ArchExpectation
{ {
/** @var Expectation<array<int, string>|string> $original */ /** @var Expectation<array<int, string>|string> $original */
@@ -740,14 +615,12 @@ final readonly class OppositeExpectation
return Targeted::make( return Targeted::make(
$original, $original,
fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false || $object->reflectionClass->getAttributes($attribute) === [], fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false || $object->reflectionClass->getAttributes($attribute) === [],
"to not have attribute '{$attribute}'", "to not have attribute [{$attribute}]",
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')) FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class'))
); );
} }
/** /**
* Handle dynamic method calls into the original expectation.
*
* @param array<int, mixed> $arguments * @param array<int, mixed> $arguments
* @return Expectation<TValue>|Expectation<mixed>|never * @return Expectation<TValue>|Expectation<mixed>|never
*/ */
@@ -768,8 +641,6 @@ final readonly class OppositeExpectation
} }
/** /**
* Handle dynamic properties gets into the original expectation.
*
* @return Expectation<TValue>|Expectation<mixed>|never * @return Expectation<TValue>|Expectation<mixed>|never
*/ */
public function __get(string $name): Expectation public function __get(string $name): Expectation
@@ -788,8 +659,6 @@ final readonly class OppositeExpectation
} }
/** /**
* Creates a new expectation failed exception with a nice readable message.
*
* @param array<int, mixed>|string $arguments * @param array<int, mixed>|string $arguments
*/ */
public function throwExpectationFailedException(string $name, array|string $arguments = []): never public function throwExpectationFailedException(string $name, array|string $arguments = []): never
@@ -806,25 +675,16 @@ final readonly class OppositeExpectation
)); ));
} }
/**
* Asserts that the given expectation target does not have a constructor method.
*/
public function toHaveConstructor(): ArchExpectation public function toHaveConstructor(): ArchExpectation
{ {
return $this->toHaveMethod('__construct'); return $this->toHaveMethod('__construct');
} }
/**
* Asserts that the given expectation target does not have a destructor method.
*/
public function toHaveDestructor(): ArchExpectation public function toHaveDestructor(): ArchExpectation
{ {
return $this->toHaveMethod('__destruct'); return $this->toHaveMethod('__destruct');
} }
/**
* Asserts that the given expectation target is not a backed enum of given type.
*/
private function toBeBackedEnum(string $backingType): ArchExpectation private function toBeBackedEnum(string $backingType): ArchExpectation
{ {
/** @var Expectation<array<int, string>|string> $original */ /** @var Expectation<array<int, string>|string> $original */
@@ -841,33 +701,21 @@ final readonly class OppositeExpectation
); );
} }
/**
* Asserts that the given expectation targets are not string backed enums.
*/
public function toBeStringBackedEnums(): ArchExpectation public function toBeStringBackedEnums(): ArchExpectation
{ {
return $this->toBeStringBackedEnum(); return $this->toBeStringBackedEnum();
} }
/**
* Asserts that the given expectation targets are not int backed enums.
*/
public function toBeIntBackedEnums(): ArchExpectation public function toBeIntBackedEnums(): ArchExpectation
{ {
return $this->toBeIntBackedEnum(); return $this->toBeIntBackedEnum();
} }
/**
* Asserts that the given expectation target is not a string backed enum.
*/
public function toBeStringBackedEnum(): ArchExpectation public function toBeStringBackedEnum(): ArchExpectation
{ {
return $this->toBeBackedEnum('string'); return $this->toBeBackedEnum('string');
} }
/**
* Asserts that the given expectation target is not an int backed enum.
*/
public function toBeIntBackedEnum(): ArchExpectation public function toBeIntBackedEnum(): ArchExpectation
{ {
return $this->toBeBackedEnum('int'); return $this->toBeBackedEnum('int');
@@ -8,24 +8,12 @@ use Pest\Support\HigherOrderMessageCollection;
trait HigherOrderable trait HigherOrderable
{ {
/**
* The higher order messages that are chainable.
*/
public HigherOrderMessageCollection $chains; public HigherOrderMessageCollection $chains;
/**
* The higher order messages that are "factory" proxyable.
*/
public HigherOrderMessageCollection $factoryProxies; public HigherOrderMessageCollection $factoryProxies;
/**
* The higher order messages that are proxyable.
*/
public HigherOrderMessageCollection $proxies; public HigherOrderMessageCollection $proxies;
/**
* Boot the higher order properties.
*/
private function bootHigherOrderable(): void private function bootHigherOrderable(): void
{ {
$this->chains = new HigherOrderMessageCollection; $this->chains = new HigherOrderMessageCollection;
+13 -31
View File
@@ -9,6 +9,7 @@ use Pest\Concerns;
use Pest\Contracts\HasPrintableTestCaseName; use Pest\Contracts\HasPrintableTestCaseName;
use Pest\Evaluators\Attributes; use Pest\Evaluators\Attributes;
use Pest\Exceptions\DatasetMissing; use Pest\Exceptions\DatasetMissing;
use Pest\Exceptions\InvalidTestClassName;
use Pest\Exceptions\ShouldNotHappen; use Pest\Exceptions\ShouldNotHappen;
use Pest\Exceptions\TestAlreadyExist; use Pest\Exceptions\TestAlreadyExist;
use Pest\Exceptions\TestClosureMustNotBeStatic; use Pest\Exceptions\TestClosureMustNotBeStatic;
@@ -29,29 +30,21 @@ final class TestCaseFactory
use HigherOrderable; use HigherOrderable;
/** /**
* The list of attributes.
*
* @var array<int, Attribute> * @var array<int, Attribute>
*/ */
public array $attributes = []; public array $attributes = [];
/** /**
* The FQN of the Test Case class.
*
* @var class-string * @var class-string
*/ */
public string $class = TestCase::class; public string $class = TestCase::class;
/** /**
* The list of class methods.
*
* @var array<string, TestCaseMethodFactory> * @var array<string, TestCaseMethodFactory>
*/ */
public array $methods = []; public array $methods = [];
/** /**
* The list of class traits.
*
* @var array <int, class-string> * @var array <int, class-string>
*/ */
public array $traits = [ public array $traits = [
@@ -59,14 +52,8 @@ final class TestCaseFactory
Concerns\Expectable::class, Concerns\Expectable::class,
]; ];
/**
* The namespace for the test case, overrides the path-based namespace when set.
*/
public ?string $namespace = null; public ?string $namespace = null;
/**
* Creates a new Factory instance.
*/
public function __construct( public function __construct(
public string $filename public string $filename
) { ) {
@@ -83,14 +70,11 @@ final class TestCaseFactory
} }
/** /**
* Creates a Test Case class using a runtime evaluate.
*
* @param array<string, TestCaseMethodFactory> $methods * @param array<string, TestCaseMethodFactory> $methods
*/ */
public function evaluate(string $filename, array $methods): void public function evaluate(string $filename, array $methods): void
{ {
if ('\\' === DIRECTORY_SEPARATOR) { if ('\\' === DIRECTORY_SEPARATOR) {
// In case Windows, strtolower drive name, like in UsesCall.
$filename = (string) preg_replace_callback('~^(?P<drive>[a-z]+:\\\)~i', static fn (array $match): string => strtolower($match['drive']), $filename); $filename = (string) preg_replace_callback('~^(?P<drive>[a-z]+:\\\)~i', static fn (array $match): string => strtolower($match['drive']), $filename);
} }
@@ -113,11 +97,8 @@ final class TestCaseFactory
$relativePath = str_replace(DIRECTORY_SEPARATOR, '\\', $relativePath); $relativePath = str_replace(DIRECTORY_SEPARATOR, '\\', $relativePath);
// Strip out any %-encoded octets.
$relativePath = (string) preg_replace('|%[a-fA-F0-9][a-fA-F0-9]|', '', $relativePath); $relativePath = (string) preg_replace('|%[a-fA-F0-9][a-fA-F0-9]|', '', $relativePath);
// Remove escaped quote sequences (maintain namespace)
$relativePath = str_replace(array_map(fn (string $quote): string => sprintf('\\%s', $quote), ['\'', '"']), '', $relativePath); $relativePath = str_replace(array_map(fn (string $quote): string => sprintf('\\%s', $quote), ['\'', '"']), '', $relativePath);
// Limit to Unicode letters and numbers.
$relativePath = (string) preg_replace('/[^\p{L}\p{N}\\\\]/u', '', $relativePath); $relativePath = (string) preg_replace('/[^\p{L}\p{N}\\\\]/u', '', $relativePath);
$classFQN = 'P\\'.$relativePath; $classFQN = 'P\\'.$relativePath;
@@ -138,6 +119,16 @@ final class TestCaseFactory
if (trim($className) === '') { if (trim($className) === '') {
$className = 'InvalidTestName'.Str::random(); $className = 'InvalidTestName'.Str::random();
} elseif (! Str::isValidClassName($className)) {
throw InvalidTestClassName::fromClassName($this->filename, $className);
}
if ($this->namespace === null) {
foreach ($partsFQN as $partFQN) {
if (! Str::isValidIdentifier($partFQN)) {
throw InvalidTestClassName::fromNamespace($this->filename, $namespace, $partFQN);
}
}
} }
$this->attributes = [ $this->attributes = [
@@ -179,16 +170,13 @@ final class TestCaseFactory
eval($classCode); eval($classCode);
} catch (ParseError $caught) { } catch (ParseError $caught) {
throw new RuntimeException(sprintf( throw new RuntimeException(sprintf(
"Unable to create test case for test file at %s. \n %s", "Unable to create test case for test file at [%s]. \n %s",
$filename, $filename,
$classCode $classCode
), 1, $caught); ), 1, $caught);
} }
} }
/**
* Adds the given Method to the Test Case.
*/
public function addMethod(TestCaseMethodFactory $method): void public function addMethod(TestCaseMethodFactory $method): void
{ {
if ($method->description === null) { if ($method->description === null) {
@@ -222,9 +210,6 @@ final class TestCaseFactory
$this->methods[$method->description] = $method; $this->methods[$method->description] = $method;
} }
/**
* Checks if a test case has a method.
*/
public function hasMethod(string $methodName): bool public function hasMethod(string $methodName): bool
{ {
foreach ($this->methods as $method) { foreach ($this->methods as $method) {
@@ -240,9 +225,6 @@ final class TestCaseFactory
return false; return false;
} }
/**
* Gets a Method by the given name.
*/
public function getMethod(string $methodName): TestCaseMethodFactory public function getMethod(string $methodName): TestCaseMethodFactory
{ {
foreach ($this->methods as $method) { foreach ($this->methods as $method) {
@@ -255,6 +237,6 @@ final class TestCaseFactory
} }
} }
throw ShouldNotHappen::fromMessage(sprintf('Method %s not found.', $methodName)); throw ShouldNotHappen::fromMessage(sprintf('Method [%s] not found.', $methodName));
} }
} }
-51
View File
@@ -27,84 +27,54 @@ final class TestCaseMethodFactory
use HigherOrderable; use HigherOrderable;
/** /**
* The list of attributes.
*
* @var array<int, Attribute> * @var array<int, Attribute>
*/ */
public array $attributes = []; public array $attributes = [];
/** /**
* The test's describing, if any.
*
* @var array<int, Description> * @var array<int, Description>
*/ */
public array $describing = []; public array $describing = [];
/**
* The test's description, if any.
*/
public ?string $description = null; public ?string $description = null;
/**
* The test's number of repetitions.
*/
public int $repetitions = 1; public int $repetitions = 1;
/**
* The test's number of flaky retry tries.
*/
public ?int $flakyTries = null; public ?int $flakyTries = null;
/**
* Determines if the test is a "todo".
*/
public bool $todo = false; public bool $todo = false;
/** /**
* The associated issue numbers.
*
* @var array<int, int> * @var array<int, int>
*/ */
public array $issues = []; public array $issues = [];
/** /**
* The test assignees.
*
* @var array<int, string> * @var array<int, string>
*/ */
public array $assignees = []; public array $assignees = [];
/** /**
* The associated PRs numbers.
*
* @var array<int, int> * @var array<int, int>
*/ */
public array $prs = []; public array $prs = [];
/** /**
* The test's notes.
*
* @var array<int, string> * @var array<int, string>
*/ */
public array $notes = []; public array $notes = [];
/** /**
* The test's datasets.
*
* @var array<Closure|iterable<int|string, mixed>|string> * @var array<Closure|iterable<int|string, mixed>|string>
*/ */
public array $datasets = []; public array $datasets = [];
/** /**
* The test's dependencies.
*
* @var array<int, string> * @var array<int, string>
*/ */
public array $depends = []; public array $depends = [];
/** /**
* The test's groups.
*
* @var array<int, string> * @var array<int, string>
*/ */
public array $groups = []; public array $groups = [];
@@ -114,9 +84,6 @@ final class TestCaseMethodFactory
*/ */
public bool $__ran = false; public bool $__ran = false;
/**
* Creates a new test case method factory instance.
*/
public function __construct( public function __construct(
public string $filename, public string $filename,
public ?Closure $closure, public ?Closure $closure,
@@ -128,9 +95,6 @@ final class TestCaseMethodFactory
$this->bootHigherOrderable(); $this->bootHigherOrderable();
} }
/**
* Sets the test's hooks, and runs any proxy to the test case.
*/
public function setUp(TestCase $concrete): void public function setUp(TestCase $concrete): void
{ {
$concrete::flush(); // @phpstan-ignore-line $concrete::flush(); // @phpstan-ignore-line
@@ -146,17 +110,11 @@ final class TestCaseMethodFactory
$this->factoryProxies->proxy($concrete); $this->factoryProxies->proxy($concrete);
} }
/**
* Flushes the test case.
*/
public function tearDown(TestCase $concrete): void public function tearDown(TestCase $concrete): void
{ {
$concrete::flush(); // @phpstan-ignore-line $concrete::flush(); // @phpstan-ignore-line
} }
/**
* Creates the test's closure.
*/
public function getClosure(): Closure public function getClosure(): Closure
{ {
$closure = $this->closure; $closure = $this->closure;
@@ -178,17 +136,11 @@ final class TestCaseMethodFactory
}; };
} }
/**
* Determine if the test case will receive argument input from Pest, or not.
*/
public function receivesArguments(): bool public function receivesArguments(): bool
{ {
return $this->datasets !== [] || $this->depends !== [] || $this->repetitions > 1; return $this->datasets !== [] || $this->depends !== [] || $this->repetitions > 1;
} }
/**
* Creates a PHPUnit method as a string ready for evaluation.
*/
public function buildForEvaluation(): string public function buildForEvaluation(): string
{ {
if ($this->description === null) { if ($this->description === null) {
@@ -248,9 +200,6 @@ final class TestCaseMethodFactory
PHP; PHP;
} }
/**
* Creates a PHPUnit Data Provider as a string ready for evaluation.
*/
private function buildDatasetForEvaluation(string $methodName, string $dataProviderName): string private function buildDatasetForEvaluation(string $methodName, string $dataProviderName): string
{ {
$datasets = $this->datasets; $datasets = $this->datasets;
-45
View File
@@ -27,8 +27,6 @@ use PHPUnit\Framework\TestCase;
if (! function_exists('expect')) { if (! function_exists('expect')) {
/** /**
* Creates a new expectation.
*
* @template TValue * @template TValue
* *
* @param TValue|null $value * @param TValue|null $value
@@ -41,9 +39,6 @@ if (! function_exists('expect')) {
} }
if (! function_exists('beforeAll')) { if (! function_exists('beforeAll')) {
/**
* Runs the given closure before all tests in the current file.
*/
function beforeAll(Closure $closure): void function beforeAll(Closure $closure): void
{ {
if (DescribeCall::describing() !== []) { if (DescribeCall::describing() !== []) {
@@ -58,8 +53,6 @@ if (! function_exists('beforeAll')) {
if (! function_exists('beforeEach')) { if (! function_exists('beforeEach')) {
/** /**
* Runs the given closure before each test in the current file.
*
* @param-closure-this TestCall $closure * @param-closure-this TestCall $closure
*/ */
function beforeEach(?Closure $closure = null): BeforeEachCall function beforeEach(?Closure $closure = null): BeforeEachCall
@@ -72,8 +65,6 @@ if (! function_exists('beforeEach')) {
if (! function_exists('dataset')) { if (! function_exists('dataset')) {
/** /**
* Registers the given dataset.
*
* @param Closure|iterable<int|string, mixed> $dataset * @param Closure|iterable<int|string, mixed> $dataset
*/ */
function dataset(string $name, Closure|iterable $dataset): void function dataset(string $name, Closure|iterable $dataset): void
@@ -85,11 +76,6 @@ if (! function_exists('dataset')) {
} }
if (! function_exists('describe')) { if (! function_exists('describe')) {
/**
* Adds the given closure as a group of tests. The first argument
* is the group description; the second argument is a closure
* that contains the group tests.
*/
function describe(string $description, Closure $tests): DescribeCall function describe(string $description, Closure $tests): DescribeCall
{ {
$filename = Backtrace::testFile(); $filename = Backtrace::testFile();
@@ -100,9 +86,6 @@ if (! function_exists('describe')) {
if (! function_exists('uses')) { if (! function_exists('uses')) {
/** /**
* The uses function binds the given
* arguments to test closures.
*
* @param class-string ...$classAndTraits * @param class-string ...$classAndTraits
*/ */
function uses(string ...$classAndTraits): UsesCall function uses(string ...$classAndTraits): UsesCall
@@ -114,9 +97,6 @@ if (! function_exists('uses')) {
} }
if (! function_exists('pest')) { if (! function_exists('pest')) {
/**
* Creates a new Pest configuration instance.
*/
function pest(): Configuration function pest(): Configuration
{ {
return new Configuration(Backtrace::testFile()); return new Configuration(Backtrace::testFile());
@@ -125,10 +105,6 @@ if (! function_exists('pest')) {
if (! function_exists('test')) { if (! function_exists('test')) {
/** /**
* Adds the given closure as a test. The first argument
* is the test description; the second argument is
* a closure that contains the test expectations.
*
* @param-closure-this TestCall $closure * @param-closure-this TestCall $closure
* *
* @return ($description is string ? TestCall : HigherOrderTapProxy|TestCall) * @return ($description is string ? TestCall : HigherOrderTapProxy|TestCall)
@@ -147,10 +123,6 @@ if (! function_exists('test')) {
if (! function_exists('it')) { if (! function_exists('it')) {
/** /**
* Adds the given closure as a test. The first argument
* is the test description; the second argument is
* a closure that contains the test expectations.
*
* @param-closure-this TestCall $closure * @param-closure-this TestCall $closure
*/ */
function it(string $description, ?Closure $closure = null): TestCall function it(string $description, ?Closure $closure = null): TestCall
@@ -162,9 +134,6 @@ if (! function_exists('it')) {
} }
if (! function_exists('todo')) { if (! function_exists('todo')) {
/**
* Creates a new test that is marked as "todo".
*/
function todo(string $description): TestCall function todo(string $description): TestCall
{ {
return test($description)->todo(); return test($description)->todo();
@@ -173,8 +142,6 @@ if (! function_exists('todo')) {
if (! function_exists('afterEach')) { if (! function_exists('afterEach')) {
/** /**
* Runs the given closure after each test in the current file.
*
* @param-closure-this TestCall $closure * @param-closure-this TestCall $closure
*/ */
function afterEach(?Closure $closure = null): AfterEachCall function afterEach(?Closure $closure = null): AfterEachCall
@@ -186,9 +153,6 @@ if (! function_exists('afterEach')) {
} }
if (! function_exists('afterAll')) { if (! function_exists('afterAll')) {
/**
* Runs the given closure after all tests in the current file.
*/
function afterAll(Closure $closure): void function afterAll(Closure $closure): void
{ {
if (DescribeCall::describing() !== []) { if (DescribeCall::describing() !== []) {
@@ -203,8 +167,6 @@ if (! function_exists('afterAll')) {
if (! function_exists('covers')) { if (! function_exists('covers')) {
/** /**
* Specifies which classes, or functions, a test case covers.
*
* @param array<int, string>|string $classesOrFunctions * @param array<int, string>|string $classesOrFunctions
*/ */
function covers(array|string ...$classesOrFunctions): void function covers(array|string ...$classesOrFunctions): void
@@ -232,8 +194,6 @@ if (! function_exists('covers')) {
if (! function_exists('mutates')) { if (! function_exists('mutates')) {
/** /**
* Specifies which classes, enums, or traits a test case mutates.
*
* @param array<int, string>|string $targets * @param array<int, string>|string $targets
*/ */
function mutates(array|string ...$targets): void function mutates(array|string ...$targets): void
@@ -266,9 +226,6 @@ if (! function_exists('mutates')) {
} }
if (! function_exists('fixture')) { if (! function_exists('fixture')) {
/**
* Returns the absolute path to a fixture file.
*/
function fixture(string $file): string function fixture(string $file): string
{ {
$file = implode(DIRECTORY_SEPARATOR, [ $file = implode(DIRECTORY_SEPARATOR, [
@@ -292,8 +249,6 @@ if (! function_exists('fixture')) {
if (! function_exists('visit')) { if (! function_exists('visit')) {
/** /**
* Browse to the given URL.
*
* @template TUrl of array<int, string>|string * @template TUrl of array<int, string>|string
* *
* @param TUrl $url * @param TUrl $url
-23
View File
@@ -29,14 +29,9 @@ use Whoops\Exception\Inspector;
*/ */
final class Kernel final class Kernel
{ {
/**
* Either the kernel is terminated or not.
*/
private bool $terminated = false; private bool $terminated = false;
/** /**
* The Kernel bootstrappers.
*
* @var array<int, class-string> * @var array<int, class-string>
*/ */
private const array BOOTSTRAPPERS = [ private const array BOOTSTRAPPERS = [
@@ -51,10 +46,6 @@ final class Kernel
]; ];
/** /**
* The Kernel restarters — resolved and invoked from `bin/pest`
* before any other Pest class is touched, so the list is exposed
* on the Kernel rather than driven from `bin/pest` directly.
*
* @var array<int, class-string<Contracts\Restarter>> * @var array<int, class-string<Contracts\Restarter>>
*/ */
public const array RESTARTERS = [ public const array RESTARTERS = [
@@ -62,14 +53,8 @@ final class Kernel
Restarters\PcovRestarter::class, Restarters\PcovRestarter::class,
]; ];
/**
* Creates a new Kernel instance.
*/
public function __construct(private readonly Application $application, private readonly OutputInterface $output) {} public function __construct(private readonly Application $application, private readonly OutputInterface $output) {}
/**
* Boots the Kernel.
*/
public static function boot(TestSuite $testSuite, InputInterface $input, OutputInterface $output): self public static function boot(TestSuite $testSuite, InputInterface $input, OutputInterface $output): self
{ {
$container = Container::getInstance(); $container = Container::getInstance();
@@ -102,8 +87,6 @@ final class Kernel
} }
/** /**
* Runs the application, and returns the exit code.
*
* @param array<int, string> $originalArguments * @param array<int, string> $originalArguments
* @param array<int, string> $arguments * @param array<int, string> $arguments
*/ */
@@ -135,9 +118,6 @@ final class Kernel
return $result; return $result;
} }
/**
* Terminate the Kernel.
*/
public function terminate(): void public function terminate(): void
{ {
if ($this->terminated) { if ($this->terminated) {
@@ -155,9 +135,6 @@ final class Kernel
CallsTerminable::execute(); CallsTerminable::execute();
} }
/**
* Shutdowns unexpectedly the Kernel.
*/
public function shutdown(): void public function shutdown(): void
{ {
$this->terminate(); $this->terminate();
+1 -25
View File
@@ -10,23 +10,14 @@ use Symfony\Component\Console\Output\OutputInterface;
final class KernelDump final class KernelDump
{ {
/**
* The output buffer, if any.
*/
private string $buffer = ''; private string $buffer = '';
/**
* Creates a new Kernel Dump instance.
*/
public function __construct( public function __construct(
private readonly OutputInterface $output, private readonly OutputInterface $output,
) { ) {
// ... //
} }
/**
* Enable the output buffering.
*/
public function enable(): void public function enable(): void
{ {
if (class_exists(Execution::class) && Execution::running()) { if (class_exists(Execution::class) && Execution::running()) {
@@ -40,9 +31,6 @@ final class KernelDump
}); });
} }
/**
* Disable the output buffering.
*/
public function disable(): void public function disable(): void
{ {
@ob_clean(); @ob_clean();
@@ -52,17 +40,11 @@ final class KernelDump
} }
} }
/**
* Terminate the output buffering.
*/
public function terminate(): void public function terminate(): void
{ {
$this->disable(); $this->disable();
} }
/**
* Flushes the buffer.
*/
private function flush(): void private function flush(): void
{ {
View::renderUsing($this->output); View::renderUsing($this->output);
@@ -103,17 +85,11 @@ final class KernelDump
$this->buffer = ''; $this->buffer = '';
} }
/**
* Checks if the given output contains an opening headline.
*/
private function isOpeningHeadline(string $output): bool private function isOpeningHeadline(string $output): bool
{ {
return str_contains($output, 'by Sebastian Bergmann and contributors.'); return str_contains($output, 'by Sebastian Bergmann and contributors.');
} }
/**
* Checks if the given output contains an opening headline.
*/
private function isInternalError(string $output): bool private function isInternalError(string $output): bool
{ {
return str_contains($output, 'An error occurred inside PHPUnit.'); return str_contains($output, 'An error occurred inside PHPUnit.');
-57
View File
@@ -25,28 +25,16 @@ use PHPUnit\TestRunner\TestResult\TestResult as PhpUnitTestResult;
*/ */
final readonly class Converter final readonly class Converter
{ {
/**
* The prefix for the test suite name.
*/
private const string PREFIX = 'P\\'; private const string PREFIX = 'P\\';
/**
* The state generator.
*/
private StateGenerator $stateGenerator; private StateGenerator $stateGenerator;
/**
* Creates a new instance of the Converter.
*/
public function __construct( public function __construct(
private string $rootPath, private string $rootPath,
) { ) {
$this->stateGenerator = new StateGenerator; $this->stateGenerator = new StateGenerator;
} }
/**
* Gets the test case method name.
*/
public function getTestCaseMethodName(Test $test): string public function getTestCaseMethodName(Test $test): string
{ {
if (! $test instanceof TestMethod) { if (! $test instanceof TestMethod) {
@@ -56,9 +44,6 @@ final readonly class Converter
return $test->testDox()->prettifiedMethodName(); return $test->testDox()->prettifiedMethodName();
} }
/**
* Gets the test case location.
*/
public function getTestCaseLocation(Test $test): string public function getTestCaseLocation(Test $test): string
{ {
if (! $test instanceof TestMethod) { if (! $test instanceof TestMethod) {
@@ -68,15 +53,11 @@ final readonly class Converter
$path = $test->testDox()->prettifiedClassName(); $path = $test->testDox()->prettifiedClassName();
$relativePath = $this->toRelativePath($path); $relativePath = $this->toRelativePath($path);
// TODO: Get the description without the dataset.
$description = $test->testDox()->prettifiedMethodName(); $description = $test->testDox()->prettifiedMethodName();
return "$relativePath::$description"; return "$relativePath::$description";
} }
/**
* Gets the exception message.
*/
public function getExceptionMessage(Throwable $throwable): string public function getExceptionMessage(Throwable $throwable): string
{ {
if (is_a($throwable->className(), FrameworkException::class, true)) { if (is_a($throwable->className(), FrameworkException::class, true)) {
@@ -93,9 +74,6 @@ final readonly class Converter
return $buffer; return $buffer;
} }
/**
* Gets the exception details.
*/
public function getExceptionDetails(Throwable $throwable): string public function getExceptionDetails(Throwable $throwable): string
{ {
$buffer = $this->getStackTrace($throwable); $buffer = $this->getStackTrace($throwable);
@@ -113,26 +91,19 @@ final readonly class Converter
return $buffer; return $buffer;
} }
/**
* Gets the stack trace.
*/
public function getStackTrace(Throwable $throwable): string public function getStackTrace(Throwable $throwable): string
{ {
$stackTrace = $throwable->stackTrace(); $stackTrace = $throwable->stackTrace();
// Split stacktrace per frame.
$frames = explode("\n", $stackTrace); $frames = explode("\n", $stackTrace);
// Remove empty lines
$frames = array_filter($frames); $frames = array_filter($frames);
// clean the paths of each frame.
$frames = array_map( $frames = array_map(
$this->toRelativePath(...), $this->toRelativePath(...),
$frames $frames
); );
// Format stacktrace as `at <path>`
$frames = array_map( $frames = array_map(
fn (string $frame): string => "at $frame", fn (string $frame): string => "at $frame",
$frames $frames
@@ -141,9 +112,6 @@ final readonly class Converter
return implode("\n", $frames); return implode("\n", $frames);
} }
/**
* Gets the test suite name.
*/
public function getTestSuiteName(TestSuite $testSuite): string public function getTestSuiteName(TestSuite $testSuite): string
{ {
if ($testSuite instanceof TestSuiteForTestMethodWithDataProvider) { if ($testSuite instanceof TestSuiteForTestMethodWithDataProvider) {
@@ -162,17 +130,11 @@ final readonly class Converter
return Str::after($name, self::PREFIX); return Str::after($name, self::PREFIX);
} }
/**
* Gets the trimmed test class name.
*/
public function getTrimmedTestClassName(TestMethod $test): string public function getTrimmedTestClassName(TestMethod $test): string
{ {
return Str::after($test->className(), self::PREFIX); return Str::after($test->className(), self::PREFIX);
} }
/**
* Gets the test suite location.
*/
public function getTestSuiteLocation(TestSuite $testSuite): ?string public function getTestSuiteLocation(TestSuite $testSuite): ?string
{ {
$firstTest = $this->getFirstTest($testSuite); $firstTest = $this->getFirstTest($testSuite);
@@ -191,22 +153,15 @@ final readonly class Converter
return $classRelativePath; return $classRelativePath;
} }
/**
* Gets the prettified test method name without dataset-related suffix.
*/
private function getTestMethodNameWithoutDatasetSuffix(TestMethod $testMethod): string private function getTestMethodNameWithoutDatasetSuffix(TestMethod $testMethod): string
{ {
return Str::beforeLast($testMethod->testDox()->prettifiedMethodName(), ' with data set '); return Str::beforeLast($testMethod->testDox()->prettifiedMethodName(), ' with data set ');
} }
/**
* Gets the first test from the test suite.
*/
private function getFirstTest(TestSuite $testSuite): ?TestMethod private function getFirstTest(TestSuite $testSuite): ?TestMethod
{ {
$tests = $testSuite->tests()->asArray(); $tests = $testSuite->tests()->asArray();
// TODO: figure out how to get the file path without a test being there.
if ($tests === []) { if ($tests === []) {
return null; return null;
} }
@@ -219,26 +174,16 @@ final readonly class Converter
return $firstTest; return $firstTest;
} }
/**
* Gets the test suite size.
*/
public function getTestSuiteSize(TestSuite $testSuite): int public function getTestSuiteSize(TestSuite $testSuite): int
{ {
return $testSuite->count(); return $testSuite->count();
} }
/**
* Transforms the given path in relative path.
*/
private function toRelativePath(string $path): string private function toRelativePath(string $path): string
{ {
// Remove cwd from the path.
return str_replace("$this->rootPath".DIRECTORY_SEPARATOR, '', $path); return str_replace("$this->rootPath".DIRECTORY_SEPARATOR, '', $path);
} }
/**
* Get the test result.
*/
public function getStateFromResult(PhpUnitTestResult $result): State public function getStateFromResult(PhpUnitTestResult $result): State
{ {
$events = [ $events = [
@@ -253,11 +198,9 @@ final readonly class Converter
foreach ($events as $event) { foreach ($events as $event) {
if ($event instanceof AfterLastTestMethodErrored) { if ($event instanceof AfterLastTestMethodErrored) {
// PHPUnit's collector does not count these towards `numberOfTestsRun`...
continue; continue;
} }
if ($event instanceof AfterLastTestMethodFailed) { if ($event instanceof AfterLastTestMethodFailed) {
// PHPUnit's collector does not count these towards `numberOfTestsRun`...
continue; continue;
} }
if ($event instanceof BeforeFirstTestMethodErrored || $event instanceof BeforeFirstTestMethodFailed) { if ($event instanceof BeforeFirstTestMethodErrored || $event instanceof BeforeFirstTestMethodFailed) {
-3
View File
@@ -9,9 +9,6 @@ namespace Pest\Logging\TeamCity;
*/ */
final class ServiceMessage final class ServiceMessage
{ {
/**
* The flow ID.
*/
private static ?int $flowId = null; private static ?int $flowId = null;
/** /**
@@ -11,14 +11,8 @@ use Pest\Logging\TeamCity\TeamCityLogger;
*/ */
abstract class Subscriber // @pest-arch-ignore-line abstract class Subscriber // @pest-arch-ignore-line
{ {
/**
* Creates a new Subscriber instance.
*/
public function __construct(private readonly TeamCityLogger $logger) {} public function __construct(private readonly TeamCityLogger $logger) {}
/**
* Creates a new TeamCityLogger instance.
*/
final protected function logger(): TeamCityLogger // @pest-arch-ignore-line final protected function logger(): TeamCityLogger // @pest-arch-ignore-line
{ {
return $this->logger; return $this->logger;
@@ -15,7 +15,7 @@ final class TestSkippedSubscriber extends Subscriber implements SkippedSubscribe
public function notify(Skipped $event): void public function notify(Skipped $event): void
{ {
if ($event->message() === '__TODO__') { if ($event->message() === '__TODO__') {
return; // "todo" tests are reported in the summary, not as ignored tests... return;
} }
$this->logger()->testSkipped($event); $this->logger()->testSkipped($event);
-18
View File
@@ -43,14 +43,8 @@ use Symfony\Component\Console\Output\OutputInterface;
*/ */
final class TeamCityLogger final class TeamCityLogger
{ {
/**
* The current time.
*/
private ?HRTime $time = null; private ?HRTime $time = null;
/**
* Indicates if the summary test count has been printed.
*/
private bool $isSummaryTestCountPrinted = false; private bool $isSummaryTestCountPrinted = false;
/** /**
@@ -129,10 +123,6 @@ final class TeamCityLogger
}); });
} }
/**
* This will trigger in the following scenarios
* - When an exception is thrown
*/
public function testErrored(Errored $event): void public function testErrored(Errored $event): void
{ {
$this->whenFirstEventForTest($event->test(), function () use ($event): void { $this->whenFirstEventForTest($event->test(), function () use ($event): void {
@@ -150,10 +140,6 @@ final class TeamCityLogger
}); });
} }
/**
* This will trigger in the following scenarios
* - When an assertion fails
*/
public function testFailed(Failed $event): void public function testFailed(Failed $event): void
{ {
$this->whenFirstEventForTest($event->test(), function () use ($event): void { $this->whenFirstEventForTest($event->test(), function () use ($event): void {
@@ -182,10 +168,6 @@ final class TeamCityLogger
}); });
} }
/**
* This will trigger in the following scenarios
* - When no assertions in a test
*/
public function testConsideredRisky(ConsideredRisky $event): void public function testConsideredRisky(ConsideredRisky $event): void
{ {
$this->whenFirstEventForTest($event->test(), function () use ($event): void { $this->whenFirstEventForTest($event->test(), function () use ($event): void {

Some files were not shown because too many files have changed in this diff Show More