diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index 5b9b0df5..ae45ea68 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -2,10 +2,15 @@ name: Static Analysis on: push: + branches: [4.x] pull_request: schedule: - cron: '0 0 * * *' +concurrency: + group: static-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + jobs: static: if: github.event_name != 'schedule' || github.repository == 'pestphp/pest' @@ -19,7 +24,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -29,8 +34,22 @@ jobs: coverage: none extensions: sockets + - name: Get Composer cache directory + id: composer-cache + shell: bash + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + + - name: Cache Composer dependencies + uses: actions/cache@v4 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: static-php-8.3-${{ matrix.dependency-version }}-composer-${{ hashFiles('**/composer.json') }} + restore-keys: | + static-php-8.3-${{ matrix.dependency-version }}-composer- + static-php-8.3-composer- + - name: Install Dependencies - run: composer update --prefer-stable --no-interaction --no-progress --ansi + run: composer update --${{ matrix.dependency-version }} --no-interaction --no-progress --ansi - name: Profanity Check run: composer test:profanity diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index bbc468ab..1f74b76e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -2,8 +2,13 @@ name: Tests on: push: + branches: [4.x] pull_request: +concurrency: + group: tests-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + jobs: tests: if: github.event_name != 'schedule' || github.repository == 'pestphp/pest' @@ -13,15 +18,18 @@ jobs: fail-fast: true matrix: os: [ubuntu-latest, macos-latest] # windows-latest - symfony: ['7.3'] - php: ['8.3', '8.4'] + symfony: ['7.4', '8.0'] + php: ['8.3', '8.4', '8.5'] dependency_version: [prefer-stable] + exclude: + - php: '8.3' + symfony: '8.0' name: PHP ${{ matrix.php }} - Symfony ^${{ matrix.symfony }} - ${{ matrix.os }} - ${{ matrix.dependency_version }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -31,6 +39,20 @@ jobs: coverage: none extensions: sockets + - name: Get Composer cache directory + id: composer-cache + shell: bash + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + + - name: Cache Composer dependencies + uses: actions/cache@v4 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ matrix.os }}-php-${{ matrix.php }}-symfony-${{ matrix.symfony }}-composer-${{ hashFiles('**/composer.json') }} + restore-keys: | + ${{ matrix.os }}-php-${{ matrix.php }}-symfony-${{ matrix.symfony }}-composer- + ${{ matrix.os }}-php-${{ matrix.php }}-composer- + - name: Setup Problem Matches run: | echo "::add-matcher::${{ runner.tool_cache }}/php.json" diff --git a/Makefile b/Makefile deleted file mode 100644 index ec4faa84..00000000 --- a/Makefile +++ /dev/null @@ -1,14 +0,0 @@ -# Well documented Makefiles -DEFAULT_GOAL := help -help: - @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z0-9_-]+:.*?##/ { printf " \033[36m%-40s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) - -build: ## Build all docker images. Specify the command e.g. via make build ARGS="--build-arg PHP=8.2" - docker compose build $(ARGS) - -##@ [Application] -install: ## Install the composer dependencies - docker compose run --rm composer install - -test: ## Run the tests - docker compose run --rm composer test diff --git a/README.md b/README.md index 9c83a3a4..af33fd43 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ Total Downloads Latest Version License + Why PHP in 2026

@@ -30,25 +31,23 @@ We cannot thank our sponsors enough for their incredible support in funding Pest ### Platinum Sponsors -- **[Laracasts](https://laracasts.com/?ref=pestphp)** -- **[NativePHP](https://nativephp.com/mobile?ref=pestphp.com)** +- **[CodeRabbit](https://coderabbit.ai/?ref=pestphp)** +- **[Mailtrap](https://l.rw.rw/pestphp)** +- **[SerpApi](https://serpapi.com/?ref=nunomaduro)** +- **[Tighten](https://tighten.com/?ref=nunomaduro)** +- **[Redberry](https://redberry.international/laravel-development/?utm_source=pest&utm_medium=banner&utm_campaign=pest_sponsorship)** ### Gold Sponsors -- **[CodeRabbit](https://coderabbit.ai/?ref=pestphp)** - **[CMS Max](https://cmsmax.com/?ref=pestphp)** ### Premium Sponsors -- [Forge](https://forge.laravel.com/?ref=pestphp) -- [Zapiet](https://www.zapiet.com/?ref=pestphp) -- [Localazy](https://localazy.com/?ref=pestphp) +- [Zapiet](https://zapiet.com/?ref=pestphp) - [Load Forge](https://loadforge.com/?ref=pestphp) -- [DocuWriter.ai](https://www.docuwriter.ai/?ref=pestphp) -- [Route4Me](https://www.route4me.com/?ref=pestphp) -- [Devtools for Livewire](https://devtools-for-livewire.com/?ref=pestphp) -- [Nerdify](https://www.getnerdify.com/?ref=pestphp) +- [Route4Me](https://route4me.com/pt?ref=pestphp) +- [Nerdify](https://getnerdify.com/?ref=pestphp) - [Akaunting](https://akaunting.com/?ref=pestphp) -- [LambdaTest](https://lambdatest.com/?ref=pestphp) +- [TestMu AI](https://www.testmuai.com/?utm_medium=sponsor&utm_source=pest) Pest is an open-sourced software licensed under the **[MIT license](https://opensource.org/licenses/MIT)**. diff --git a/composer.json b/composer.json index 5859b378..7ef9486e 100644 --- a/composer.json +++ b/composer.json @@ -18,19 +18,19 @@ ], "require": { "php": "^8.3.0", - "brianium/paratest": "^7.14.0", - "nunomaduro/collision": "^8.8.2", - "nunomaduro/termwind": "^2.3.1", + "brianium/paratest": "^7.20.0", + "nunomaduro/collision": "^8.9.3", + "nunomaduro/termwind": "^2.4.0", "pestphp/pest-plugin": "^4.0.0", "pestphp/pest-plugin-arch": "^4.0.0", "pestphp/pest-plugin-mutate": "^4.0.1", - "pestphp/pest-plugin-profanity": "^4.1.0", - "phpunit/phpunit": "^12.4.1", - "symfony/process": "^7.3.4" + "pestphp/pest-plugin-profanity": "^4.2.1", + "phpunit/phpunit": "^12.5.16", + "symfony/process": "^7.4.8|^8.0.8" }, "conflict": { "filp/whoops": "<2.18.3", - "phpunit/phpunit": ">12.4.1", + "phpunit/phpunit": ">12.5.16", "sebastian/exporter": "<7.0.0", "webmozart/assert": "<1.11.0" }, @@ -55,10 +55,10 @@ ] }, "require-dev": { - "pestphp/pest-dev-tools": "^4.0.0", - "pestphp/pest-plugin-browser": "^4.1.1", - "pestphp/pest-plugin-type-coverage": "^4.0.2", - "psy/psysh": "^0.12.12" + "pestphp/pest-dev-tools": "^4.1.0", + "pestphp/pest-plugin-browser": "^4.3.0", + "pestphp/pest-plugin-type-coverage": "^4.0.4", + "psy/psysh": "^0.12.22" }, "minimum-stability": "dev", "prefer-stable": true, diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 70b75de2..00000000 --- a/docker-compose.yml +++ /dev/null @@ -1,14 +0,0 @@ -version: "3.8" - -services: - php: - build: - context: ./docker - volumes: - - .:/var/www/html - composer: - build: - context: ./docker - volumes: - - .:/var/www/html - entrypoint: ["composer"] diff --git a/overrides/Logging/JUnit/JunitXmlLogger.php b/overrides/Logging/JUnit/JunitXmlLogger.php index b7362b1f..1fc237a6 100644 --- a/overrides/Logging/JUnit/JunitXmlLogger.php +++ b/overrides/Logging/JUnit/JunitXmlLogger.php @@ -14,6 +14,9 @@ namespace PHPUnit\Logging\JUnit; use DOMDocument; use DOMElement; +use Pest\Logging\Converter; +use Pest\Support\Container; +use Pest\TestSuite; use PHPUnit\Event\Code\Test; use PHPUnit\Event\Code\TestMethod; use PHPUnit\Event\EventFacadeIsSealedException; @@ -50,7 +53,7 @@ final class JunitXmlLogger { private readonly Printer $printer; - private readonly \Pest\Logging\Converter $converter; // pest-added + private readonly Converter $converter; // pest-added private DOMDocument $document; @@ -108,7 +111,7 @@ final class JunitXmlLogger public function __construct(Printer $printer, Facade $facade) { $this->printer = $printer; - $this->converter = new \Pest\Logging\Converter(\Pest\Support\Container::getInstance()->get(\Pest\TestSuite::class)->rootPath); // pest-added + $this->converter = new Converter(Container::getInstance()->get(TestSuite::class)->rootPath); // pest-added $this->registerSubscribers($facade); $this->createDocument(); diff --git a/rector.php b/rector.php index 49c56f8a..caec3188 100644 --- a/rector.php +++ b/rector.php @@ -2,8 +2,10 @@ declare(strict_types=1); -use Rector\CodingStyle\Rector\FunctionLike\FunctionLikeToFirstClassCallableRector; +use Rector\CodingStyle\Rector\ArrowFunction\ArrowFunctionDelegatingCallToFirstClassCallableRector; use Rector\Config\RectorConfig; +use Rector\DeadCode\Rector\ClassMethod\RemoveParentDelegatingConstructorRector; +use Rector\TypeDeclaration\Rector\ClassMethod\NarrowObjectReturnTypeRector; use Rector\TypeDeclaration\Rector\ClassMethod\ReturnNeverTypeRector; return RectorConfig::configure() @@ -13,7 +15,9 @@ return RectorConfig::configure() ->withSkip([ __DIR__.'/src/Plugins/Parallel/Paratest/WrapperRunner.php', ReturnNeverTypeRector::class, - FunctionLikeToFirstClassCallableRector::class, + ArrowFunctionDelegatingCallToFirstClassCallableRector::class, + NarrowObjectReturnTypeRector::class, + RemoveParentDelegatingConstructorRector::class, ]) ->withPreparedSets( deadCode: true, diff --git a/src/ArchPresets/Laravel.php b/src/ArchPresets/Laravel.php index 7d094c36..167725b6 100644 --- a/src/ArchPresets/Laravel.php +++ b/src/ArchPresets/Laravel.php @@ -150,7 +150,7 @@ final class Laravel extends AbstractPreset ->toHaveSuffix('Controller'); $this->expectations[] = expect('App\Http') - ->toOnlyBeUsedIn('App\Http'); + ->toOnlyBeUsedIn(['App\Http', 'App\Providers']); $this->expectations[] = expect('App\Http\Controllers') ->not->toHavePublicMethodsBesides(['__construct', '__invoke', 'index', 'show', 'create', 'store', 'edit', 'update', 'destroy', 'middleware']); diff --git a/src/ArchPresets/Php.php b/src/ArchPresets/Php.php index b70a2a11..22b409b6 100644 --- a/src/ArchPresets/Php.php +++ b/src/ArchPresets/Php.php @@ -4,9 +4,6 @@ declare(strict_types=1); namespace Pest\ArchPresets; -use Pest\Arch\Contracts\ArchExpectation; -use Pest\Expectation; - /** * @internal */ @@ -92,9 +89,5 @@ final class Php extends AbstractPreset 'xdebug_var_dump', 'trap', ])->not->toBeUsed(); - - $this->eachUserNamespace( - fn (Expectation $namespace): ArchExpectation => $namespace->not->toHaveSuspiciousCharacters(), - ); } } diff --git a/src/Concerns/Extendable.php b/src/Concerns/Extendable.php index a2f7e40b..1ff6626b 100644 --- a/src/Concerns/Extendable.php +++ b/src/Concerns/Extendable.php @@ -8,6 +8,8 @@ use Closure; /** * @internal + * + * @template T of object */ trait Extendable { @@ -20,6 +22,8 @@ trait Extendable /** * Register a new extend. + * + * @param-closure-this T $extend */ public function extend(string $name, Closure $extend): void { diff --git a/src/Concerns/Pipeable.php b/src/Concerns/Pipeable.php index 63ab0b7d..4e44f8db 100644 --- a/src/Concerns/Pipeable.php +++ b/src/Concerns/Pipeable.php @@ -66,6 +66,6 @@ trait Pipeable */ private function pipes(string $name, object $context, string $scope): array { - return array_map(fn (Closure $pipe): \Closure => $pipe->bindTo($context, $scope), self::$pipes[$name] ?? []); + return array_map(fn (Closure $pipe): Closure => $pipe->bindTo($context, $scope), self::$pipes[$name] ?? []); } } diff --git a/src/Concerns/Testable.php b/src/Concerns/Testable.php index 767a7c69..7ed0887d 100644 --- a/src/Concerns/Testable.php +++ b/src/Concerns/Testable.php @@ -129,7 +129,7 @@ trait Testable */ public function __addBeforeAll(?Closure $hook): void { - if (! $hook instanceof \Closure) { + if (! $hook instanceof Closure) { return; } @@ -143,7 +143,7 @@ trait Testable */ public function __addAfterAll(?Closure $hook): void { - if (! $hook instanceof \Closure) { + if (! $hook instanceof Closure) { return; } @@ -173,7 +173,7 @@ trait Testable */ private function __addHook(string $property, ?Closure $hook): void { - if (! $hook instanceof \Closure) { + if (! $hook instanceof Closure) { return; } diff --git a/src/Configuration.php b/src/Configuration.php index fb4f45a4..4261f3ef 100644 --- a/src/Configuration.php +++ b/src/Configuration.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Pest; +use Pest\PendingCalls\BeforeEachCall; use Pest\PendingCalls\UsesCall; /** @@ -62,6 +63,14 @@ final readonly class Configuration return (new UsesCall($this->filename, []))->group(...$groups); } + /** + * Marks all tests in the current file to be run exclusively. + */ + public function only(): void + { + (new BeforeEachCall(TestSuite::getInstance(), $this->filename))->only(); + } + /** * Depending on where is called, it will extend the given classes and traits globally or locally. */ diff --git a/src/Expectation.php b/src/Expectation.php index 8091ad82..5d7666cb 100644 --- a/src/Expectation.php +++ b/src/Expectation.php @@ -18,6 +18,7 @@ use Pest\Arch\Expectations\ToOnlyUse; use Pest\Arch\Expectations\ToUse; use Pest\Arch\Expectations\ToUseNothing; use Pest\Arch\PendingArchExpectation; +use Pest\Arch\Support\Composer; use Pest\Arch\Support\FileLineFinder; use Pest\Concerns\Extendable; use Pest\Concerns\Pipeable; @@ -52,7 +53,9 @@ use ReflectionProperty; */ final class Expectation { + /** @use Extendable> */ use Extendable; + use Pipeable; use Retrievable; @@ -134,7 +137,7 @@ 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 */ public function ddWhen(Closure|bool $condition, mixed ...$arguments): Expectation @@ -151,7 +154,7 @@ 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 */ public function ddUnless(Closure|bool $condition, mixed ...$arguments): Expectation @@ -667,6 +670,41 @@ final class Expectation throw InvalidExpectation::fromMethods(['toHavePrivateMethods']); } + /** + * Asserts that the given expectation target is cased correctly. + */ + public function toBeCasedCorrectly(): ArchExpectation + { + return Targeted::make( + $this, + function (ObjectDescription $object): bool { + if (! isset($object->reflectionClass)) { + return false; + } + + $realPath = realpath($object->path); + + foreach (Composer::userNamespaces() as $directory => $namespace) { + if (str_starts_with($realPath, $directory)) { + $relativePath = substr($realPath, strlen($directory) + 1); + $relativePath = explode('.', $relativePath)[0]; + $classFromPath = $namespace . '\\' . str_replace(DIRECTORY_SEPARATOR, '\\', $relativePath); + + if ($classFromPath === $object->reflectionClass->getName()) { + return true; + } + + return false; + } + } + + return false; + }, + "to be cased correctly", + FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), + ); + } + /** * Asserts that the given expectation target is enum. */ @@ -781,7 +819,22 @@ final class Expectation return false; } - if (! in_array($trait, $object->reflectionClass->getTraitNames(), true)) { + $currentClass = $object->reflectionClass; + $usedTraits = []; + + do { + $classTraits = $currentClass->getTraits(); + foreach ($classTraits as $traitReflection) { + $usedTraits[$traitReflection->getName()] = $traitReflection->getName(); + + $nestedTraits = $traitReflection->getTraits(); + foreach ($nestedTraits as $nestedTrait) { + $usedTraits[$nestedTrait->getName()] = $nestedTrait->getName(); + } + } + } while ($currentClass = $currentClass->getParentClass()); + + if (! array_key_exists($trait, $usedTraits)) { return false; } } diff --git a/src/Expectations/OppositeExpectation.php b/src/Expectations/OppositeExpectation.php index eec50c63..2713c724 100644 --- a/src/Expectations/OppositeExpectation.php +++ b/src/Expectations/OppositeExpectation.php @@ -15,6 +15,7 @@ use Pest\Arch\PendingArchExpectation; use Pest\Arch\SingleArchExpectation; use Pest\Arch\Support\FileLineFinder; use Pest\Exceptions\InvalidExpectation; +use Pest\Exceptions\MissingDependency; use Pest\Expectation; use Pest\Support\Arr; use Pest\Support\Exporter; @@ -284,6 +285,10 @@ final readonly class OppositeExpectation */ public function toHaveSuspiciousCharacters(): ArchExpectation { + if (! class_exists(Spoofchecker::class)) { + throw new MissingDependency(__FUNCTION__, 'ext-intl >= 2.0'); + } + $checker = new Spoofchecker; /** @var Expectation|string> $original */ diff --git a/src/Factories/TestCaseFactory.php b/src/Factories/TestCaseFactory.php index 0d2a0978..4599ee77 100644 --- a/src/Factories/TestCaseFactory.php +++ b/src/Factories/TestCaseFactory.php @@ -17,6 +17,7 @@ use Pest\Factories\Concerns\HigherOrderable; use Pest\Support\Reflection; use Pest\Support\Str; use Pest\TestSuite; +use PHPUnit\Framework\Attributes\TestDox; use PHPUnit\Framework\TestCase; use RuntimeException; @@ -58,6 +59,11 @@ final class TestCaseFactory Concerns\Expectable::class, ]; + /** + * The namespace for the test case, overrides the path-based namespace when set. + */ + public ?string $namespace = null; + /** * Creates a new Factory instance. */ @@ -126,7 +132,7 @@ final class TestCaseFactory $partsFQN = explode('\\', $classFQN); $className = array_pop($partsFQN); - $namespace = implode('\\', $partsFQN); + $namespace = $this->namespace ?? implode('\\', $partsFQN); $baseClass = sprintf('\%s', $this->class); if (trim($className) === '') { @@ -135,7 +141,7 @@ final class TestCaseFactory $this->attributes = [ new Attribute( - \PHPUnit\Framework\Attributes\TestDox::class, + TestDox::class, [$this->filename], ), ...$this->attributes, diff --git a/src/Factories/TestCaseMethodFactory.php b/src/Factories/TestCaseMethodFactory.php index c88db44d..9438f837 100644 --- a/src/Factories/TestCaseMethodFactory.php +++ b/src/Factories/TestCaseMethodFactory.php @@ -9,10 +9,14 @@ use Pest\Evaluators\Attributes; use Pest\Exceptions\ShouldNotHappen; use Pest\Factories\Concerns\HigherOrderable; use Pest\Repositories\DatasetsRepository; +use Pest\Support\Description; use Pest\Support\Str; use Pest\TestSuite; use PHPUnit\Framework\Assert; use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Depends; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\Attributes\TestDox; use PHPUnit\Framework\TestCase; /** @@ -32,7 +36,7 @@ final class TestCaseMethodFactory /** * The test's describing, if any. * - * @var array + * @var array */ public array $describing = []; @@ -192,11 +196,11 @@ final class TestCaseMethodFactory $this->attributes = [ new Attribute( - \PHPUnit\Framework\Attributes\Test::class, + Test::class, [], ), new Attribute( - \PHPUnit\Framework\Attributes\TestDox::class, + TestDox::class, [str_replace('*/', '{@*}', $this->description)], ), ...$this->attributes, @@ -206,7 +210,7 @@ final class TestCaseMethodFactory $depend = Str::evaluable($this->describing === [] ? $depend : Str::describe($this->describing, $depend)); $this->attributes[] = new Attribute( - \PHPUnit\Framework\Attributes\Depends::class, + Depends::class, [$depend], ); } diff --git a/src/Functions.php b/src/Functions.php index 5bc916a2..afeae865 100644 --- a/src/Functions.php +++ b/src/Functions.php @@ -140,7 +140,7 @@ if (! function_exists('test')) { */ function test(?string $description = null, ?Closure $closure = null): HigherOrderTapProxy|TestCall { - if ($description === null && TestSuite::getInstance()->test instanceof \PHPUnit\Framework\TestCase) { + if ($description === null && TestSuite::getInstance()->test instanceof TestCase) { return new HigherOrderTapProxy(TestSuite::getInstance()->test); } @@ -236,7 +236,7 @@ if (! function_exists('covers')) { /** @var MutationTestRunner $runner */ $runner = Container::getInstance()->get(MutationTestRunner::class); - /** @var \Pest\Mutate\Repositories\ConfigurationRepository $configurationRepository */ + /** @var ConfigurationRepository $configurationRepository */ $configurationRepository = Container::getInstance()->get(ConfigurationRepository::class); $everything = $configurationRepository->cliConfiguration->toArray()['everything'] ?? false; $classes = $configurationRepository->cliConfiguration->toArray()['classes'] ?? false; @@ -263,7 +263,7 @@ if (! function_exists('mutates')) { /** @var MutationTestRunner $runner */ $runner = Container::getInstance()->get(MutationTestRunner::class); - /** @var \Pest\Mutate\Repositories\ConfigurationRepository $configurationRepository */ + /** @var ConfigurationRepository $configurationRepository */ $configurationRepository = Container::getInstance()->get(ConfigurationRepository::class); $everything = $configurationRepository->cliConfiguration->toArray()['everything'] ?? false; $classes = $configurationRepository->cliConfiguration->toArray()['classes'] ?? false; @@ -320,7 +320,7 @@ if (! function_exists('visit')) { */ function visit(array|string $url, array $options = []): ArrayablePendingAwaitablePage|PendingAwaitablePage { - if (! class_exists(\Pest\Browser\Configuration::class)) { + if (! class_exists(Pest\Browser\Configuration::class)) { PluginBrowser::install(); exit(0); diff --git a/src/Logging/Converter.php b/src/Logging/Converter.php index 50a42d1a..e0b69bb0 100644 --- a/src/Logging/Converter.php +++ b/src/Logging/Converter.php @@ -151,7 +151,7 @@ final readonly class Converter { if ($testSuite instanceof TestSuiteForTestMethodWithDataProvider) { $firstTest = $this->getFirstTest($testSuite); - if ($firstTest instanceof \PHPUnit\Event\Code\TestMethod) { + if ($firstTest instanceof TestMethod) { return $this->getTestMethodNameWithoutDatasetSuffix($firstTest); } } @@ -179,7 +179,7 @@ final readonly class Converter public function getTestSuiteLocation(TestSuite $testSuite): ?string { $firstTest = $this->getFirstTest($testSuite); - if (! $firstTest instanceof \PHPUnit\Event\Code\TestMethod) { + if (! $firstTest instanceof TestMethod) { return null; } $path = $firstTest->testDox()->prettifiedClassName(); diff --git a/src/Logging/TeamCity/TeamCityLogger.php b/src/Logging/TeamCity/TeamCityLogger.php index 19a7be69..cbbf0347 100644 --- a/src/Logging/TeamCity/TeamCityLogger.php +++ b/src/Logging/TeamCity/TeamCityLogger.php @@ -200,7 +200,7 @@ final class TeamCityLogger public function testFinished(Finished $event): void { - if (! $this->time instanceof \PHPUnit\Event\Telemetry\HRTime) { + if (! $this->time instanceof HRTime) { throw ShouldNotHappen::fromMessage('Start time has not been set.'); } diff --git a/src/Mixins/Expectation.php b/src/Mixins/Expectation.php index 09974787..f235c748 100644 --- a/src/Mixins/Expectation.php +++ b/src/Mixins/Expectation.php @@ -9,6 +9,7 @@ use Closure; use Countable; use DateTimeInterface; use Error; +use Illuminate\Testing\TestResponse; use InvalidArgumentException; use JsonSerializable; use Pest\Exceptions\InvalidExpectationValue; @@ -842,7 +843,7 @@ final class Expectation is_object($this->value) && method_exists($this->value, 'toSnapshot') => $this->value->toSnapshot(), is_object($this->value) && method_exists($this->value, '__toString') => $this->value->__toString(), is_object($this->value) && method_exists($this->value, 'toString') => $this->value->toString(), - $this->value instanceof \Illuminate\Testing\TestResponse => $this->value->getContent(), // @phpstan-ignore-line + $this->value instanceof TestResponse => $this->value->getContent(), // @phpstan-ignore-line is_array($this->value) => json_encode($this->value, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT), $this->value instanceof Traversable => json_encode(iterator_to_array($this->value), JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT), $this->value instanceof JsonSerializable => json_encode($this->value->jsonSerialize(), JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT), @@ -983,7 +984,7 @@ final class Expectation */ private function export(mixed $value): string { - if (! $this->exporter instanceof \Pest\Support\Exporter) { + if (! $this->exporter instanceof Exporter) { $this->exporter = Exporter::default(); } diff --git a/src/PendingCalls/Concerns/Describable.php b/src/PendingCalls/Concerns/Describable.php index 0208ea4b..cac2fb0b 100644 --- a/src/PendingCalls/Concerns/Describable.php +++ b/src/PendingCalls/Concerns/Describable.php @@ -4,6 +4,8 @@ declare(strict_types=1); namespace Pest\PendingCalls\Concerns; +use Pest\Support\Description; + /** * @internal */ @@ -12,14 +14,14 @@ trait Describable /** * Note: this is property is not used; however, it gets added automatically by rector php. * - * @var array + * @var array */ public array $__describing; /** * The describing of the test case. * - * @var array + * @var array */ public array $describing = []; } diff --git a/src/PendingCalls/TestCall.php b/src/PendingCalls/TestCall.php index a65c8bb5..79264596 100644 --- a/src/PendingCalls/TestCall.php +++ b/src/PendingCalls/TestCall.php @@ -22,6 +22,10 @@ use Pest\Support\NullClosure; use Pest\Support\Str; use Pest\TestSuite; use PHPUnit\Framework\AssertionFailedError; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\CoversFunction; +use PHPUnit\Framework\Attributes\CoversTrait; +use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\TestCase; /** @@ -211,7 +215,7 @@ final class TestCall // @phpstan-ignore-line { foreach ($groups as $group) { $this->testCaseMethod->attributes[] = new Attribute( - \PHPUnit\Framework\Attributes\Group::class, + Group::class, [$group], ); } @@ -604,7 +608,7 @@ final class TestCall // @phpstan-ignore-line { foreach ($classes as $class) { $this->testCaseFactoryAttributes[] = new Attribute( - \PHPUnit\Framework\Attributes\CoversClass::class, + CoversClass::class, [$class], ); } @@ -627,7 +631,7 @@ final class TestCall // @phpstan-ignore-line { foreach ($traits as $trait) { $this->testCaseFactoryAttributes[] = new Attribute( - \PHPUnit\Framework\Attributes\CoversTrait::class, + CoversTrait::class, [$trait], ); } @@ -650,7 +654,7 @@ final class TestCall // @phpstan-ignore-line { foreach ($functions as $function) { $this->testCaseFactoryAttributes[] = new Attribute( - \PHPUnit\Framework\Attributes\CoversFunction::class, + CoversFunction::class, [$function], ); } diff --git a/src/Pest.php b/src/Pest.php index fa581af4..8595bd5f 100644 --- a/src/Pest.php +++ b/src/Pest.php @@ -6,7 +6,7 @@ namespace Pest; function version(): string { - return '4.1.3'; + return '4.4.4'; } function testDirectory(string $file = ''): string diff --git a/src/Plugins/Concerns/HandleArguments.php b/src/Plugins/Concerns/HandleArguments.php index 9cf5e606..6fdcff8c 100644 --- a/src/Plugins/Concerns/HandleArguments.php +++ b/src/Plugins/Concerns/HandleArguments.php @@ -56,4 +56,31 @@ trait HandleArguments return array_values(array_flip($arguments)); } + + /** + * Pops the given argument and its value from the arguments, returning the value. + * + * @param array $arguments + */ + public function popArgumentValue(string $argument, array &$arguments): ?string + { + foreach ($arguments as $key => $value) { + if (str_contains($value, "$argument=")) { + unset($arguments[$key]); + $arguments = array_values($arguments); + + return substr($value, strlen($argument) + 1); + } + + if ($value === $argument && isset($arguments[$key + 1])) { + $result = $arguments[$key + 1]; + unset($arguments[$key], $arguments[$key + 1]); + $arguments = array_values($arguments); + + return $result; + } + } + + return null; + } } diff --git a/src/Plugins/Help.php b/src/Plugins/Help.php index 89a47b66..a2fb1ef0 100644 --- a/src/Plugins/Help.php +++ b/src/Plugins/Help.php @@ -99,6 +99,7 @@ final readonly class Help implements HandlesArguments { $helpReflection = new PHPUnitHelp; + // @phpstan-ignore-next-line $content = (fn (): array => $this->elements())->call($helpReflection); $content['Configuration'] = [...[[ @@ -106,6 +107,13 @@ final readonly class Help implements HandlesArguments 'desc' => 'Initialise a standard Pest configuration', ]], ...$content['Configuration']]; + $content['AI'] = [ + [ + 'arg' => '--ai', + 'desc' => 'Run a code snippet as a fully scaffolded test for AI verification', + ], + ]; + $content['Execution'] = [...[ [ 'arg' => '--parallel', @@ -141,6 +149,9 @@ final readonly class Help implements HandlesArguments ], [ 'arg' => '--retry', 'desc' => 'Run non-passing tests first and stop execution upon first error or failure', + ], [ + 'arg' => '--dirty', + 'desc' => 'Only run tests that have uncommitted changes according to Git', ], ...$content['Selection']]; $content['Reporting'] = [...$content['Reporting'], ...[ diff --git a/src/Plugins/Parallel/Handlers/Laravel.php b/src/Plugins/Parallel/Handlers/Laravel.php index f634be56..30063d5f 100644 --- a/src/Plugins/Parallel/Handlers/Laravel.php +++ b/src/Plugins/Parallel/Handlers/Laravel.php @@ -7,6 +7,7 @@ namespace Pest\Plugins\Parallel\Handlers; use Closure; use Composer\InstalledVersions; use Illuminate\Testing\ParallelRunner; +use Orchestra\Testbench\TestCase; use ParaTest\Options; use ParaTest\RunnerInterface; use Pest\Contracts\Plugins\HandlesArguments; @@ -39,13 +40,13 @@ final class Laravel implements HandlesArguments * Executes the given closure when running Laravel. * * @param array $arguments - * @param CLosure(array): array $closure + * @param Closure(array): array $closure * @return array */ private function whenUsingLaravel(array $arguments, Closure $closure): array { $isLaravelApplication = InstalledVersions::isInstalled('laravel/framework', false); - $isLaravelPackage = class_exists(\Orchestra\Testbench\TestCase::class); + $isLaravelPackage = class_exists(TestCase::class); if ($isLaravelApplication && ! $isLaravelPackage) { return $closure($arguments); diff --git a/src/Plugins/Parallel/Paratest/WrapperRunner.php b/src/Plugins/Parallel/Paratest/WrapperRunner.php index 469f2aa6..b853c65f 100644 --- a/src/Plugins/Parallel/Paratest/WrapperRunner.php +++ b/src/Plugins/Parallel/Paratest/WrapperRunner.php @@ -51,6 +51,11 @@ final class WrapperRunner implements RunnerInterface /** * The time to sleep between cycles. */ + /** + * The merged test result from the parallel run. + */ + public static ?TestResult $result = null; + private const int CYCLE_SLEEP = 10000; /** @@ -131,6 +136,7 @@ final class WrapperRunner implements RunnerInterface $parameters = $this->handleLaravelHerd($parameters); $parameters[] = $wrapper; + $parameters[] = '--test-directory='.TestSuite::getInstance()->testPath; $this->parameters = $parameters; $this->codeCoverageFilterRegistry = new CodeCoverageFilterRegistry; @@ -385,6 +391,8 @@ final class WrapperRunner implements RunnerInterface $testResultSum->numberOfIssuesIgnoredByBaseline(), ); + self::$result = $testResultSum; + if ($this->options->configuration->cacheResult()) { $resultCacheSum = new DefaultResultCache($this->options->configuration->testResultCacheFile()); foreach ($this->resultCacheFiles as $resultCacheFile) { diff --git a/src/Repositories/TestRepository.php b/src/Repositories/TestRepository.php index 466d3226..cfce2cf1 100644 --- a/src/Repositories/TestRepository.php +++ b/src/Repositories/TestRepository.php @@ -113,6 +113,16 @@ final class TestRepository $this->testCaseMethodFilters[] = $filter; } + /** + * Gets the class and traits configured for the given directory path. + * + * @return array + */ + public function getUsesForPath(string $path): array + { + return $this->uses[$path][0] ?? []; + } + /** * Gets the test case factory from the given filename. */ diff --git a/src/Support/Closure.php b/src/Support/Closure.php index e447903f..bf715780 100644 --- a/src/Support/Closure.php +++ b/src/Support/Closure.php @@ -19,14 +19,14 @@ final class Closure */ public static function bind(?BaseClosure $closure, ?object $newThis, object|string|null $newScope = 'static'): BaseClosure { - if (! $closure instanceof \Closure) { + if (! $closure instanceof BaseClosure) { throw ShouldNotHappen::fromMessage('Could not bind null closure.'); } // @phpstan-ignore-next-line $closure = BaseClosure::bind($closure, $newThis, $newScope); - if (! $closure instanceof \Closure) { + if (! $closure instanceof BaseClosure) { throw ShouldNotHappen::fromMessage('Could not bind closure.'); } diff --git a/src/Support/Container.php b/src/Support/Container.php index 241825f5..d43c22be 100644 --- a/src/Support/Container.php +++ b/src/Support/Container.php @@ -28,7 +28,7 @@ final class Container */ public static function getInstance(): self { - if (! self::$instance instanceof \Pest\Support\Container) { + if (! self::$instance instanceof Container) { self::$instance = new self; } diff --git a/src/Support/ExceptionTrace.php b/src/Support/ExceptionTrace.php index 9d4132e2..92047840 100644 --- a/src/Support/ExceptionTrace.php +++ b/src/Support/ExceptionTrace.php @@ -26,6 +26,7 @@ final class ExceptionTrace return $closure(); } catch (Throwable $throwable) { if (Str::startsWith($message = $throwable->getMessage(), self::UNDEFINED_METHOD)) { + // @phpstan-ignore-next-line $class = preg_match('/^Call to undefined method ([^:]+)::/', $message, $matches) === false ? null : $matches[1]; $message = str_replace(self::UNDEFINED_METHOD, 'Call to undefined method ', $message); diff --git a/src/Support/HigherOrderCallables.php b/src/Support/HigherOrderCallables.php index 9e5bba36..358b4da5 100644 --- a/src/Support/HigherOrderCallables.php +++ b/src/Support/HigherOrderCallables.php @@ -46,6 +46,7 @@ final readonly class HigherOrderCallables */ public function and(mixed $value): Expectation { + // @phpstan-ignore-next-line return $this->expect($value); } diff --git a/src/Support/Reflection.php b/src/Support/Reflection.php index d4f5f133..ac1a4273 100644 --- a/src/Support/Reflection.php +++ b/src/Support/Reflection.php @@ -8,6 +8,7 @@ use Closure; use InvalidArgumentException; use Pest\Exceptions\ShouldNotHappen; use Pest\TestSuite; +use PHPUnit\Framework\TestCase; use ReflectionClass; use ReflectionException; use ReflectionFunction; @@ -66,7 +67,7 @@ final class Reflection { $test = TestSuite::getInstance()->test; - if (! $test instanceof \PHPUnit\Framework\TestCase) { + if (! $test instanceof TestCase) { return self::bindCallable($callable); } @@ -221,7 +222,7 @@ final class Reflection { $getProperties = fn (ReflectionClass $reflectionClass): array => array_filter( array_map( - fn (ReflectionProperty $property): \ReflectionProperty => $property, + fn (ReflectionProperty $property): ReflectionProperty => $property, $reflectionClass->getProperties(), ), fn (ReflectionProperty $property): bool => $property->getDeclaringClass()->getName() === $reflectionClass->getName(), ); @@ -256,7 +257,7 @@ final class Reflection { $getMethods = fn (ReflectionClass $reflectionClass): array => array_filter( array_map( - fn (ReflectionMethod $method): \ReflectionMethod => $method, + fn (ReflectionMethod $method): ReflectionMethod => $method, $reflectionClass->getMethods($filter), ), fn (ReflectionMethod $method): bool => $method->getDeclaringClass()->getName() === $reflectionClass->getName(), ); diff --git a/src/Support/Str.php b/src/Support/Str.php index 6e32c05d..04f4b1fd 100644 --- a/src/Support/Str.php +++ b/src/Support/Str.php @@ -79,7 +79,7 @@ final class Str return $subject; } - return substr($subject, 0, $pos); + return mb_substr($subject, 0, $pos); } /** diff --git a/stubs/init-laravel/Pest.php.stub b/stubs/init-laravel/Pest.php.stub index 60f04a45..2c5012ca 100644 --- a/stubs/init-laravel/Pest.php.stub +++ b/stubs/init-laravel/Pest.php.stub @@ -1,5 +1,8 @@ extend(Tests\TestCase::class) - // ->use(Illuminate\Foundation\Testing\RefreshDatabase::class) +pest()->extend(TestCase::class) + // ->use(RefreshDatabase::class) ->in('Feature'); /* diff --git a/stubs/init/Pest.php.stub b/stubs/init/Pest.php.stub index b239048c..d4ffffba 100644 --- a/stubs/init/Pest.php.stub +++ b/stubs/init/Pest.php.stub @@ -1,5 +1,7 @@ extend(Tests\TestCase::class)->in('Feature'); +pest()->extend(TestCase::class)->in('Feature'); /* |-------------------------------------------------------------------------- diff --git a/tests/.pest/snapshots/Visual/Help/visual_snapshot_of_help_command_output.snap b/tests/.pest/snapshots/Visual/Help/visual_snapshot_of_help_command_output.snap index 63f4714c..75439fc3 100644 --- a/tests/.pest/snapshots/Visual/Help/visual_snapshot_of_help_command_output.snap +++ b/tests/.pest/snapshots/Visual/Help/visual_snapshot_of_help_command_output.snap @@ -1,5 +1,5 @@ - Pest Testing Framework 4.1.3. + Pest Testing Framework 4.4.4. USAGE: pest [options] @@ -27,6 +27,8 @@ --pr .... Output to standard output tests with the given pull request number --pull-request Output to standard output tests with the given pull request number (alias for --pr) --retry Run non-passing tests first and stop execution upon first error or failure + --dirty ...... Only run tests that have uncommitted changes according to Git + --all .................... Ignore test selection from XML configuration file --list-suites ................................... List available test suites --testsuite [name] ......... Only run tests from the specified test suite(s) --exclude-testsuite [name] .. Exclude tests from the specified test suite(s) @@ -90,7 +92,7 @@ --random-order-seed [N] Use the specified random seed when running tests in random order REPORTING OPTIONS: - --colors [flag] ......... Use colors in output ("never", "auto" or "always") + --colors=[flag] ......... Use colors in output ("never", "auto" or "always") --columns [n] ................. Number of columns to use for progress output --columns max ............ Use maximum number of columns for progress output --stderr ................................. Write to STDERR instead of STDOUT @@ -138,12 +140,16 @@ --only-summary-for-coverage-text Option for code coverage report in text format: only show summary --show-uncovered-for-coverage-text Option for code coverage report in text format: show uncovered files --coverage-xml [dir] . Write code coverage report in XML format to directory + --exclude-source-from-xml-coverage Exclude [source] element from code coverage report in XML format --warm-coverage-cache ........................... Warm static analysis cache --coverage-filter [dir] ........... Include [dir] in code coverage reporting --path-coverage .......... Report path coverage in addition to line coverage --disable-coverage-ignore ...... Disable metadata for ignoring code coverage --no-coverage Ignore code coverage reporting configured in the XML configuration file + AI OPTIONS: + --ai ..... Run a code snippet as a fully scaffolded test for AI verification + MUTATION TESTING OPTIONS: --mutate .... Runs mutation testing, to understand the quality of your tests --mutate --parallel ...................... Runs mutation testing in parallel diff --git a/tests/.pest/snapshots/Visual/Version/visual_snapshot_of_help_command_output.snap b/tests/.pest/snapshots/Visual/Version/visual_snapshot_of_help_command_output.snap index 8bafa6ce..988eec06 100644 --- a/tests/.pest/snapshots/Visual/Version/visual_snapshot_of_help_command_output.snap +++ b/tests/.pest/snapshots/Visual/Version/visual_snapshot_of_help_command_output.snap @@ -1,3 +1,3 @@ - Pest Testing Framework 4.1.3. + Pest Testing Framework 4.4.4. diff --git a/tests/.snapshots/SuccessOnly.php.inc b/tests/.snapshots/SuccessOnly.php.inc index b00a0e36..b940b7b6 100644 --- a/tests/.snapshots/SuccessOnly.php.inc +++ b/tests/.snapshots/SuccessOnly.php.inc @@ -1,5 +1,5 @@ ##teamcity[testSuiteStarted name='Tests/tests/SuccessOnly' locationHint='pest_qn://tests/.tests/SuccessOnly.php' flowId='1234'] -##teamcity[testCount count='3' flowId='1234'] +##teamcity[testCount count='4' flowId='1234'] ##teamcity[testStarted name='it can pass with comparison' locationHint='pest_qn://tests/.tests/SuccessOnly.php::it can pass with comparison' flowId='1234'] ##teamcity[testFinished name='it can pass with comparison' duration='100000' flowId='1234'] ##teamcity[testStarted name='can also pass' locationHint='pest_qn://tests/.tests/SuccessOnly.php::can also pass' flowId='1234'] @@ -8,8 +8,12 @@ ##teamcity[testStarted name='can pass with dataset with data set "(true)"' locationHint='pest_qn://tests/.tests/SuccessOnly.php::can pass with dataset with data set "(true)"' flowId='1234'] ##teamcity[testFinished name='can pass with dataset with data set "(true)"' duration='100000' flowId='1234'] ##teamcity[testSuiteFinished name='can pass with dataset' flowId='1234'] +##teamcity[testSuiteStarted name='`block` → can pass with dataset in describe block' locationHint='pest_qn://tests/.tests/SuccessOnly.php::`block` → can pass with dataset in describe block' flowId='1234'] +##teamcity[testStarted name='`block` → can pass with dataset in describe block with data set "(1)"' locationHint='pest_qn://tests/.tests/SuccessOnly.php::`block` → can pass with dataset in describe block with data set "(1)"' flowId='1234'] +##teamcity[testFinished name='`block` → can pass with dataset in describe block with data set "(1)"' duration='100000' flowId='1234'] +##teamcity[testSuiteFinished name='`block` → can pass with dataset in describe block' flowId='1234'] ##teamcity[testSuiteFinished name='Tests/tests/SuccessOnly' flowId='1234'] - Tests: 3 passed (3 assertions) + Tests: 4 passed (4 assertions) Duration: 1.00s diff --git a/tests/.snapshots/success.txt b/tests/.snapshots/success.txt index bfa91bb1..7b3d6f9b 100644 --- a/tests/.snapshots/success.txt +++ b/tests/.snapshots/success.txt @@ -1782,4 +1782,4 @@ ✓ pass with dataset with ('my-datas-set-value') ✓ within describe → pass with dataset with ('my-datas-set-value') - Tests: 2 deprecated, 4 warnings, 5 incomplete, 2 notices, 39 todos, 35 skipped, 1188 passed (2814 assertions) \ No newline at end of file + Tests: 2 deprecated, 4 warnings, 5 incomplete, 2 notices, 39 todos, 35 skipped, 1188 passed (2813 assertions) \ No newline at end of file diff --git a/tests/.tests/SuccessOnly.php b/tests/.tests/SuccessOnly.php index cb4009a6..4d231a8d 100644 --- a/tests/.tests/SuccessOnly.php +++ b/tests/.tests/SuccessOnly.php @@ -13,3 +13,9 @@ test('can also pass', function () { test('can pass with dataset', function ($value) { expect($value)->toEqual(true); })->with([true]); + +describe('block', function () { + test('can pass with dataset in describe block', function ($number) { + expect($number)->toBeInt(); + })->with([1]); +}); diff --git a/tests/Autoload.php b/tests/Autoload.php index 2d3ce530..5bd3c23b 100644 --- a/tests/Autoload.php +++ b/tests/Autoload.php @@ -1,5 +1,7 @@ bar = 0; beforeAll(function () use ($foo) { diff --git a/tests/Features/Coverage.php b/tests/Features/Coverage.php index 974568e6..55ce4164 100644 --- a/tests/Features/Coverage.php +++ b/tests/Features/Coverage.php @@ -17,7 +17,7 @@ it('adds coverage if --coverage exist', function () { $arguments = $plugin->handleArguments(['--coverage']); expect($arguments)->toEqual(['--coverage-php', Coverage::getPath()]) ->and($plugin->coverage)->toBeTrue(); -})->skip(! \Pest\Support\Coverage::isAvailable() || ! function_exists('xdebug_info') || ! in_array('coverage', xdebug_info('mode'), true), 'Coverage is not available'); +})->skip(! Coverage::isAvailable() || ! function_exists('xdebug_info') || ! in_array('coverage', xdebug_info('mode'), true), 'Coverage is not available'); it('adds coverage if --min exist', function () { $plugin = new CoveragePlugin(new ConsoleOutput); diff --git a/tests/Features/Expect/toBeCasedCorrectly.php b/tests/Features/Expect/toBeCasedCorrectly.php new file mode 100644 index 00000000..6a8432d2 --- /dev/null +++ b/tests/Features/Expect/toBeCasedCorrectly.php @@ -0,0 +1,12 @@ +expect('Tests\Fixtures\Arch\ToBeCasedCorrectly\CorrectCasing') + ->toBeCasedCorrectly(); + +test('failure') + ->expect('Tests\Fixtures\Arch\ToBeCasedCorrectly\IncorrectCasing') + ->toBeCasedCorrectly() + ->throws(ArchExpectationFailedException::class); diff --git a/tests/Features/Expect/toUseTrait.php b/tests/Features/Expect/toUseTrait.php index fd9c933f..806490b3 100644 --- a/tests/Features/Expect/toUseTrait.php +++ b/tests/Features/Expect/toUseTrait.php @@ -14,3 +14,19 @@ test('failures', function () { test('not failures', function () { expect('Pest\Expectations\HigherOrderExpectation')->not->toUseTrait('Pest\Concerns\Retrievable'); })->throws(ArchExpectationFailedException::class); + +test('trait inheritance - direct usage', function () { + expect('Tests\Fixtures\Arch\ToUseTrait\HasTrait\ParentClassWithTrait')->toUseTrait('Tests\Fixtures\Arch\ToUseTrait\HasTrait\TestTraitForInheritance'); +}); + +test('trait inheritance - inherited usage', function () { + expect('Tests\Fixtures\Arch\ToUseTrait\HasInheritedTrait\ChildClassExtendingParent')->toUseTrait('Tests\Fixtures\Arch\ToUseTrait\HasTrait\TestTraitForInheritance'); +}); + +test('trait inheritance - negative case', function () { + expect('Tests\Fixtures\Arch\ToUseTrait\HasInheritedTrait\ChildClassExtendingParent')->not->toUseTrait('NonExistentTrait'); +}); + +test('nested trait inheritance', function () { + expect('Tests\Fixtures\Arch\ToUseTrait\HasInheritedTrait\ChildClassExtendingParent')->toUseTrait('Tests\Fixtures\Arch\ToUseTrait\HasNestedTrait\NestedTrait'); +}); diff --git a/tests/Features/Helpers.php b/tests/Features/Helpers.php index e32faffa..d2ea9485 100644 --- a/tests/Features/Helpers.php +++ b/tests/Features/Helpers.php @@ -39,7 +39,7 @@ it('allows to call underlying protected/private methods', function () { it('throws error if method do not exist', function () { test()->foo(); -})->throws(\ReflectionException::class, 'Call to undefined method PHPUnit\Framework\TestCase::foo()'); +})->throws(ReflectionException::class, 'Call to undefined method PHPUnit\Framework\TestCase::foo()'); it('can forward unexpected calls to any global function')->_assertThat(); diff --git a/tests/Fixtures/Arch/ToBeCasedCorrectly/CorrectCasing/CorrectCasing.php b/tests/Fixtures/Arch/ToBeCasedCorrectly/CorrectCasing/CorrectCasing.php new file mode 100644 index 00000000..8418d969 --- /dev/null +++ b/tests/Fixtures/Arch/ToBeCasedCorrectly/CorrectCasing/CorrectCasing.php @@ -0,0 +1,5 @@ +use(Tests\CustomTestCase\CustomTestCase::class)->in(__DIR__); +use Tests\CustomTestCase\CustomTestCase; + +pest()->use(CustomTestCase::class)->in(__DIR__); test('closure was bound to CustomTestCase', function () { $this->assertCustomTrue(); diff --git a/tests/PHPUnit/CustomTestCaseInSubFolders/SubFolder2/UsesPerFile.php b/tests/PHPUnit/CustomTestCaseInSubFolders/SubFolder2/UsesPerFile.php index 676e80f4..edce0fe6 100644 --- a/tests/PHPUnit/CustomTestCaseInSubFolders/SubFolder2/UsesPerFile.php +++ b/tests/PHPUnit/CustomTestCaseInSubFolders/SubFolder2/UsesPerFile.php @@ -1,5 +1,7 @@ printer(); - expect($theme)->toBeInstanceOf(Pest\Configuration\Printer::class); + expect($theme)->toBeInstanceOf(Printer::class); }); diff --git a/tests/Visual/Collision.php b/tests/Visual/Collision.php index 7812fc0d..99870cac 100644 --- a/tests/Visual/Collision.php +++ b/tests/Visual/Collision.php @@ -1,8 +1,10 @@ 'DefaultPrinter', 'COLLISION_IGNORE_DURATION' => 'true', 'COLLISION_TEST' => true] diff --git a/tests/Visual/Help.php b/tests/Visual/Help.php index eea3fd99..35b25989 100644 --- a/tests/Visual/Help.php +++ b/tests/Visual/Help.php @@ -1,8 +1,10 @@ 'DefaultPrinter', 'COLLISION_IGNORE_DURATION' => 'true'])); + $process = (new Process(['php', 'bin/pest', '--help'], null, ['COLLISION_PRINTER' => 'DefaultPrinter', 'COLLISION_IGNORE_DURATION' => 'true'])); $process->run(); diff --git a/tests/Visual/JUnit.php b/tests/Visual/JUnit.php index 3523bdd6..fd34ea7a 100644 --- a/tests/Visual/JUnit.php +++ b/tests/Visual/JUnit.php @@ -36,8 +36,8 @@ test('junit output', function () use ($normalizedPath, $run) { expect($result['testsuite']['@attributes']) ->name->toBe('Tests\tests\SuccessOnly') ->file->toBe($normalizedPath('tests/.tests/SuccessOnly.php')) - ->tests->toBe('3') - ->assertions->toBe('3') + ->tests->toBe('4') + ->assertions->toBe('4') ->errors->toBe('0') ->failures->toBe('0') ->skipped->toBe('0'); diff --git a/tests/Visual/Parallel.php b/tests/Visual/Parallel.php index f70c2826..1aced21d 100644 --- a/tests/Visual/Parallel.php +++ b/tests/Visual/Parallel.php @@ -16,7 +16,7 @@ $run = function () { test('parallel', function () use ($run) { expect($run('--exclude-group=integration')) - ->toContain('Tests: 2 deprecated, 4 warnings, 5 incomplete, 2 notices, 39 todos, 26 skipped, 1178 passed (2790 assertions)') + ->toContain('Tests: 2 deprecated, 4 warnings, 5 incomplete, 2 notices, 39 todos, 26 skipped, 1177 passed (2789 assertions)') ->toContain('Parallel: 3 processes'); })->skipOnWindows(); diff --git a/tests/Visual/Success.php b/tests/Visual/Success.php index d87ad304..16d49a38 100644 --- a/tests/Visual/Success.php +++ b/tests/Visual/Success.php @@ -1,5 +1,7 @@ 'integration', '--exclude-group' => 'integration', 'REBUILD_SNAPSHOTS' => false, 'PARATEST' => 0, 'COLLISION_PRINTER' => 'DefaultPrinter', 'COLLISION_IGNORE_DURATION' => 'true'], diff --git a/tests/Visual/TeamCity.php b/tests/Visual/TeamCity.php index c02ee21e..926f3957 100644 --- a/tests/Visual/TeamCity.php +++ b/tests/Visual/TeamCity.php @@ -1,5 +1,7 @@ 'DefaultPrinter', 'COLLISION_IGNORE_DURATION' => 'true'])); + $process = (new Process(['php', 'bin/pest', '--version'], null, ['COLLISION_PRINTER' => 'DefaultPrinter', 'COLLISION_IGNORE_DURATION' => 'true'])); $process->run();