Repository: pestphp/pest Branch: 4.x Commit: 5d42e8fe3ae1 Files: 560 Total size: 870.5 KB Directory structure: gitextract_nn7gly9v/ ├── .editorconfig ├── .gitattributes ├── .github/ │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ └── bug_report.yml │ ├── PULL_REQUEST_TEMPLATE.md │ └── workflows/ │ ├── static.yml │ └── tests.yml ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── RELEASE.md ├── bin/ │ ├── pest │ └── worker.php ├── composer.json ├── docker/ │ └── Dockerfile ├── extension.neon ├── overrides/ │ ├── Event/ │ │ └── Value/ │ │ └── ThrowableBuilder.php │ ├── Logging/ │ │ └── JUnit/ │ │ └── JunitXmlLogger.php │ ├── Runner/ │ │ ├── Filter/ │ │ │ └── NameFilterIterator.php │ │ ├── ResultCache/ │ │ │ └── DefaultResultCache.php │ │ └── TestSuiteLoader.php │ └── TextUI/ │ ├── Command/ │ │ └── Commands/ │ │ └── WarmCodeCoverageCacheCommand.php │ ├── Output/ │ │ └── Default/ │ │ └── ProgressPrinter/ │ │ └── Subscriber/ │ │ └── TestSkippedSubscriber.php │ └── TestSuiteFilterProcessor.php ├── phpstan-baseline.neon ├── phpstan.neon ├── phpunit.xml ├── rector.php ├── resources/ │ ├── base-phpunit.xml │ └── views/ │ ├── components/ │ │ ├── badge.php │ │ ├── new-line.php │ │ └── two-column-detail.php │ ├── installers/ │ │ └── plugin-browser.php │ ├── usage.php │ └── version.php ├── src/ │ ├── ArchPresets/ │ │ ├── AbstractPreset.php │ │ ├── Custom.php │ │ ├── Laravel.php │ │ ├── Php.php │ │ ├── Relaxed.php │ │ ├── Security.php │ │ └── Strict.php │ ├── Bootstrappers/ │ │ ├── BootExcludeList.php │ │ ├── BootFiles.php │ │ ├── BootKernelDump.php │ │ ├── BootOverrides.php │ │ ├── BootSubscribers.php │ │ └── BootView.php │ ├── Collision/ │ │ └── Events.php │ ├── Concerns/ │ │ ├── Expectable.php │ │ ├── Extendable.php │ │ ├── Logging/ │ │ │ └── WritesToConsole.php │ │ ├── Pipeable.php │ │ ├── Retrievable.php │ │ └── Testable.php │ ├── Configuration/ │ │ ├── Presets.php │ │ ├── Printer.php │ │ └── Project.php │ ├── Configuration.php │ ├── Console/ │ │ ├── Help.php │ │ └── Thanks.php │ ├── Contracts/ │ │ ├── ArchPreset.php │ │ ├── Bootstrapper.php │ │ ├── HasPrintableTestCaseName.php │ │ ├── Panicable.php │ │ ├── Plugins/ │ │ │ ├── AddsOutput.php │ │ │ ├── Bootable.php │ │ │ ├── HandlesArguments.php │ │ │ ├── HandlesOriginalArguments.php │ │ │ └── Terminable.php │ │ ├── TestCaseFilter.php │ │ └── TestCaseMethodFilter.php │ ├── Evaluators/ │ │ └── Attributes.php │ ├── Exceptions/ │ │ ├── AfterAllAlreadyExist.php │ │ ├── AfterAllWithinDescribe.php │ │ ├── AfterBeforeTestFunction.php │ │ ├── BeforeAllAlreadyExist.php │ │ ├── BeforeAllWithinDescribe.php │ │ ├── DatasetAlreadyExists.php │ │ ├── DatasetArgumentsMismatch.php │ │ ├── DatasetDoesNotExist.php │ │ ├── DatasetMissing.php │ │ ├── ExpectationNotFound.php │ │ ├── FatalException.php │ │ ├── FileOrFolderNotFound.php │ │ ├── InvalidArgumentException.php │ │ ├── InvalidExpectation.php │ │ ├── InvalidExpectationValue.php │ │ ├── InvalidOption.php │ │ ├── InvalidPestCommand.php │ │ ├── MissingDependency.php │ │ ├── NoDirtyTestsFound.php │ │ ├── ShouldNotHappen.php │ │ ├── TestAlreadyExist.php │ │ ├── TestCaseAlreadyInUse.php │ │ ├── TestCaseClassOrTraitNotFound.php │ │ ├── TestClosureMustNotBeStatic.php │ │ └── TestDescriptionMissing.php │ ├── Expectation.php │ ├── Expectations/ │ │ ├── EachExpectation.php │ │ ├── HigherOrderExpectation.php │ │ └── OppositeExpectation.php │ ├── Factories/ │ │ ├── Attribute.php │ │ ├── Concerns/ │ │ │ └── HigherOrderable.php │ │ ├── Covers/ │ │ │ ├── CoversClass.php │ │ │ └── CoversFunction.php │ │ ├── TestCaseFactory.php │ │ └── TestCaseMethodFactory.php │ ├── Functions.php │ ├── Installers/ │ │ └── PluginBrowser.php │ ├── Kernel.php │ ├── KernelDump.php │ ├── Logging/ │ │ ├── Converter.php │ │ └── TeamCity/ │ │ ├── ServiceMessage.php │ │ ├── Subscriber/ │ │ │ ├── Subscriber.php │ │ │ ├── TestConsideredRiskySubscriber.php │ │ │ ├── TestErroredSubscriber.php │ │ │ ├── TestExecutionFinishedSubscriber.php │ │ │ ├── TestFailedSubscriber.php │ │ │ ├── TestFinishedSubscriber.php │ │ │ ├── TestPreparedSubscriber.php │ │ │ ├── TestSkippedSubscriber.php │ │ │ ├── TestSuiteFinishedSubscriber.php │ │ │ └── TestSuiteStartedSubscriber.php │ │ └── TeamCityLogger.php │ ├── Matchers/ │ │ └── Any.php │ ├── Mixins/ │ │ └── Expectation.php │ ├── Panic.php │ ├── PendingCalls/ │ │ ├── AfterEachCall.php │ │ ├── BeforeEachCall.php │ │ ├── Concerns/ │ │ │ └── Describable.php │ │ ├── DescribeCall.php │ │ ├── TestCall.php │ │ └── UsesCall.php │ ├── Pest.php │ ├── Plugin.php │ ├── Plugins/ │ │ ├── Actions/ │ │ │ ├── CallsAddsOutput.php │ │ │ ├── CallsBoot.php │ │ │ ├── CallsHandleArguments.php │ │ │ ├── CallsHandleOriginalArguments.php │ │ │ └── CallsTerminable.php │ │ ├── Bail.php │ │ ├── Cache.php │ │ ├── Concerns/ │ │ │ └── HandleArguments.php │ │ ├── Configuration.php │ │ ├── Coverage.php │ │ ├── Environment.php │ │ ├── Help.php │ │ ├── Init.php │ │ ├── Memory.php │ │ ├── Only.php │ │ ├── Parallel/ │ │ │ ├── Contracts/ │ │ │ │ └── HandlersWorkerArguments.php │ │ │ ├── Handlers/ │ │ │ │ ├── Laravel.php │ │ │ │ ├── Parallel.php │ │ │ │ └── Pest.php │ │ │ ├── Paratest/ │ │ │ │ ├── CleanConsoleOutput.php │ │ │ │ ├── ResultPrinter.php │ │ │ │ └── WrapperRunner.php │ │ │ └── Support/ │ │ │ └── CompactPrinter.php │ │ ├── Parallel.php │ │ ├── Printer.php │ │ ├── ProcessIsolation.php │ │ ├── Profile.php │ │ ├── Retry.php │ │ ├── Shard.php │ │ ├── Snapshot.php │ │ ├── Verbose.php │ │ └── Version.php │ ├── Preset.php │ ├── Repositories/ │ │ ├── AfterAllRepository.php │ │ ├── AfterEachRepository.php │ │ ├── BeforeAllRepository.php │ │ ├── BeforeEachRepository.php │ │ ├── DatasetsRepository.php │ │ ├── SnapshotRepository.php │ │ └── TestRepository.php │ ├── Result.php │ ├── Runner/ │ │ └── Filter/ │ │ └── EnsureTestCaseIsInitiatedFilter.php │ ├── Subscribers/ │ │ ├── EnsureConfigurationIsAvailable.php │ │ ├── EnsureIgnorableTestCasesAreIgnored.php │ │ ├── EnsureKernelDumpIsFlushed.php │ │ └── EnsureTeamCityEnabled.php │ ├── Support/ │ │ ├── Arr.php │ │ ├── Backtrace.php │ │ ├── ChainableClosure.php │ │ ├── Closure.php │ │ ├── Container.php │ │ ├── Coverage.php │ │ ├── DatasetInfo.php │ │ ├── Description.php │ │ ├── ExceptionTrace.php │ │ ├── ExpectationPipeline.php │ │ ├── Exporter.php │ │ ├── HigherOrderCallables.php │ │ ├── HigherOrderMessage.php │ │ ├── HigherOrderMessageCollection.php │ │ ├── HigherOrderTapProxy.php │ │ ├── NullClosure.php │ │ ├── Reflection.php │ │ ├── Shell.php │ │ ├── StateGenerator.php │ │ ├── Str.php │ │ └── View.php │ ├── TestCaseFilters/ │ │ └── GitDirtyTestCaseFilter.php │ ├── TestCaseMethodFilters/ │ │ ├── AssigneeTestCaseFilter.php │ │ ├── IssueTestCaseFilter.php │ │ ├── NotesTestCaseFilter.php │ │ ├── PrTestCaseFilter.php │ │ └── TodoTestCaseFilter.php │ ├── TestCases/ │ │ └── IgnorableTestCase.php │ └── TestSuite.php ├── stubs/ │ ├── Browser.php │ ├── Dataset.php │ ├── Feature.php │ ├── Unit.php │ ├── init/ │ │ ├── Feature/ │ │ │ └── ExampleTest.php.stub │ │ ├── Pest.php.stub │ │ ├── TestCase.php.stub │ │ ├── Unit/ │ │ │ └── ExampleTest.php.stub │ │ └── phpunit.xml.stub │ └── init-laravel/ │ ├── Feature/ │ │ └── ExampleTest.php.stub │ ├── Pest.php.stub │ ├── TestCase.php.stub │ ├── Unit/ │ │ └── ExampleTest.php.stub │ └── phpunit.xml.stub ├── tests/ │ ├── .cache/ │ │ └── test-results │ ├── .pest/ │ │ ├── snapshots/ │ │ │ ├── Features/ │ │ │ │ └── Expect/ │ │ │ │ └── toMatchSnapshot/ │ │ │ │ ├── _describable__→_multiple_snapshot_expectations_with_describe.snap │ │ │ │ ├── _describable__→_multiple_snapshot_expectations_with_describe__2.snap │ │ │ │ ├── _within_describe__→_pass_with_dataset_with_data_set____my_datas_set_value___.snap │ │ │ │ ├── failures.snap │ │ │ │ ├── failures_with_custom_message.snap │ │ │ │ ├── multiple_snapshot_expectations.snap │ │ │ │ ├── multiple_snapshot_expectations__2.snap │ │ │ │ ├── multiple_snapshot_expectations_with_datasets_with_data_set___1__.snap │ │ │ │ ├── multiple_snapshot_expectations_with_datasets_with_data_set___1____2.snap │ │ │ │ ├── multiple_snapshot_expectations_with_datasets_with_data_set____bar___.snap │ │ │ │ ├── multiple_snapshot_expectations_with_datasets_with_data_set____bar_____2.snap │ │ │ │ ├── multiple_snapshot_expectations_with_datasets_with_data_set____baz___.snap │ │ │ │ ├── multiple_snapshot_expectations_with_datasets_with_data_set____baz_____2.snap │ │ │ │ ├── multiple_snapshot_expectations_with_datasets_with_data_set____foo___.snap │ │ │ │ ├── multiple_snapshot_expectations_with_datasets_with_data_set____foo_____2.snap │ │ │ │ ├── multiple_snapshot_expectations_with_repeat_with_data_set___10__.snap │ │ │ │ ├── multiple_snapshot_expectations_with_repeat_with_data_set___10____2.snap │ │ │ │ ├── multiple_snapshot_expectations_with_repeat_with_data_set___1__.snap │ │ │ │ ├── multiple_snapshot_expectations_with_repeat_with_data_set___1____2.snap │ │ │ │ ├── multiple_snapshot_expectations_with_repeat_with_data_set___2__.snap │ │ │ │ ├── multiple_snapshot_expectations_with_repeat_with_data_set___2____2.snap │ │ │ │ ├── multiple_snapshot_expectations_with_repeat_with_data_set___3__.snap │ │ │ │ ├── multiple_snapshot_expectations_with_repeat_with_data_set___3____2.snap │ │ │ │ ├── multiple_snapshot_expectations_with_repeat_with_data_set___4__.snap │ │ │ │ ├── multiple_snapshot_expectations_with_repeat_with_data_set___4____2.snap │ │ │ │ ├── multiple_snapshot_expectations_with_repeat_with_data_set___5__.snap │ │ │ │ ├── multiple_snapshot_expectations_with_repeat_with_data_set___5____2.snap │ │ │ │ ├── multiple_snapshot_expectations_with_repeat_with_data_set___6__.snap │ │ │ │ ├── multiple_snapshot_expectations_with_repeat_with_data_set___6____2.snap │ │ │ │ ├── multiple_snapshot_expectations_with_repeat_with_data_set___7__.snap │ │ │ │ ├── multiple_snapshot_expectations_with_repeat_with_data_set___7____2.snap │ │ │ │ ├── multiple_snapshot_expectations_with_repeat_with_data_set___8__.snap │ │ │ │ ├── multiple_snapshot_expectations_with_repeat_with_data_set___8____2.snap │ │ │ │ ├── multiple_snapshot_expectations_with_repeat_with_data_set___9__.snap │ │ │ │ ├── multiple_snapshot_expectations_with_repeat_with_data_set___9____2.snap │ │ │ │ ├── not_failures.snap │ │ │ │ ├── pass.snap │ │ │ │ ├── pass_using_pipes.snap │ │ │ │ ├── pass_with______toString_.snap │ │ │ │ ├── pass_with__toArray_.snap │ │ │ │ ├── pass_with__toSnapshot_.snap │ │ │ │ ├── pass_with__toString_.snap │ │ │ │ ├── pass_with_array.snap │ │ │ │ └── pass_with_dataset_with_data_set____my_datas_set_value___.snap │ │ │ └── Visual/ │ │ │ ├── Collision/ │ │ │ │ └── collision_with_data_set_________.snap │ │ │ ├── Help/ │ │ │ │ └── visual_snapshot_of_help_command_output.snap │ │ │ ├── Todo/ │ │ │ │ ├── todo.snap │ │ │ │ ├── todo_in_parallel.snap │ │ │ │ ├── todos.snap │ │ │ │ └── todos_in_parallel.snap │ │ │ └── Version/ │ │ │ └── visual_snapshot_of_help_command_output.snap │ │ └── snapshots-external/ │ │ └── Features/ │ │ └── Expect/ │ │ └── toMatchSnapshot/ │ │ ├── _within_describe__→_pass_with_dataset___my_datas_set_value___with_data___my_datas_set_value__.snap │ │ ├── _within_describe__→_pass_with_dataset_with_data_set_____my_datas_set_value___.snap │ │ ├── _within_describe__→_pass_with_dataset_with_data_set____my_datas_set_value___.snap │ │ ├── pass_with_dataset___my_datas_set_value___with_data___my_datas_set_value__.snap │ │ ├── pass_with_dataset_with_data_set_____my_datas_set_value___.snap │ │ └── pass_with_dataset_with_data_set____my_datas_set_value___.snap │ ├── .snapshots/ │ │ ├── Failure.php.inc │ │ ├── SuccessOnly.php.inc │ │ ├── allows-to-run-a-directory.txt │ │ ├── allows-to-run-a-single-test.txt │ │ ├── disable-decorating-printer.txt │ │ └── success.txt │ ├── .tests/ │ │ ├── Failure.php │ │ └── SuccessOnly.php │ ├── Arch.php │ ├── Autoload.php │ ├── Datasets/ │ │ ├── Bound.php │ │ └── Numbers.php │ ├── Environments/ │ │ └── Windows.php │ ├── Features/ │ │ ├── After.php │ │ ├── AfterAll.php │ │ ├── AfterEach.php │ │ ├── Assignee.php │ │ ├── BeforeAll.php │ │ ├── BeforeEach.php │ │ ├── BeforeEachProxiesToTestCallWithExpectations.php │ │ ├── BeforeEachProxiesToTestCallWithSkip.php │ │ ├── BeforeEachProxiesToTestCallWithTodo.php │ │ ├── Coverage.php │ │ ├── Covers/ │ │ │ ├── ClassCoverage.php │ │ │ ├── ExceptionHandling.php │ │ │ ├── FunctionCoverage.php │ │ │ ├── GuessCoverage.php │ │ │ └── TraitCoverage.php │ │ ├── DatasetsTests.php │ │ ├── Depends.php │ │ ├── DependsInheritance.php │ │ ├── Deprecated.php │ │ ├── Describe.php │ │ ├── DescriptionLess.php │ │ ├── Done.php │ │ ├── Exceptions.php │ │ ├── Expect/ │ │ │ ├── HigherOrder/ │ │ │ │ ├── methods.php │ │ │ │ ├── methodsAndProperties.php │ │ │ │ └── properties.php │ │ │ ├── each.php │ │ │ ├── extend.php │ │ │ ├── json.php │ │ │ ├── matchExpectation.php │ │ │ ├── not.php │ │ │ ├── pipes.php │ │ │ ├── ray.php │ │ │ ├── sequence.php │ │ │ ├── toBe.php │ │ │ ├── toBeAlpha.php │ │ │ ├── toBeAlphaNumeric.php │ │ │ ├── toBeArray.php │ │ │ ├── toBeBetween.php │ │ │ ├── toBeBool.php │ │ │ ├── toBeCallable.php │ │ │ ├── toBeCamelCase.php │ │ │ ├── toBeDigits.php │ │ │ ├── toBeDirectory.php │ │ │ ├── toBeEmpty.php │ │ │ ├── toBeFalse.php │ │ │ ├── toBeFalsy.php │ │ │ ├── toBeFile.php │ │ │ ├── toBeFloat.php │ │ │ ├── toBeGreaterThan.php │ │ │ ├── toBeGreaterThanOrEqual.php │ │ │ ├── toBeIn.php │ │ │ ├── toBeInfinite.php │ │ │ ├── toBeInstanceOf.php │ │ │ ├── toBeInt.php │ │ │ ├── toBeIntBackedEnum.php │ │ │ ├── toBeInvokable.php │ │ │ ├── toBeIterable.php │ │ │ ├── toBeJson.php │ │ │ ├── toBeKebabCase.php │ │ │ ├── toBeLessThan.php │ │ │ ├── toBeLessThanOrEqual.php │ │ │ ├── toBeList.php │ │ │ ├── toBeLowercase.php │ │ │ ├── toBeNAN.php │ │ │ ├── toBeNull.php │ │ │ ├── toBeNumeric.php │ │ │ ├── toBeObject.php │ │ │ ├── toBeReadableDirectory.php │ │ │ ├── toBeReadableFile.php │ │ │ ├── toBeResource.php │ │ │ ├── toBeScalar.php │ │ │ ├── toBeSlug.php │ │ │ ├── toBeSnakeCase.php │ │ │ ├── toBeString.php │ │ │ ├── toBeStringBackedEnum.php │ │ │ ├── toBeStudlyCase.php │ │ │ ├── toBeTrue.php │ │ │ ├── toBeTruthy.php │ │ │ ├── toBeUppercase.php │ │ │ ├── toBeUrl.php │ │ │ ├── toBeUuid.php │ │ │ ├── toBeWritableDirectory.php │ │ │ ├── toBeWritableFile.php │ │ │ ├── toContain.php │ │ │ ├── toContainEqual.php │ │ │ ├── toContainOnlyInstancesOf.php │ │ │ ├── toEndWith.php │ │ │ ├── toEqual.php │ │ │ ├── toEqualCanonicalizing.php │ │ │ ├── toEqualWithDelta.php │ │ │ ├── toHaveAttribute.php │ │ │ ├── toHaveCamelCaseKeys.php │ │ │ ├── toHaveConstructor.php │ │ │ ├── toHaveCount.php │ │ │ ├── toHaveDestructor.php │ │ │ ├── toHaveFileSystemPermissions.php │ │ │ ├── toHaveKebabCaseKeys.php │ │ │ ├── toHaveKey.php │ │ │ ├── toHaveKeys.php │ │ │ ├── toHaveLength.php │ │ │ ├── toHaveLineCountLessThan.php │ │ │ ├── toHaveMethod.php │ │ │ ├── toHaveMethods.php │ │ │ ├── toHaveMethodsDocumented.php │ │ │ ├── toHavePrefix.php │ │ │ ├── toHavePrivateMethodsBesides.php │ │ │ ├── toHaveProperties.php │ │ │ ├── toHavePropertiesDocumented.php │ │ │ ├── toHaveProperty.php │ │ │ ├── toHaveProtectedMethodsBesides.php │ │ │ ├── toHavePublicMethodsBesides.php │ │ │ ├── toHaveSameSize.php │ │ │ ├── toHaveSnakeCaseKeys.php │ │ │ ├── toHaveStudlyCaseKeys.php │ │ │ ├── toHaveSuffix.php │ │ │ ├── toMatch.php │ │ │ ├── toMatchArray.php │ │ │ ├── toMatchConstraint.php │ │ │ ├── toMatchObject.php │ │ │ ├── toMatchSnapshot.php │ │ │ ├── toStartWith.php │ │ │ ├── toThrow.php │ │ │ ├── toUseStrictEquality.php │ │ │ ├── toUseStrictTypes.php │ │ │ ├── toUseTrait.php │ │ │ ├── unless.php │ │ │ └── when.php │ │ ├── Fail.php │ │ ├── Fails.php │ │ ├── Fixture.php │ │ ├── Helpers.php │ │ ├── HigherOrderTests.php │ │ ├── Incompleted.php │ │ ├── Issue.php │ │ ├── It.php │ │ ├── Note.php │ │ ├── Notices.php │ │ ├── Pr.php │ │ ├── References.php │ │ ├── Repeat.php │ │ ├── ScopedDatasets/ │ │ │ ├── Directory/ │ │ │ │ ├── Datasets/ │ │ │ │ │ └── Scoped.php │ │ │ │ ├── NestedDirectory1/ │ │ │ │ │ ├── Datasets.php │ │ │ │ │ └── TestFileInNestedDirectoryWithDatasetsFile.php │ │ │ │ ├── NestedDirectory2/ │ │ │ │ │ └── TestFileInNestedDirectory.php │ │ │ │ ├── TestFileWithLocallyDefinedDataset.php │ │ │ │ └── TestFileWithScopedDataset.php │ │ │ └── TestFileOutOfScope.php │ │ ├── See.php │ │ ├── Skip.php │ │ ├── SkipOnPhp.php │ │ ├── Test.php │ │ ├── TestCycle.php │ │ ├── ThrowsNoExceptions.php │ │ ├── Ticket.php │ │ ├── Todo.php │ │ ├── Warnings.php │ │ └── Wip.php │ ├── Fixtures/ │ │ ├── Arch/ │ │ │ ├── ToBeIntBackedEnum/ │ │ │ │ ├── HasIntBacking/ │ │ │ │ │ └── HasIntBackingEnum.php │ │ │ │ └── HasStringBacking/ │ │ │ │ └── HasStringBackingEnum.php │ │ │ ├── ToBeInvokable/ │ │ │ │ ├── IsInvokable/ │ │ │ │ │ ├── InvokableClass.php │ │ │ │ │ ├── InvokableClassViaParent.php │ │ │ │ │ ├── InvokableClassViaTrait.php │ │ │ │ │ ├── InvokableTrait.php │ │ │ │ │ └── ParentInvokableClass.php │ │ │ │ └── IsNotInvokable/ │ │ │ │ └── IsNotInvokableClass.php │ │ │ ├── ToBeStringBackedEnum/ │ │ │ │ ├── HasIntBacking/ │ │ │ │ │ └── HasIntBackingEnum.php │ │ │ │ └── HasStringBacking/ │ │ │ │ └── HasStringBackingEnum.php │ │ │ ├── ToHaveAttribute/ │ │ │ │ ├── Attributes/ │ │ │ │ │ └── AsAttribute.php │ │ │ │ ├── HaveAttribute/ │ │ │ │ │ └── HaveAttributeClass.php │ │ │ │ └── NotHaveAttribute/ │ │ │ │ └── NotHaveAttributeClass.php │ │ │ ├── ToHaveConstructor/ │ │ │ │ ├── HasConstructor/ │ │ │ │ │ └── HasConstructor.php │ │ │ │ └── HasNoConstructor/ │ │ │ │ └── HasNoConstructor.php │ │ │ ├── ToHaveDestructor/ │ │ │ │ ├── HasDestructor/ │ │ │ │ │ └── HasDestructor.php │ │ │ │ └── HasNoDestructor/ │ │ │ │ └── HasNoDestructor.php │ │ │ ├── ToHaveMethod/ │ │ │ │ ├── HasMethod/ │ │ │ │ │ ├── HasMethod.php │ │ │ │ │ ├── HasMethodTrait.php │ │ │ │ │ ├── HasMethodViaParent.php │ │ │ │ │ ├── HasMethodViaTrait.php │ │ │ │ │ └── ParentHasMethodClass.php │ │ │ │ └── HasNoMethod/ │ │ │ │ └── HasNoMethodClass.php │ │ │ ├── ToHavePrefix/ │ │ │ │ ├── HasNoPrefix/ │ │ │ │ │ └── ClassWithout.php │ │ │ │ └── HasPrefix/ │ │ │ │ └── PrefixClassWith.php │ │ │ ├── ToHavePublicMethodsBesides/ │ │ │ │ └── UserController.php │ │ │ ├── ToHaveSuffix/ │ │ │ │ ├── HasNoSuffix/ │ │ │ │ │ └── ClassWithout.php │ │ │ │ └── HasSuffix/ │ │ │ │ └── ClassWithSuffix.php │ │ │ ├── ToUseStrictEquality/ │ │ │ │ ├── NotStrictEquality.php │ │ │ │ └── StrictEquality.php │ │ │ └── ToUseStrictTypes/ │ │ │ ├── HasNoStrictType.php │ │ │ ├── HasStrictType.php │ │ │ └── HasStrictTypeWithCommentsAbove.php │ │ ├── CollisionTest.php │ │ ├── Covers/ │ │ │ ├── CoversClass1.php │ │ │ ├── CoversClass2.php │ │ │ ├── CoversClass3.php │ │ │ └── CoversTrait.php │ │ ├── DirectoryWithTests/ │ │ │ └── ExampleTest.php │ │ ├── ExampleTest.php │ │ ├── Inheritance/ │ │ │ ├── Base/ │ │ │ │ └── ExampleTest.php │ │ │ └── ExampleTest.php │ │ ├── UnexpectedOutput.php │ │ ├── phpunit-in-isolation.xml │ │ └── phpunit-not-in-isolation.xml │ ├── Helpers/ │ │ ├── Helper.php │ │ └── TestInHelpers.php │ ├── Helpers.php │ ├── Hooks/ │ │ ├── AfterEachTest.php │ │ ├── BeforeAllTest.php │ │ └── BeforeEachTest.php │ ├── PHPUnit/ │ │ ├── CustomAffixes/ │ │ │ ├── @#$%^&()-_=+.php │ │ │ ├── A Test With Spaces.php │ │ │ ├── AdditionalFileExtension.spec.php │ │ │ ├── FolderWithAn@/ │ │ │ │ └── ExampleTest.php │ │ │ ├── ManyExtensions.class.test.php │ │ │ ├── Test 'Case' With Quotes.php │ │ │ ├── kebab-case-spec.php │ │ │ └── snake_case_spec.php │ │ ├── CustomTestCase/ │ │ │ ├── ChildTest.php │ │ │ ├── CustomTestCase.php │ │ │ ├── ExecutedTest.php │ │ │ ├── ParentTest.php │ │ │ └── UsesPerDirectory.php │ │ ├── CustomTestCaseInSubFolders/ │ │ │ ├── SubFolder/ │ │ │ │ └── SubFolder/ │ │ │ │ ├── CustomTestCaseInSubFolder.php │ │ │ │ └── UsesPerSubDirectory.php │ │ │ └── SubFolder2/ │ │ │ └── UsesPerFile.php │ │ ├── GlobPatternTests/ │ │ │ ├── SubFolder/ │ │ │ │ └── InnerFolder/ │ │ │ │ └── UsesPerDirectoryAsPattern.php │ │ │ └── SubFolder2/ │ │ │ └── UsesPerFileAsPattern.php │ │ └── IgnorableTest.php │ ├── Pest.php │ ├── Playground.php │ ├── Plugins/ │ │ ├── Coverage.php │ │ └── Traits.php │ ├── Unit/ │ │ ├── Configuration/ │ │ │ ├── In.php │ │ │ └── Theme.php │ │ ├── Console/ │ │ │ └── Help.php │ │ ├── DatasetsTests.php │ │ ├── Expectations/ │ │ │ └── OppositeExpectation.php │ │ ├── Overrides/ │ │ │ └── ThrowableBuilder.php │ │ ├── Plugins/ │ │ │ ├── Concerns/ │ │ │ │ └── HandleArguments.php │ │ │ ├── Environment.php │ │ │ └── Retry.php │ │ ├── Preset.php │ │ ├── Support/ │ │ │ ├── Arr.php │ │ │ ├── Backtrace.php │ │ │ ├── Container.php │ │ │ ├── DatasetInfo.php │ │ │ ├── ExceptionTrace.php │ │ │ ├── HigherOrderMessage.php │ │ │ ├── Reflection.php │ │ │ └── Str.php │ │ ├── TestName.php │ │ └── TestSuite.php │ └── Visual/ │ ├── BeforeEachTestName.php │ ├── Collision.php │ ├── Help.php │ ├── JUnit.php │ ├── Parallel.php │ ├── SingleTestOrDirectory.php │ ├── Success.php │ ├── TeamCity.php │ ├── Todo.php │ └── Version.php └── tests-external/ └── Features/ └── Expect/ └── toMatchSnapshot.php ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ ; This file is for unifying the coding style for different editors and IDEs. ; More information at http://editorconfig.org root = true [*] charset = utf-8 end_of_line = lf insert_final_newline = true indent_style = space indent_size = 4 trim_trailing_whitespace = true [*.md] trim_trailing_whitespace = false [*.{yml,yaml}] indent_size = 2 ================================================ FILE: .gitattributes ================================================ /docker export-ignore /docs export-ignore /tests export-ignore /scripts export-ignore /.github export-ignore .editorconfig export-ignore .gitattributes export-ignore .gitignore export-ignore /phpstan.neon export-ignore /phpunit.xml export-ignore /CHANGELOG.md export-ignore /CONTRIBUTING.md export-ignore /docker-compose.yml export-ignore /Makefile export-ignore /rector.php export-ignore /README.md export-ignore /RELEASE.md export-ignore ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms github: [nunomaduro] custom: https://www.paypal.com/paypalme/enunomaduro ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.yml ================================================ name: Bug Report description: Report an Issue or Bug with the Pest title: "[Bug]: " labels: ["bug"] body: - type: markdown attributes: value: | We're sorry to hear you have a problem. Can you help us solve it by providing the following details. - type: textarea id: what-happened attributes: label: What Happened description: What did you expect to happen? placeholder: When I use expect()->toBeTrue() in my tests, I get an error validations: required: true - type: textarea id: how-to-reproduce attributes: label: How to Reproduce description: How did this occur, please add any config values used and provide a set of reliable steps if possible. placeholder: Install a fresh Laravel app, add Pest, add a test that uses expect()->toBeTrue() validations: required: true - type: input id: repository-sample attributes: label: Sample Repository description: If possible, please provide a sample repository that reproduces the issue. placeholder: https://github.com.br/your-username/your-repository - type: input id: pest-version attributes: label: Pest Version description: What version of our Package are you running? Please be as specific as possible placeholder: 2.14.1 validations: required: true - type: input id: php-version attributes: label: PHP Version description: What version of PHP are you running? Please be as specific as possible placeholder: 8.1.20 validations: required: true - type: dropdown id: operating-systems attributes: label: Operation System description: On which operating systems does the problem occur? You can select more than one. multiple: true options: - macOS - Windows - Linux validations: required: true - type: textarea id: notes attributes: label: Notes description: Use this field to provide any other notes that you feel might be relevant to the issue. validations: required: false ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ ### What: - [ ] Bug Fix - [ ] New Feature ### Description: ### Related: ================================================ FILE: .github/workflows/static.yml ================================================ 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' name: Static Tests runs-on: ubuntu-latest strategy: fail-fast: true matrix: dependency-version: [prefer-lowest, prefer-stable] steps: - name: Checkout uses: actions/checkout@v6 - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: 8.3 tools: composer:v2 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 --${{ matrix.dependency-version }} --no-interaction --no-progress --ansi - name: Profanity Check run: composer test:profanity - name: Type Check run: composer test:type:check - name: Type Coverage run: composer test:type:coverage - name: Refacto run: composer test:refacto - name: Style run: composer test:lint ================================================ FILE: .github/workflows/tests.yml ================================================ 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' runs-on: ${{ matrix.os }} strategy: fail-fast: true matrix: os: [ubuntu-latest, macos-latest] # windows-latest 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@v6 - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} tools: composer:v2 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" echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" - name: Install PHP dependencies shell: bash run: composer update --${{ matrix.dependency_version }} --no-interaction --no-progress --ansi --with="symfony/console:^${{ matrix.symfony }}" - name: Unit Tests run: composer test:unit - name: Parallel Tests run: composer test:parallel - name: Integration Tests run: composer test:integration ================================================ FILE: .gitignore ================================================ .idea/* .idea/codeStyleSettings.xml .temp/* composer.lock /vendor/ coverage.xml .phpunit.result.cache .phpunit.cache /.php-cs-fixer.php .php-cs-fixer.cache .temp/coverage.php *.swp *.swo .vscode/ .STREAM.md ================================================ FILE: CONTRIBUTING.md ================================================ # CONTRIBUTING Contributions are welcome, and are accepted via pull requests. Please review these guidelines before submitting any pull requests. ## Process 1. Fork the project 1. Create a new branch 1. Code, test, commit and push 1. Open a pull request detailing your changes. Make sure to follow the [template](.github/PULL_REQUEST_TEMPLATE.md) ## Guidelines * Please ensure the coding style running `composer lint`. * Send a coherent commit history, making sure each individual commit in your pull request is meaningful. * You may need to [rebase](https://git-scm.com/book/en/v2/Git-Branching-Rebasing) to avoid merge conflicts. * Please remember that we follow [SemVer](http://semver.org/). ## Setup Clone your fork, then install the dev dependencies: ```bash composer install ``` ## Lint Lint your code: ```bash composer lint ``` ## Tests Update the snapshots: ```bash composer update:snapshots ``` Run all tests: ```bash composer test ``` Check types: ```bash composer test:type:check ``` Unit tests: ```bash composer test:unit ``` Integration tests: ```bash composer test:integration ``` ## Simplified setup using Docker If you have Docker installed, you can quickly get all dependencies for Pest in place using our Docker files. Assuming you have the repository cloned, you may run the following commands: 1. `make build` to build the Docker image 2. `make install` to install Composer dependencies 3. `make test` to run the project tests and analysis tools If you want to check things work against a specific version of PHP, you may include the `PHP` build argument when building the image: ```bash make build ARGS="--build-arg PHP=8.3" ``` The default PHP version will always be the lowest version of PHP supported by Pest. ================================================ FILE: LICENSE.md ================================================ The MIT License (MIT) Copyright (c) Nuno Maduro 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. ================================================ FILE: README.md ================================================

PEST

GitHub Workflow Status (master) Total Downloads Latest Version License Why PHP in 2026

------ > Pest v4 Now Available: **[Read the announcement »](https://pestphp.com/docs/pest-v4-is-here-now-with-browser-testing)**. **Pest** is an elegant PHP testing Framework with a focus on simplicity, meticulously designed to bring back the joy of testing in PHP. - Explore our docs at **[pestphp.com »](https://pestphp.com)** - Follow the creator Nuno Maduro: - YouTube: **[youtube.com/@nunomaduro](https://youtube.com/@nunomaduro)** — Videos every week - Twitch: **[twitch.tv/nunomaduro](https://twitch.tv/nunomaduro)** — Live coding on Mondays, Wednesdays, and Fridays at 9PM UTC - Twitter / X: **[x.com/enunomaduro](https://x.com/enunomaduro)** - LinkedIn: **[linkedin.com/in/nunomaduro](https://www.linkedin.com/in/nunomaduro)** - Instagram: **[instagram.com/enunomaduro](https://www.instagram.com/enunomaduro)** - Tiktok: **[tiktok.com/@enunomaduro](https://www.tiktok.com/@enunomaduro)** ## Sponsors We cannot thank our sponsors enough for their incredible support in funding Pest's development. Their contributions have been instrumental in making Pest the best it can be. For those who are interested in becoming a sponsor, please visit Nuno Maduro's Sponsor page at **[github.com/sponsors/nunomaduro](https://github.com/sponsors/nunomaduro)**. ### Platinum Sponsors - **[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 - **[CMS Max](https://cmsmax.com/?ref=pestphp)** ### Premium Sponsors - [Zapiet](https://zapiet.com/?ref=pestphp) - [Load Forge](https://loadforge.com/?ref=pestphp) - [Route4Me](https://route4me.com/pt?ref=pestphp) - [Nerdify](https://getnerdify.com/?ref=pestphp) - [Akaunting](https://akaunting.com/?ref=pestphp) - [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)**. ================================================ FILE: RELEASE.md ================================================ # Release process When releasing a new version of Pest there are some checks and updates that need to be done: > **For Pest v3 you should use the `3.x` branch instead.** - Clear your local repository with: `git add . && git reset --hard && git checkout 4.x` - On the GitHub repository, check the contents of [github.com/pestphp/pest/compare/{latest_version}...4.x](https://github.com/pestphp/pest/compare/{latest_version}...4.x) - Update the version number in [src/Pest.php](src/Pest.php) - Run the tests locally using: `composer test` - Commit the Pest file with the message: `git commit -m "release: vX.X.X"` - Push the changes to GitHub - Check that the CI is passing as expected: [github.com/pestphp/pest/actions](https://github.com/pestphp/pest/actions) - Tag and push the tag with `git tag vX.X.X && git push --tags` - Publish release here: [github.com/pestphp/pest/releases/new](https://github.com/pestphp/pest/releases/new). ### Plugins Plugins should be versioned using the same major (or minor for `0.x` releases) version as Pest core. ================================================ FILE: bin/pest ================================================ #!/usr/bin/env php $value) { if ($value === '--compact') { $_SERVER['COLLISION_PRINTER_COMPACT'] = 'true'; unset($arguments[$key]); } if ($value === '--profile') { $_SERVER['COLLISION_PRINTER_PROFILE'] = 'true'; unset($arguments[$key]); } if (str_contains($value, '--test-directory=')) { unset($arguments[$key]); } elseif ($value === '--test-directory') { unset($arguments[$key]); if (isset($arguments[$key + 1])) { unset($arguments[$key + 1]); } } if ($value === '--dirty') { $dirty = true; unset($arguments[$key]); } if (in_array($value, ['--todo', '--todos'], true)) { $todo = true; unset($arguments[$key]); } if ($value === '--notes') { $notes = true; unset($arguments[$key]); } if (str_contains($value, '--assignee=')) { unset($arguments[$key]); } elseif ($value === '--assignee') { unset($arguments[$key]); if (isset($arguments[$key + 1])) { unset($arguments[$key + 1]); } } if (str_contains($value, '--issue=')) { unset($arguments[$key]); } elseif ($value === '--issue') { unset($arguments[$key]); if (isset($arguments[$key + 1])) { unset($arguments[$key + 1]); } } if (str_contains($value, '--ticket=')) { unset($arguments[$key]); } elseif ($value === '--ticket') { unset($arguments[$key]); if (isset($arguments[$key + 1])) { unset($arguments[$key + 1]); } } if (str_contains($value, '--pr=')) { unset($arguments[$key]); } elseif ($value === '--pr') { unset($arguments[$key]); if (isset($arguments[$key + 1])) { unset($arguments[$key + 1]); } } if (str_contains($value, '--pull-request=')) { unset($arguments[$key]); } elseif ($value === '--pull-request') { unset($arguments[$key]); if (isset($arguments[$key + 1])) { unset($arguments[$key + 1]); } } if (str_contains($value, '--teamcity')) { unset($arguments[$key]); $arguments[] = '--no-output'; unset($_SERVER['COLLISION_PRINTER']); } } // Used when Pest is required using composer. $vendorPath = dirname(__DIR__, 4).'/vendor/autoload.php'; // Used when Pest maintainers are running Pest tests. $localPath = dirname(__DIR__).'/vendor/autoload.php'; if (file_exists($vendorPath)) { include_once $vendorPath; $autoloadPath = $vendorPath; } else { include_once $localPath; $autoloadPath = $localPath; } // Get $rootPath based on $autoloadPath $rootPath = dirname($autoloadPath, 2); $input = new ArgvInput; $testSuite = TestSuite::getInstance( $rootPath, $input->getParameterOption('--test-directory', 'tests'), ); if ($dirty) { $testSuite->tests->addTestCaseFilter(new GitDirtyTestCaseFilter($rootPath)); } if ($todo) { $testSuite->tests->addTestCaseMethodFilter(new TodoTestCaseFilter); } if ($notes) { $testSuite->tests->addTestCaseMethodFilter(new NotesTestCaseFilter); } if ($assignee = $input->getParameterOption('--assignee')) { $testSuite->tests->addTestCaseMethodFilter(new AssigneeTestCaseFilter((string) $assignee)); } if ($issue = $input->getParameterOption('--issue')) { $testSuite->tests->addTestCaseMethodFilter(new IssueTestCaseFilter((int) $issue)); } if ($issue = $input->getParameterOption('--ticket')) { $testSuite->tests->addTestCaseMethodFilter(new IssueTestCaseFilter((int) $issue)); } if ($pr = $input->getParameterOption('--pr')) { $testSuite->tests->addTestCaseMethodFilter(new PrTestCaseFilter((int) $pr)); } if ($pr = $input->getParameterOption('--pull-request')) { $testSuite->tests->addTestCaseMethodFilter(new PrTestCaseFilter((int) $pr)); } $isDecorated = $input->getParameterOption('--colors', 'always') !== 'never'; $output = new ConsoleOutput(ConsoleOutput::VERBOSITY_NORMAL, $isDecorated); try { $kernel = Kernel::boot($testSuite, $input, $output); $result = $kernel->handle($originalArguments, $arguments); $kernel->terminate(); } catch (Throwable|Error $e) { Panic::with($e); } exit($result); })(); ================================================ FILE: bin/worker.php ================================================ getParameterOption( '--test-directory', 'tests' )); $input = new ArgvInput; $output = new ConsoleOutput(OutputInterface::VERBOSITY_NORMAL, true); Kernel::boot($testSuite, $input, $output); }); (static function () use ($bootPest): void { $getopt = getopt('', [ 'status-file:', 'progress-file:', 'unexpected-output-file:', 'test-result-file:', 'result-cache-file:', 'teamcity-file:', 'testdox-file:', 'testdox-color', 'testdox-columns:', 'testdox-summary', 'phpunit-argv:', ]); $composerAutoloadFiles = [ dirname(__DIR__, 3).DIRECTORY_SEPARATOR.'autoload.php', dirname(__DIR__, 2).DIRECTORY_SEPARATOR.'vendor'.DIRECTORY_SEPARATOR.'autoload.php', dirname(__DIR__).DIRECTORY_SEPARATOR.'vendor'.DIRECTORY_SEPARATOR.'autoload.php', ]; foreach ($composerAutoloadFiles as $file) { if (file_exists($file)) { require_once $file; define('PHPUNIT_COMPOSER_INSTALL', $file); break; } } assert(isset($getopt['status-file']) && is_string($getopt['status-file'])); $statusFile = fopen($getopt['status-file'], 'wb'); assert(is_resource($statusFile)); assert(isset($getopt['progress-file']) && is_string($getopt['progress-file'])); assert(isset($getopt['unexpected-output-file']) && is_string($getopt['unexpected-output-file'])); assert(isset($getopt['test-result-file']) && is_string($getopt['test-result-file'])); assert(! isset($getopt['result-cache-file']) || is_string($getopt['result-cache-file'])); assert(! isset($getopt['teamcity-file']) || is_string($getopt['teamcity-file'])); assert(! isset($getopt['testdox-file']) || is_string($getopt['testdox-file'])); assert(isset($getopt['phpunit-argv']) && is_string($getopt['phpunit-argv'])); $phpunitArgv = unserialize($getopt['phpunit-argv'], ['allowed_classes' => false]); assert(is_array($phpunitArgv)); $bootPest(); $phpunitArgv = CallsHandleArguments::execute($phpunitArgv); $application = new ApplicationForWrapperWorker( $phpunitArgv, $getopt['progress-file'], $getopt['unexpected-output-file'], $getopt['test-result-file'], $getopt['result-cache-file'] ?? null, $getopt['teamcity-file'] ?? null, $getopt['testdox-file'] ?? null, isset($getopt['testdox-color']), (int) ($getopt['testdox-columns'] ?? null), ); while (true) { if (feof(STDIN)) { $application->end(); exit; } $testPath = fgets(STDIN); if ($testPath === false || $testPath === WrapperWorker::COMMAND_EXIT) { $application->end(); 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))); fwrite($statusFile, (string) $exitCode); fflush($statusFile); } })(); ================================================ FILE: composer.json ================================================ { "name": "pestphp/pest", "description": "The elegant PHP Testing Framework.", "keywords": [ "php", "framework", "pest", "unit", "test", "testing" ], "license": "MIT", "authors": [ { "name": "Nuno Maduro", "email": "enunomaduro@gmail.com" } ], "require": { "php": "^8.3.0", "brianium/paratest": "^7.19.0", "nunomaduro/collision": "^8.9.1", "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.2.1", "phpunit/phpunit": "^12.5.12", "symfony/process": "^7.4.5|^8.0.5" }, "conflict": { "filp/whoops": "<2.18.3", "phpunit/phpunit": ">12.5.12", "sebastian/exporter": "<7.0.0", "webmozart/assert": "<1.11.0" }, "autoload": { "psr-4": { "Pest\\": "src/" }, "files": [ "src/Functions.php", "src/Pest.php" ] }, "autoload-dev": { "psr-4": { "Tests\\Fixtures\\Covers\\": "tests/Fixtures/Covers", "Tests\\Fixtures\\Inheritance\\": "tests/Fixtures/Inheritance", "Tests\\Fixtures\\Arch\\": "tests/Fixtures/Arch", "Tests\\": "tests/PHPUnit/" }, "files": [ "tests/Autoload.php" ] }, "require-dev": { "pestphp/pest-dev-tools": "^4.1.0", "pestphp/pest-plugin-browser": "^4.3.0", "pestphp/pest-plugin-type-coverage": "^4.0.3", "psy/psysh": "^0.12.21" }, "minimum-stability": "dev", "prefer-stable": true, "config": { "sort-packages": true, "preferred-install": "dist", "allow-plugins": { "pestphp/pest-plugin": true } }, "bin": [ "bin/pest" ], "scripts": { "refacto": "rector", "lint": "pint --parallel", "test:refacto": "rector --dry-run", "test:lint": "pint --parallel --test", "test:profanity": "php bin/pest --profanity --compact", "test:type:check": "phpstan analyse --ansi --memory-limit=-1 --debug", "test:type:coverage": "php -d memory_limit=-1 bin/pest --type-coverage --min=100", "test:unit": "php bin/pest --exclude-group=integration --compact", "test:inline": "php bin/pest --configuration=phpunit.inline.xml", "test:parallel": "php bin/pest --exclude-group=integration --parallel --processes=3", "test:integration": "php bin/pest --group=integration -v", "update:snapshots": "REBUILD_SNAPSHOTS=true php bin/pest --update-snapshots", "test": [ "@test:refacto", "@test:lint", "@test:type:check", "@test:type:coverage", "@test:unit", "@test:parallel", "@test:integration" ] }, "extra": { "pest": { "plugins": [ "Pest\\Mutate\\Plugins\\Mutate", "Pest\\Plugins\\Configuration", "Pest\\Plugins\\Bail", "Pest\\Plugins\\Cache", "Pest\\Plugins\\Coverage", "Pest\\Plugins\\Init", "Pest\\Plugins\\Environment", "Pest\\Plugins\\Help", "Pest\\Plugins\\Memory", "Pest\\Plugins\\Only", "Pest\\Plugins\\Printer", "Pest\\Plugins\\ProcessIsolation", "Pest\\Plugins\\Profile", "Pest\\Plugins\\Retry", "Pest\\Plugins\\Snapshot", "Pest\\Plugins\\Verbose", "Pest\\Plugins\\Version", "Pest\\Plugins\\Shard", "Pest\\Plugins\\Parallel" ] }, "phpstan": { "includes": [ "extension.neon" ] } } } ================================================ FILE: docker/Dockerfile ================================================ ARG PHP=8.1 FROM php:${PHP}-cli-alpine RUN apk update && apk add \ zip libzip-dev icu-dev git RUN docker-php-ext-install zip intl RUN apk add --no-cache linux-headers autoconf build-base RUN pecl install xdebug RUN docker-php-ext-enable xdebug COPY --from=composer:2 /usr/bin/composer /usr/bin/composer WORKDIR /var/www/html ENTRYPOINT ["php"] ================================================ FILE: extension.neon ================================================ parameters: universalObjectCratesClasses: - Pest\Support\HigherOrderTapProxy - Pest\Expectation ================================================ FILE: overrides/Event/Value/ThrowableBuilder.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Code; use NunoMaduro\Collision\Contracts\RenderableOnCollisionEditor; use PHPUnit\Event\NoPreviousThrowableException; use PHPUnit\Framework\Exception; use PHPUnit\Util\Filter; use PHPUnit\Util\ThrowableToStringMapper; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final readonly class ThrowableBuilder { /** * @throws Exception * @throws NoPreviousThrowableException */ public static function from(\Throwable $t): Throwable { $previous = $t->getPrevious(); if ($previous !== null) { $previous = self::from($previous); } $trace = Filter::stackTraceFromThrowableAsString($t); if ($t instanceof RenderableOnCollisionEditor && $frame = $t->toCollisionEditor()) { $file = $frame->getFile(); $line = $frame->getLine(); $trace = "$file:$line\n$trace"; } return new Throwable( $t::class, $t->getMessage(), ThrowableToStringMapper::map($t), $trace, $previous, ); } } ================================================ FILE: overrides/Logging/JUnit/JunitXmlLogger.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ 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; use PHPUnit\Event\Facade; use PHPUnit\Event\InvalidArgumentException; use PHPUnit\Event\Telemetry\HRTime; use PHPUnit\Event\Telemetry\Info; use PHPUnit\Event\Test\Errored; use PHPUnit\Event\Test\Failed; use PHPUnit\Event\Test\Finished; use PHPUnit\Event\Test\MarkedIncomplete; use PHPUnit\Event\Test\PreparationStarted; use PHPUnit\Event\Test\Prepared; use PHPUnit\Event\Test\PrintedUnexpectedOutput; use PHPUnit\Event\Test\Skipped; use PHPUnit\Event\TestSuite\Started; use PHPUnit\Event\UnknownSubscriberTypeException; use PHPUnit\TextUI\Output\Printer; use PHPUnit\Util\Xml; use function assert; use function basename; use function is_int; use function sprintf; use function str_replace; use function trim; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class JunitXmlLogger { private readonly Printer $printer; private readonly Converter $converter; // pest-added private DOMDocument $document; private DOMElement $root; /** * @var DOMElement[] */ private array $testSuites = []; /** * @var array */ private array $testSuiteTests = [0]; /** * @var array */ private array $testSuiteAssertions = [0]; /** * @var array */ private array $testSuiteErrors = [0]; /** * @var array */ private array $testSuiteFailures = [0]; /** * @var array */ private array $testSuiteSkipped = [0]; /** * @var array */ private array $testSuiteTimes = [0]; private int $testSuiteLevel = 0; private ?DOMElement $currentTestCase = null; private ?HRTime $time = null; private bool $prepared = false; private bool $preparationFailed = false; /** * @throws EventFacadeIsSealedException * @throws UnknownSubscriberTypeException */ public function __construct(Printer $printer, Facade $facade) { $this->printer = $printer; $this->converter = new Converter(Container::getInstance()->get(TestSuite::class)->rootPath); // pest-added $this->registerSubscribers($facade); $this->createDocument(); } public function flush(): void { $this->printer->print($this->document->saveXML() ?: ''); $this->printer->flush(); } public function testSuiteStarted(Started $event): void { $testSuite = $this->document->createElement('testsuite'); $testSuite->setAttribute('name', $this->converter->getTestSuiteName($event->testSuite())); // pest-changed if ($event->testSuite()->isForTestClass()) { $testSuite->setAttribute('file', $this->converter->getTestSuiteLocation($event->testSuite()) ?? ''); // pest-changed } if ($this->testSuiteLevel > 0) { $this->testSuites[$this->testSuiteLevel]->appendChild($testSuite); } else { $this->root->appendChild($testSuite); } $this->testSuiteLevel++; $this->testSuites[$this->testSuiteLevel] = $testSuite; $this->testSuiteTests[$this->testSuiteLevel] = 0; $this->testSuiteAssertions[$this->testSuiteLevel] = 0; $this->testSuiteErrors[$this->testSuiteLevel] = 0; $this->testSuiteFailures[$this->testSuiteLevel] = 0; $this->testSuiteSkipped[$this->testSuiteLevel] = 0; $this->testSuiteTimes[$this->testSuiteLevel] = 0; } public function testSuiteFinished(): void { $this->testSuites[$this->testSuiteLevel]->setAttribute( 'tests', (string) $this->testSuiteTests[$this->testSuiteLevel], ); $this->testSuites[$this->testSuiteLevel]->setAttribute( 'assertions', (string) $this->testSuiteAssertions[$this->testSuiteLevel], ); $this->testSuites[$this->testSuiteLevel]->setAttribute( 'errors', (string) $this->testSuiteErrors[$this->testSuiteLevel], ); $this->testSuites[$this->testSuiteLevel]->setAttribute( 'failures', (string) $this->testSuiteFailures[$this->testSuiteLevel], ); $this->testSuites[$this->testSuiteLevel]->setAttribute( 'skipped', (string) $this->testSuiteSkipped[$this->testSuiteLevel], ); $this->testSuites[$this->testSuiteLevel]->setAttribute( 'time', sprintf('%F', $this->testSuiteTimes[$this->testSuiteLevel]), ); if ($this->testSuiteLevel > 1) { $this->testSuiteTests[$this->testSuiteLevel - 1] += $this->testSuiteTests[$this->testSuiteLevel]; $this->testSuiteAssertions[$this->testSuiteLevel - 1] += $this->testSuiteAssertions[$this->testSuiteLevel]; $this->testSuiteErrors[$this->testSuiteLevel - 1] += $this->testSuiteErrors[$this->testSuiteLevel]; $this->testSuiteFailures[$this->testSuiteLevel - 1] += $this->testSuiteFailures[$this->testSuiteLevel]; $this->testSuiteSkipped[$this->testSuiteLevel - 1] += $this->testSuiteSkipped[$this->testSuiteLevel]; $this->testSuiteTimes[$this->testSuiteLevel - 1] += $this->testSuiteTimes[$this->testSuiteLevel]; } $this->testSuiteLevel--; } /** * @throws InvalidArgumentException */ public function testPreparationStarted(PreparationStarted $event): void { $this->createTestCase($event); } public function testPreparationFailed(): void { $this->preparationFailed = true; } public function testPrepared(): void { $this->prepared = true; } public function testPrintedUnexpectedOutput(PrintedUnexpectedOutput $event): void { assert($this->currentTestCase !== null); $systemOut = $this->document->createElement( 'system-out', Xml::prepareString($event->output()), ); $this->currentTestCase->appendChild($systemOut); } /** * @throws InvalidArgumentException */ public function testFinished(Finished $event): void { if (! $this->prepared || $this->preparationFailed) { return; } $this->handleFinish($event->telemetryInfo(), $event->numberOfAssertionsPerformed()); } /** * @throws InvalidArgumentException */ public function testMarkedIncomplete(MarkedIncomplete $event): void { $this->handleIncompleteOrSkipped($event); } /** * @throws InvalidArgumentException */ public function testSkipped(Skipped $event): void { $this->handleIncompleteOrSkipped($event); } /** * @throws InvalidArgumentException */ public function testErrored(Errored $event): void { $this->handleFault($event, 'error'); $this->testSuiteErrors[$this->testSuiteLevel]++; } /** * @throws InvalidArgumentException */ public function testFailed(Failed $event): void { $this->handleFault($event, 'failure'); $this->testSuiteFailures[$this->testSuiteLevel]++; } /** * @throws InvalidArgumentException */ private function handleFinish(Info $telemetryInfo, int $numberOfAssertionsPerformed): void { assert($this->currentTestCase !== null); assert($this->time !== null); $time = $telemetryInfo->time()->duration($this->time)->asFloat(); $this->testSuiteAssertions[$this->testSuiteLevel] += $numberOfAssertionsPerformed; $this->currentTestCase->setAttribute( 'assertions', (string) $numberOfAssertionsPerformed, ); $this->currentTestCase->setAttribute( 'time', sprintf('%F', $time), ); $this->testSuites[$this->testSuiteLevel]->appendChild( $this->currentTestCase, ); $this->testSuiteTests[$this->testSuiteLevel]++; $this->testSuiteTimes[$this->testSuiteLevel] += $time; $this->currentTestCase = null; $this->time = null; $this->prepared = false; } /** * @throws EventFacadeIsSealedException * @throws UnknownSubscriberTypeException */ private function registerSubscribers(Facade $facade): void { $facade->registerSubscribers( new TestSuiteStartedSubscriber($this), new TestSuiteFinishedSubscriber($this), new TestPreparationStartedSubscriber($this), new TestPreparationFailedSubscriber($this), new TestPreparedSubscriber($this), new TestPrintedUnexpectedOutputSubscriber($this), new TestFinishedSubscriber($this), new TestErroredSubscriber($this), new TestFailedSubscriber($this), new TestMarkedIncompleteSubscriber($this), new TestSkippedSubscriber($this), new TestRunnerExecutionFinishedSubscriber($this), ); } private function createDocument(): void { $this->document = new DOMDocument('1.0', 'UTF-8'); $this->document->formatOutput = true; $this->root = $this->document->createElement('testsuites'); $this->document->appendChild($this->root); } /** * @throws InvalidArgumentException */ private function handleFault(Errored|Failed $event, string $type): void { if (! $this->prepared) { $this->createTestCase($event); } assert($this->currentTestCase !== null); $buffer = $this->converter->getTestCaseMethodName($event->test()); // pest-changed $throwable = $event->throwable(); $buffer .= trim( $this->converter->getExceptionMessage($throwable).PHP_EOL. // pest-changed $this->converter->getExceptionDetails($throwable), // pest-changed ); $fault = $this->document->createElement( $type, Xml::prepareString($buffer), ); $fault->setAttribute('type', $throwable->className()); $this->currentTestCase->appendChild($fault); if (! $this->prepared) { $this->handleFinish($event->telemetryInfo(), 0); } } /** * @throws InvalidArgumentException */ private function handleIncompleteOrSkipped(MarkedIncomplete|Skipped $event): void { if (! $this->prepared) { $this->createTestCase($event); } assert($this->currentTestCase !== null); $skipped = $this->document->createElement('skipped'); $this->currentTestCase->appendChild($skipped); $this->testSuiteSkipped[$this->testSuiteLevel]++; if (! $this->prepared) { $this->handleFinish($event->telemetryInfo(), 0); } } /** * @throws InvalidArgumentException */ private function testAsString(Test $test): string { if ($test->isPhpt()) { return basename($test->file()); } assert($test instanceof TestMethod); return sprintf( '%s::%s%s', $test->className(), $this->name($test), PHP_EOL, ); } /** * @throws InvalidArgumentException */ private function name(Test $test): string { if ($test->isPhpt()) { return basename($test->file()); } assert($test instanceof TestMethod); if (! $test->testData()->hasDataFromDataProvider()) { return $test->methodName(); } $dataSetName = $test->testData()->dataFromDataProvider()->dataSetName(); if (is_int($dataSetName)) { return sprintf( '%s with data set #%d', $test->methodName(), $dataSetName, ); } return sprintf( '%s with data set "%s"', $test->methodName(), $dataSetName, ); } /** * @throws InvalidArgumentException * * @phpstan-assert !null $this->currentTestCase */ private function createTestCase(Errored|Failed|MarkedIncomplete|PreparationStarted|Prepared|Skipped $event): void { $testCase = $this->document->createElement('testcase'); $test = $event->test(); $file = $this->converter->getTestCaseLocation($test); // pest-added $testCase->setAttribute('name', $this->converter->getTestCaseMethodName($test)); // pest-changed $testCase->setAttribute('file', $file); // pest-changed if ($test->isTestMethod()) { assert($test instanceof TestMethod); // $testCase->setAttribute('line', (string) $test->line()); // pest-removed $className = $this->converter->getTrimmedTestClassName($test); // pest-added $testCase->setAttribute('class', $className); // pest-changed $testCase->setAttribute('classname', str_replace('\\', '.', $className)); // pest-changed } $this->currentTestCase = $testCase; $this->time = $event->telemetryInfo()->time(); } } ================================================ FILE: overrides/Runner/Filter/NameFilterIterator.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Runner\Filter; use Pest\Contracts\HasPrintableTestCaseName; use PHPUnit\Framework\Test; use PHPUnit\Framework\TestSuite; use PHPUnit\Runner\PhptTestCase; use RecursiveFilterIterator; use RecursiveIterator; use function end; use function preg_match; use function sprintf; use function str_replace; /** * @internal This class is not covered by the backward compatibility promise for PHPUnit */ abstract class NameFilterIterator extends RecursiveFilterIterator { /** * @psalm-var non-empty-string */ private readonly string $regularExpression; private readonly ?int $dataSetMinimum; private readonly ?int $dataSetMaximum; /** * @psalm-param RecursiveIterator $iterator * @psalm-param non-empty-string $filter */ public function __construct(RecursiveIterator $iterator, string $filter) { parent::__construct($iterator); $preparedFilter = $this->prepareFilter($filter); $this->regularExpression = $preparedFilter['regularExpression']; $this->dataSetMinimum = $preparedFilter['dataSetMinimum']; $this->dataSetMaximum = $preparedFilter['dataSetMaximum']; } public function accept(): bool { $test = $this->getInnerIterator()->current(); if ($test instanceof TestSuite) { return true; } if ($test instanceof PhptTestCase) { return false; } if ($test instanceof HasPrintableTestCaseName) { $name = trim( $test::getPrintableTestCaseName().'::'.$test->getPrintableTestCaseMethodName().$test->dataSetAsString() ); } else { $name = $test::class.'::'.$test->nameWithDataSet(); } $accepted = @preg_match($this->regularExpression, $name, $matches) === 1; if ($accepted && isset($this->dataSetMaximum)) { $set = end($matches); $accepted = $set >= $this->dataSetMinimum && $set <= $this->dataSetMaximum; } return $this->doAccept($accepted); } abstract protected function doAccept(bool $result): bool; /** * @psalm-param non-empty-string $filter * * @psalm-return array{regularExpression: non-empty-string, dataSetMinimum: ?int, dataSetMaximum: ?int} */ private function prepareFilter(string $filter): array { $dataSetMinimum = null; $dataSetMaximum = null; if (@preg_match($filter, '') === false) { // Handles: // * testAssertEqualsSucceeds#4 // * testAssertEqualsSucceeds#4-8 if (preg_match('/^(.*?)#(\d+)(?:-(\d+))?$/', $filter, $matches)) { if (isset($matches[3]) && $matches[2] < $matches[3]) { $filter = sprintf( '%s.*with data set #(\d+)$', $matches[1], ); $dataSetMinimum = (int) $matches[2]; $dataSetMaximum = (int) $matches[3]; } else { $filter = sprintf( '%s.*with data set #%s$', $matches[1], $matches[2], ); } } // Handles: // * testDetermineJsonError@JSON_ERROR_NONE // * testDetermineJsonError@JSON.* elseif (preg_match('/^(.*?)@(.+)$/', $filter, $matches)) { $filter = sprintf( '%s.*with data set "%s"$', $matches[1], $matches[2], ); } // Escape delimiters in regular expression. Do NOT use preg_quote, // to keep magic characters. $filter = sprintf( '/%s/i', str_replace( '/', '\\/', $filter, ), ); } return [ 'regularExpression' => $filter, 'dataSetMinimum' => $dataSetMinimum, 'dataSetMaximum' => $dataSetMaximum, ]; } } ================================================ FILE: overrides/Runner/ResultCache/DefaultResultCache.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Runner\ResultCache; use const DIRECTORY_SEPARATOR; use const LOCK_EX; use PHPUnit\Framework\TestStatus\TestStatus; use PHPUnit\Runner\DirectoryDoesNotExistException; use PHPUnit\Runner\Exception; use PHPUnit\Util\Filesystem; use function array_keys; use function assert; use function dirname; use function file_get_contents; use function file_put_contents; use function is_array; use function is_dir; use function is_file; use function json_decode; use function json_encode; use function Pest\version; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class DefaultResultCache implements ResultCache { private const string DEFAULT_RESULT_CACHE_FILENAME = '.phpunit.result.cache'; private readonly string $cacheFilename; /** * @var array */ private array $defects = []; /** * @var array */ private array $times = []; public function __construct(?string $filepath = null) { if ($filepath !== null && is_dir($filepath)) { $filepath .= DIRECTORY_SEPARATOR.self::DEFAULT_RESULT_CACHE_FILENAME; } $this->cacheFilename = $filepath ?? $_ENV['PHPUNIT_RESULT_CACHE'] ?? self::DEFAULT_RESULT_CACHE_FILENAME; } public function setStatus(ResultCacheId $id, TestStatus $status): void { if ($status->isSuccess()) { return; } $this->defects[$id->asString()] = $status; } public function status(ResultCacheId $id): TestStatus { return $this->defects[$id->asString()] ?? TestStatus::unknown(); } public function setTime(ResultCacheId $id, float $time): void { $this->times[$id->asString()] = $time; } public function time(ResultCacheId $id): float { return $this->times[$id->asString()] ?? 0.0; } public function mergeWith(self $other): void { foreach ($other->defects as $id => $defect) { $this->defects[$id] = $defect; } foreach ($other->times as $id => $time) { $this->times[$id] = $time; } } public function load(): void { if (! is_file($this->cacheFilename)) { return; } $contents = file_get_contents($this->cacheFilename); if ($contents === false) { return; } $data = json_decode( $contents, true, ); if ($data === null) { return; } if (! isset($data['version'])) { return; } if ($data['version'] !== $this->cacheVersion()) { return; } assert(isset($data['defects']) && is_array($data['defects'])); assert(isset($data['times']) && is_array($data['times'])); foreach (array_keys($data['defects']) as $test) { $data['defects'][$test] = TestStatus::from($data['defects'][$test]); } $this->defects = $data['defects']; $this->times = $data['times']; } /** * @throws Exception */ public function persist(): void { if (! Filesystem::createDirectory(dirname($this->cacheFilename))) { throw new DirectoryDoesNotExistException(dirname($this->cacheFilename)); } $data = [ 'version' => $this->cacheVersion(), 'defects' => [], 'times' => $this->times, ]; foreach ($this->defects as $test => $status) { $data['defects'][$test] = $status->asInt(); } file_put_contents( $this->cacheFilename, json_encode($data), LOCK_EX, ); } /** * Returns the cache version. */ private function cacheVersion(): string { return 'pest_'.version(); } } ================================================ FILE: overrides/Runner/TestSuiteLoader.php ================================================ */ private static array $loadedClasses = []; /** * @psalm-var array> */ private static array $loadedClassesByFilename = []; /** * @psalm-var list */ private static array $declaredClasses = []; public function __construct() { if (empty(self::$declaredClasses)) { self::$declaredClasses = get_declared_classes(); } } /** * @throws Exception */ public function load(string $suiteClassFile): ReflectionClass { $suiteClassName = $this->classNameFromFileName($suiteClassFile); (static function () use ($suiteClassFile) { try { include_once $suiteClassFile; } catch (Throwable $e) { Panic::with($e); } TestSuite::getInstance()->tests->makeIfNeeded($suiteClassFile); })(); $loadedClasses = array_values( array_diff( get_declared_classes(), array_merge( self::$declaredClasses, self::$loadedClasses ) ) ); self::$loadedClasses = array_merge($loadedClasses, self::$loadedClasses); foreach ($loadedClasses as $loadedClass) { $reflection = new ReflectionClass($loadedClass); $filename = $reflection->getFileName(); self::$loadedClassesByFilename[$filename] = [ $loadedClass, ...self::$loadedClassesByFilename[$filename] ?? [], ]; } $loadedClasses = array_merge(self::$loadedClassesByFilename[$suiteClassFile] ?? [], $loadedClasses); if (empty($loadedClasses)) { return $this->exceptionFor($suiteClassName, $suiteClassFile); } $testCaseFound = false; $class = false; foreach (array_reverse($loadedClasses) as $loadedClass) { if ( is_subclass_of($loadedClass, HasPrintableTestCaseName::class) || is_subclass_of($loadedClass, TestCase::class)) { try { $class = new ReflectionClass($loadedClass); // @codeCoverageIgnoreStart } catch (ReflectionException) { continue; } if ($class->isAbstract() || ($suiteClassFile !== $class->getFileName())) { if (! str_contains($class->getFileName(), 'TestCaseFactory.php')) { continue; } } $suiteClassName = $loadedClass; $testCaseFound = true; break; } } if (! $testCaseFound) { foreach (array_reverse($loadedClasses) as $loadedClass) { $offset = 0 - strlen($suiteClassName); if (stripos(substr($loadedClass, $offset - 1), '\\'.$suiteClassName) === 0 || stripos(substr($loadedClass, $offset - 1), '_'.$suiteClassName) === 0) { try { $class = new ReflectionClass($loadedClass); // @codeCoverageIgnoreStart } catch (ReflectionException) { continue; } $suiteClassName = $loadedClass; $testCaseFound = true; break; } } } if (! $testCaseFound) { return $this->exceptionFor($suiteClassName, $suiteClassFile); } if (! class_exists($suiteClassName, false)) { return $this->exceptionFor($suiteClassName, $suiteClassFile); } // @codeCoverageIgnoreEnd if ($class->isSubclassOf(TestCase::class) && ! $class->isAbstract()) { return $class; } if ($class->hasMethod('suite')) { try { $method = $class->getMethod('suite'); // @codeCoverageIgnoreStart } catch (ReflectionException $e) { throw new Exception($e->getMessage(), (int) $e->getCode(), $e); } // @codeCoverageIgnoreEnd if (! $method->isAbstract() && $method->isPublic() && $method->isStatic()) { return $class; } } return $this->exceptionFor($suiteClassName, $suiteClassFile); } public function reload(ReflectionClass $aClass): ReflectionClass { return $aClass; } private function classNameFromFileName(string $suiteClassFile): string { $className = basename($suiteClassFile, '.php'); $dotPos = strpos($className, '.'); if ($dotPos !== false) { $className = substr($className, 0, $dotPos); } return $className; } private function exceptionFor(string $className, string $filename): ReflectionClass { return new ReflectionClass(IgnorableTestCase::class); } } ================================================ FILE: overrides/TextUI/Command/Commands/WarmCodeCoverageCacheCommand.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Command; use const PHP_EOL; use PHPUnit\TextUI\Configuration\CodeCoverageFilterRegistry; use PHPUnit\TextUI\Configuration\Configuration; use PHPUnit\TextUI\Configuration\NoCoverageCacheDirectoryException; use SebastianBergmann\CodeCoverage\StaticAnalysis\CacheWarmer; use SebastianBergmann\Timer\NoActiveTimerException; use SebastianBergmann\Timer\Timer; /** * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final readonly class WarmCodeCoverageCacheCommand implements Command { private Configuration $configuration; private CodeCoverageFilterRegistry $codeCoverageFilterRegistry; public function __construct(Configuration $configuration, CodeCoverageFilterRegistry $codeCoverageFilterRegistry) { $this->configuration = $configuration; $this->codeCoverageFilterRegistry = $codeCoverageFilterRegistry; } /** * @throws NoActiveTimerException * @throws NoCoverageCacheDirectoryException */ public function execute(): Result { if (! $this->configuration->hasCoverageCacheDirectory()) { return Result::from( 'Cache for static analysis has not been configured'.PHP_EOL, Result::FAILURE, ); } $this->codeCoverageFilterRegistry->init($this->configuration, true); if (! $this->codeCoverageFilterRegistry->configured()) { return Result::from( 'Filter for code coverage has not been configured'.PHP_EOL, Result::FAILURE, ); } $timer = new Timer; $timer->start(); (new CacheWarmer)->warmCache( $this->configuration->coverageCacheDirectory(), ! $this->configuration->disableCodeCoverageIgnore(), $this->configuration->ignoreDeprecatedCodeUnitsFromCodeCoverage(), $this->codeCoverageFilterRegistry->get(), ); return Result::from(); } } ================================================ FILE: overrides/TextUI/Output/Default/ProgressPrinter/Subscriber/TestSkippedSubscriber.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Pest\Logging\TeamCity\Subscriber; use PHPUnit\Event\Test\Skipped; use PHPUnit\Event\Test\SkippedSubscriber; use ReflectionClass; /** * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestSkippedSubscriber extends Subscriber implements SkippedSubscriber { public function notify(Skipped $event): void { if (str_contains($event->message(), '__TODO__')) { $this->printTodoItem(); } $this->logger()->testSkipped($event); } /** * Prints a "T" to the standard PHPUnit output to indicate a todo item. */ private function printTodoItem(): void { $mirror = new ReflectionClass($this->printer()); $printerMirror = $mirror->getMethod('printProgress'); $printerMirror->invoke($this->printer(), 'T'); } } ================================================ FILE: overrides/TextUI/TestSuiteFilterProcessor.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI; use Pest\Plugins\Only; use Pest\Runner\Filter\EnsureTestCaseIsInitiatedFilter; use PHPUnit\Event; use PHPUnit\Framework\TestSuite; use PHPUnit\Runner\Filter\Factory; use PHPUnit\TextUI\Configuration\Configuration; use PHPUnit\TextUI\Configuration\FilterNotConfiguredException; use function array_map; /** * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final readonly class TestSuiteFilterProcessor { /** * @throws Event\RuntimeException * @throws FilterNotConfiguredException */ public function process(Configuration $configuration, TestSuite $suite): void { $factory = new Factory; // @phpstan-ignore-next-line (fn () => $this->filters[] = [ 'className' => EnsureTestCaseIsInitiatedFilter::class, 'argument' => '', ])->call($factory); if (! $configuration->hasFilter() && ! $configuration->hasGroups() && ! $configuration->hasExcludeGroups() && ! $configuration->hasExcludeFilter() && ! $configuration->hasTestsCovering() && ! $configuration->hasTestsUsing() && ! Only::isEnabled()) { $suite->injectFilter($factory); return; } if ($configuration->hasExcludeGroups()) { $factory->addExcludeGroupFilter( $configuration->excludeGroups(), ); } if (Only::isEnabled()) { $factory->addIncludeGroupFilter([Only::group()]); } elseif ($configuration->hasGroups()) { $factory->addIncludeGroupFilter( $configuration->groups(), ); } if ($configuration->hasTestsCovering()) { $factory->addIncludeGroupFilter( array_map( static fn (string $name): string => '__phpunit_covers_'.$name, $configuration->testsCovering(), ), ); } if ($configuration->hasTestsUsing()) { $factory->addIncludeGroupFilter( array_map( static fn (string $name): string => '__phpunit_uses_'.$name, $configuration->testsUsing(), ), ); } if ($configuration->hasExcludeFilter()) { $factory->addExcludeNameFilter( $configuration->excludeFilter(), ); } if ($configuration->hasFilter()) { $factory->addIncludeNameFilter( $configuration->filter(), ); } $suite->injectFilter($factory); Event\Facade::emitter()->testSuiteFiltered( Event\TestSuite\TestSuiteBuilder::from($suite), ); } } ================================================ FILE: phpstan-baseline.neon ================================================ parameters: ignoreErrors: - message: '#^Parameter \#1 of callable callable\(Pest\\Expectation\\)\: Pest\\Arch\\Contracts\\ArchExpectation expects Pest\\Expectation\, Pest\\Expectation\ given\.$#' identifier: argument.type count: 1 path: src/ArchPresets/AbstractPreset.php - message: '#^Trait Pest\\Concerns\\Expectable is used zero times and is not analysed\.$#' identifier: trait.unused count: 1 path: src/Concerns/Expectable.php - message: '#^Trait Pest\\Concerns\\Logging\\WritesToConsole is used zero times and is not analysed\.$#' identifier: trait.unused count: 1 path: src/Concerns/Logging/WritesToConsole.php - message: '#^Trait Pest\\Concerns\\Testable is used zero times and is not analysed\.$#' identifier: trait.unused count: 1 path: src/Concerns/Testable.php - message: '#^Loose comparison using \!\= between \(Closure\|null\) and false will always evaluate to false\.$#' identifier: notEqual.alwaysFalse count: 1 path: src/Expectation.php - message: '#^Method Pest\\Expectation\:\:and\(\) should return Pest\\Expectation\ but returns \(Pest\\Expectation&TAndValue\)\|Pest\\Expectation\\.$#' identifier: return.type count: 1 path: src/Expectation.php - message: '#^PHPDoc tag @property for property Pest\\Expectation\:\:\$each contains generic class Pest\\Expectations\\EachExpectation but does not specify its types\: TValue$#' identifier: missingType.generics count: 1 path: src/Expectation.php - message: '#^PHPDoc tag @property for property Pest\\Expectation\:\:\$not contains generic class Pest\\Expectations\\OppositeExpectation but does not specify its types\: TValue$#' identifier: missingType.generics count: 1 path: src/Expectation.php - message: '#^Parameter \#2 \$newScope of method Closure\:\:bindTo\(\) expects ''static''\|class\-string\|object\|null, string given\.$#' identifier: argument.type count: 1 path: src/Expectation.php - message: '#^Function expect\(\) should return Pest\\Expectation\ but returns Pest\\Expectation\\.$#' identifier: return.type count: 1 path: src/Functions.php - message: '#^Parameter \#1 \$argv of method PHPUnit\\TextUI\\Application\:\:run\(\) expects list\, array\ given\.$#' identifier: argument.type count: 1 path: src/Kernel.php - message: '#^Call to an undefined method object&TValue of mixed\:\:__toString\(\)\.$#' identifier: method.notFound count: 1 path: src/Mixins/Expectation.php - message: '#^Call to an undefined method object&TValue of mixed\:\:toArray\(\)\.$#' identifier: method.notFound count: 4 path: src/Mixins/Expectation.php - message: '#^Call to an undefined method object&TValue of mixed\:\:toSnapshot\(\)\.$#' identifier: method.notFound count: 1 path: src/Mixins/Expectation.php - message: '#^Call to an undefined method object&TValue of mixed\:\:toString\(\)\.$#' identifier: method.notFound count: 1 path: src/Mixins/Expectation.php - message: '#^Call to static method PHPUnit\\Framework\\Assert\:\:assertTrue\(\) with true will always evaluate to true\.$#' identifier: staticMethod.alreadyNarrowedType count: 2 path: src/Mixins/Expectation.php - message: '#^PHPDoc tag @var with type callable\(\)\: bool is not subtype of native type Closure\|null\.$#' identifier: varTag.nativeType count: 1 path: src/PendingCalls/TestCall.php - message: '#^Parameter \#1 \$argv of class Symfony\\Component\\Console\\Input\\ArgvInput constructor expects list\\|null, array\ given\.$#' identifier: argument.type count: 1 path: src/Plugins/Parallel.php - message: '#^Parameter \#13 \$testRunnerTriggeredDeprecationEvents of class PHPUnit\\TestRunner\\TestResult\\TestResult constructor expects list\, array given\.$#' identifier: argument.type count: 1 path: src/Plugins/Parallel/Paratest/WrapperRunner.php - message: '#^Parameter \#14 \$testRunnerTriggeredWarningEvents of class PHPUnit\\TestRunner\\TestResult\\TestResult constructor expects list\, array given\.$#' identifier: argument.type count: 1 path: src/Plugins/Parallel/Paratest/WrapperRunner.php - message: '#^Parameter \#15 \$errors of class PHPUnit\\TestRunner\\TestResult\\TestResult constructor expects list\, array given\.$#' identifier: argument.type count: 1 path: src/Plugins/Parallel/Paratest/WrapperRunner.php - message: '#^Parameter \#16 \$deprecations of class PHPUnit\\TestRunner\\TestResult\\TestResult constructor expects list\, array given\.$#' identifier: argument.type count: 1 path: src/Plugins/Parallel/Paratest/WrapperRunner.php - message: '#^Parameter \#17 \$notices of class PHPUnit\\TestRunner\\TestResult\\TestResult constructor expects list\, array given\.$#' identifier: argument.type count: 1 path: src/Plugins/Parallel/Paratest/WrapperRunner.php - message: '#^Parameter \#18 \$warnings of class PHPUnit\\TestRunner\\TestResult\\TestResult constructor expects list\, array given\.$#' identifier: argument.type count: 1 path: src/Plugins/Parallel/Paratest/WrapperRunner.php - message: '#^Parameter \#19 \$phpDeprecations of class PHPUnit\\TestRunner\\TestResult\\TestResult constructor expects list\, array given\.$#' identifier: argument.type count: 1 path: src/Plugins/Parallel/Paratest/WrapperRunner.php - message: '#^Parameter \#20 \$phpNotices of class PHPUnit\\TestRunner\\TestResult\\TestResult constructor expects list\, array given\.$#' identifier: argument.type count: 1 path: src/Plugins/Parallel/Paratest/WrapperRunner.php - message: '#^Parameter \#21 \$phpWarnings of class PHPUnit\\TestRunner\\TestResult\\TestResult constructor expects list\, array given\.$#' identifier: argument.type count: 1 path: src/Plugins/Parallel/Paratest/WrapperRunner.php - message: '#^Parameter \#4 \$testErroredEvents of class PHPUnit\\TestRunner\\TestResult\\TestResult constructor expects list\, array given\.$#' identifier: argument.type count: 1 path: src/Plugins/Parallel/Paratest/WrapperRunner.php - message: '#^Parameter \#5 \$testFailedEvents of class PHPUnit\\TestRunner\\TestResult\\TestResult constructor expects list\, array given\.$#' identifier: argument.type count: 1 path: src/Plugins/Parallel/Paratest/WrapperRunner.php - message: '#^Parameter \#7 \$testSuiteSkippedEvents of class PHPUnit\\TestRunner\\TestResult\\TestResult constructor expects list\, array given\.$#' identifier: argument.type count: 1 path: src/Plugins/Parallel/Paratest/WrapperRunner.php - message: '#^Parameter \#8 \$testSkippedEvents of class PHPUnit\\TestRunner\\TestResult\\TestResult constructor expects list\, array given\.$#' identifier: argument.type count: 1 path: src/Plugins/Parallel/Paratest/WrapperRunner.php - message: '#^Parameter \#9 \$testMarkedIncompleteEvents of class PHPUnit\\TestRunner\\TestResult\\TestResult constructor expects list\, array given\.$#' identifier: argument.type count: 1 path: src/Plugins/Parallel/Paratest/WrapperRunner.php - message: '#^Property Pest\\Plugins\\Parallel\\Paratest\\WrapperRunner\:\:\$pending \(list\\) does not accept array\\.$#' identifier: assign.propertyType count: 1 path: src/Plugins/Parallel/Paratest/WrapperRunner.php ================================================ FILE: phpstan.neon ================================================ includes: - phpstan-baseline.neon parameters: level: 7 paths: - src reportUnmatchedIgnoredErrors: false ignoreErrors: - "#type mixed is not subtype of native#" ================================================ FILE: phpunit.xml ================================================ ./tests ./tests-external ./tests/.snapshots ./tests/.tests ./tests/Fixtures/Inheritance ./src ================================================ FILE: rector.php ================================================ withPaths([ __DIR__.'/src', ]) ->withSkip([ __DIR__.'/src/Plugins/Parallel/Paratest/WrapperRunner.php', ReturnNeverTypeRector::class, ArrowFunctionDelegatingCallToFirstClassCallableRector::class, NarrowObjectReturnTypeRector::class, RemoveParentDelegatingConstructorRector::class, ]) ->withPreparedSets( deadCode: true, codeQuality: true, typeDeclarations: true, privatization: true, earlyReturn: true, ) ->withPhpSets(); ================================================ FILE: resources/base-phpunit.xml ================================================ tests/ app src ================================================ FILE: resources/views/components/badge.php ================================================ ['blue', 'INFO'], 'ERROR' => ['red', 'ERROR'], }; ?>
================================================ FILE: resources/views/components/new-line.php ================================================
================================================ FILE: resources/views/components/two-column-detail.php ================================================
================================================ FILE: resources/views/installers/plugin-browser.php ================================================

Using the visit() function requires the Pest Plugin Browser to be installed. Run:

- composer require pestphp/pest-plugin-browser:^4.0 --dev
- npm install playwright@latest
- npx playwright install
================================================ FILE: resources/views/usage.php ================================================
USAGE:pest') ?> [options]
================================================ FILE: resources/views/version.php ================================================
Pest Testing Framework.
================================================ FILE: src/ArchPresets/AbstractPreset.php ================================================ |ArchExpectation> */ protected array $expectations = []; /** * Creates a new preset instance. * * @param array $userNamespaces */ public function __construct( private readonly array $userNamespaces, ) { // } /** * Executes the arch preset. * * @internal */ abstract public function execute(): void; /** * Ignores the given "targets" or "dependencies". * * @param array|string $targetsOrDependencies */ final public function ignoring(array|string $targetsOrDependencies): void { $this->expectations = array_map( fn (ArchExpectation|Expectation $expectation): Expectation|ArchExpectation => $expectation instanceof ArchExpectation ? $expectation->ignoring($targetsOrDependencies) : $expectation, $this->expectations, ); } /** * Runs the given callback for each namespace. * * @param callable(Expectation): ArchExpectation ...$callbacks */ final public function eachUserNamespace(callable ...$callbacks): void { foreach ($this->userNamespaces as $namespace) { foreach ($callbacks as $callback) { $this->expectations[] = $callback(expect($namespace)); } } } /** * Flushes the expectations. */ final public function flush(): void { $this->expectations = []; } } ================================================ FILE: src/ArchPresets/Custom.php ================================================ $userNamespaces * @param Closure(array): array|ArchExpectation> $execute */ public function __construct( private readonly array $userNamespaces, private readonly string $name, private readonly Closure $execute, ) { parent::__construct($userNamespaces); } /** * Returns the name of the preset. */ public function name(): string { return $this->name; } /** * Executes the arch preset. */ public function execute(): void { $this->expectations = ($this->execute)($this->userNamespaces); } } ================================================ FILE: src/ArchPresets/Laravel.php ================================================ expectations[] = expect('App\Traits') ->toBeTraits(); $this->expectations[] = expect('App\Concerns') ->toBeTraits(); $this->expectations[] = expect('App') ->not->toBeEnums() ->ignoring('App\Enums'); $this->expectations[] = expect('App\Enums') ->toBeEnums() ->ignoring('App\Enums\Concerns'); $this->expectations[] = expect('App\Features') ->toBeClasses() ->ignoring('App\Features\Concerns'); $this->expectations[] = expect('App\Features') ->toHaveMethod('resolve') ->ignoring('App\Features\Concerns'); $this->expectations[] = expect('App\Exceptions') ->classes() ->toImplement('Throwable') ->ignoring('App\Exceptions\Handler'); $this->expectations[] = expect('App') ->not->toImplement(Throwable::class) ->ignoring('App\Exceptions'); $this->expectations[] = expect('App\Http\Middleware') ->classes() ->toHaveMethod('handle'); $this->expectations[] = expect('App\Models') ->classes() ->toExtend('Illuminate\Database\Eloquent\Model') ->ignoring('App\Models\Scopes'); $this->expectations[] = expect('App\Models') ->classes() ->not->toHaveSuffix('Model'); $this->expectations[] = expect('App') ->not->toExtend('Illuminate\Database\Eloquent\Model') ->ignoring('App\Models'); $this->expectations[] = expect('App\Http\Requests') ->classes() ->toHaveSuffix('Request'); $this->expectations[] = expect('App\Http\Requests') ->toExtend('Illuminate\Foundation\Http\FormRequest'); $this->expectations[] = expect('App\Http\Requests') ->toHaveMethod('rules'); $this->expectations[] = expect('App') ->not->toExtend('Illuminate\Foundation\Http\FormRequest') ->ignoring('App\Http\Requests'); $this->expectations[] = expect('App\Console\Commands') ->classes() ->toHaveSuffix('Command'); $this->expectations[] = expect('App\Console\Commands') ->classes() ->toExtend('Illuminate\Console\Command'); $this->expectations[] = expect('App\Console\Commands') ->classes() ->toHaveMethod('handle'); $this->expectations[] = expect('App') ->not->toExtend('Illuminate\Console\Command') ->ignoring('App\Console\Commands'); $this->expectations[] = expect('App\Mail') ->classes() ->toExtend('Illuminate\Mail\Mailable'); $this->expectations[] = expect('App\Mail') ->classes() ->toImplement('Illuminate\Contracts\Queue\ShouldQueue'); $this->expectations[] = expect('App') ->not->toExtend('Illuminate\Mail\Mailable') ->ignoring('App\Mail'); $this->expectations[] = expect('App\Jobs') ->classes() ->toImplement('Illuminate\Contracts\Queue\ShouldQueue'); $this->expectations[] = expect('App\Jobs') ->classes() ->toHaveMethod('handle'); $this->expectations[] = expect('App\Listeners') ->toHaveMethod('handle'); $this->expectations[] = expect('App\Notifications') ->toExtend('Illuminate\Notifications\Notification'); $this->expectations[] = expect('App') ->not->toExtend('Illuminate\Notifications\Notification') ->ignoring('App\Notifications'); $this->expectations[] = expect('App\Providers') ->toHaveSuffix('ServiceProvider'); $this->expectations[] = expect('App\Providers') ->toExtend('Illuminate\Support\ServiceProvider'); $this->expectations[] = expect('App\Providers') ->not->toBeUsed(); $this->expectations[] = expect('App') ->not->toExtend('Illuminate\Support\ServiceProvider') ->ignoring('App\Providers'); $this->expectations[] = expect('App') ->not->toHaveSuffix('ServiceProvider') ->ignoring('App\Providers'); $this->expectations[] = expect('App') ->not->toHaveSuffix('Controller') ->ignoring('App\Http\Controllers'); $this->expectations[] = expect('App\Http\Controllers') ->classes() ->toHaveSuffix('Controller'); $this->expectations[] = expect('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']); $this->expectations[] = expect([ 'dd', 'ddd', 'dump', 'env', 'exit', 'ray', ])->not->toBeUsed(); $this->expectations[] = expect('App\Policies') ->classes() ->toHaveSuffix('Policy'); $this->expectations[] = expect('App\Attributes') ->classes() ->toImplement('Illuminate\Contracts\Container\ContextualAttribute') ->toHaveAttribute('Attribute') ->toHaveMethod('resolve'); } } ================================================ FILE: src/ArchPresets/Php.php ================================================ expectations[] = expect([ 'debug_zval_dump', 'debug_backtrace', 'debug_print_backtrace', 'dump', 'ray', 'ds', 'die', 'goto', 'global', 'var_dump', 'phpinfo', 'echo', 'ereg', 'eregi', 'mysql_connect', 'mysql_pconnect', 'mysql_query', 'mysql_select_db', 'mysql_fetch_array', 'mysql_fetch_assoc', 'mysql_fetch_object', 'mysql_fetch_row', 'mysql_num_rows', 'mysql_affected_rows', 'mysql_free_result', 'mysql_insert_id', 'mysql_error', 'mysql_real_escape_string', 'print', 'print_r', 'var_export', 'xdebug_break', 'xdebug_call_class', 'xdebug_call_file', 'xdebug_call_int', 'xdebug_call_line', 'xdebug_code_coverage_started', 'xdebug_connect_to_client', 'xdebug_debug_zval', 'xdebug_debug_zval_stdout', 'xdebug_dump_superglobals', 'xdebug_get_code_coverage', 'xdebug_get_collected_errors', 'xdebug_get_function_count', 'xdebug_get_function_stack', 'xdebug_get_gc_run_count', 'xdebug_get_gc_total_collected_roots', 'xdebug_get_gcstats_filename', 'xdebug_get_headers', 'xdebug_get_monitored_functions', 'xdebug_get_profiler_filename', 'xdebug_get_stack_depth', 'xdebug_get_tracefile_name', 'xdebug_info', 'xdebug_is_debugger_active', 'xdebug_memory_usage', 'xdebug_notify', 'xdebug_peak_memory_usage', 'xdebug_print_function_stack', 'xdebug_set_filter', 'xdebug_start_code_coverage', 'xdebug_start_error_collection', 'xdebug_start_function_monitor', 'xdebug_start_gcstats', 'xdebug_start_trace', 'xdebug_stop_code_coverage', 'xdebug_stop_error_collection', 'xdebug_stop_function_monitor', 'xdebug_stop_gcstats', 'xdebug_stop_trace', 'xdebug_time_index', 'xdebug_var_dump', 'trap', ])->not->toBeUsed(); } } ================================================ FILE: src/ArchPresets/Relaxed.php ================================================ eachUserNamespace( fn (Expectation $namespace): ArchExpectation => $namespace->not->toUseStrictTypes(), fn (Expectation $namespace): ArchExpectation => $namespace->classes()->not->toBeFinal(), fn (Expectation $namespace): ArchExpectation => $namespace->classes()->not->toHavePrivateMethods(), ); } } ================================================ FILE: src/ArchPresets/Security.php ================================================ expectations[] = expect([ 'md5', 'sha1', 'uniqid', 'rand', 'mt_rand', 'tempnam', 'str_shuffle', 'shuffle', 'array_rand', 'eval', 'exec', 'shell_exec', 'system', 'passthru', 'create_function', 'unserialize', 'extract', 'mb_parse_str', 'dl', 'assert', ])->not->toBeUsed(); } } ================================================ FILE: src/ArchPresets/Strict.php ================================================ eachUserNamespace( fn (Expectation $namespace): ArchExpectation => $namespace->classes()->not->toHaveProtectedMethods(), fn (Expectation $namespace): ArchExpectation => $namespace->classes()->not->toBeAbstract(), fn (Expectation $namespace): ArchExpectation => $namespace->toUseStrictTypes(), fn (Expectation $namespace): ArchExpectation => $namespace->toUseStrictEquality(), fn (Expectation $namespace): ArchExpectation => $namespace->classes()->toBeFinal(), ); $this->expectations[] = expect([ 'sleep', 'usleep', ])->not->toBeUsed(); } } ================================================ FILE: src/Bootstrappers/BootExcludeList.php ================================================ */ private const array EXCLUDE_LIST = [ 'bin', 'overrides', 'resources', 'src', 'stubs', ]; /** * Boots the "exclude list" for PHPUnit to ignore Pest files. */ public function boot(): void { $baseDirectory = dirname(__DIR__, 2); foreach (self::EXCLUDE_LIST as $directory) { ExcludeList::addDirectory($baseDirectory.DIRECTORY_SEPARATOR.$directory); } } } ================================================ FILE: src/Bootstrappers/BootFiles.php ================================================ */ private const array STRUCTURE = [ 'Expectations', 'Expectations.php', 'Helpers', 'Helpers.php', 'Pest.php', ]; /** * Boots the structure of the tests directory. */ public function boot(): void { $rootPath = TestSuite::getInstance()->rootPath; $testsPath = $rootPath.DIRECTORY_SEPARATOR.testDirectory(); if (! is_dir($testsPath)) { throw new FatalException(sprintf('The test directory [%s] does not exist.', $testsPath)); } foreach (self::STRUCTURE as $filename) { $filename = sprintf('%s%s%s', $testsPath, DIRECTORY_SEPARATOR, $filename); if (! file_exists($filename)) { continue; } if (is_dir($filename)) { $directory = new RecursiveDirectoryIterator($filename); $iterator = new RecursiveIteratorIterator($directory); /** @var \DirectoryIterator $file */ foreach ($iterator as $file) { $this->load($file->__toString()); } } else { $this->load($filename); } } $this->bootDatasets($testsPath); } /** * Loads, if possible, the given file. */ private function load(string $filename): void { if (! Str::endsWith($filename, '.php')) { return; } if (! file_exists($filename)) { return; } include_once $filename; } private function bootDatasets(string $testsPath): void { assert($testsPath !== ''); $files = (new PhpUnitFileIterator)->getFilesAsArray($testsPath, '.php'); foreach ($files as $file) { if (DatasetInfo::isADatasetsFile($file) || DatasetInfo::isInsideADatasetsDirectory($file)) { $this->load($file); } } } } ================================================ FILE: src/Bootstrappers/BootKernelDump.php ================================================ add(KernelDump::class, $kernelDump = new KernelDump( $this->output, )); $kernelDump->enable(); } } ================================================ FILE: src/Bootstrappers/BootOverrides.php ================================================ */ public const array FILES = [ 'Runner/Filter/NameFilterIterator.php', 'Runner/ResultCache/DefaultResultCache.php', 'Runner/TestSuiteLoader.php', 'TextUI/Command/Commands/WarmCodeCoverageCacheCommand.php', 'TextUI/Output/Default/ProgressPrinter/Subscriber/TestSkippedSubscriber.php', 'TextUI/TestSuiteFilterProcessor.php', 'Event/Value/ThrowableBuilder.php', 'Logging/JUnit/JunitXmlLogger.php', ]; /** * Boots the list of files to be overridden. */ public function boot(): void { foreach (self::FILES as $file) { $file = __DIR__."/../../overrides/$file"; if (! file_exists($file)) { throw ShouldNotHappen::fromMessage(sprintf('File [%s] does not exist.', $file)); } require_once $file; } } } ================================================ FILE: src/Bootstrappers/BootSubscribers.php ================================================ > */ private const array SUBSCRIBERS = [ Subscribers\EnsureConfigurationIsAvailable::class, Subscribers\EnsureIgnorableTestCasesAreIgnored::class, Subscribers\EnsureKernelDumpIsFlushed::class, Subscribers\EnsureTeamCityEnabled::class, ]; /** * Creates a new instance of the Boot Subscribers. */ public function __construct( private Container $container, ) {} /** * Boots the list of Subscribers. */ public function boot(): void { foreach (self::SUBSCRIBERS as $subscriber) { $instance = $this->container->get($subscriber); assert($instance instanceof Subscriber); Event\Facade::instance()->registerSubscriber($instance); } } } ================================================ FILE: src/Bootstrappers/BootView.php ================================================ output); } } ================================================ FILE: src/Collision/Events.php ================================================ context) === []) { return $description; } renderUsing(self::$output); [ 'assignees' => $assignees, 'issues' => $issues, 'prs' => $prs, ] = $context; if (($link = Project::getInstance()->issues) !== '') { $issuesDescription = array_map(fn (int $issue): string => sprintf('#%s', sprintf($link, $issue), $issue), $issues); } if (($link = Project::getInstance()->prs) !== '') { $prsDescription = array_map(fn (int $pr): string => sprintf('#%s', sprintf($link, $pr), $pr), $prs); } if (($link = Project::getInstance()->assignees) !== '' && count($assignees) > 0) { $assigneesDescription = array_map(fn (string $assignee): string => sprintf( '@%s', sprintf($link, $assignee), $assignee, ), $assignees); } if (count($assignees) > 0 || count($issues) > 0 || count($prs) > 0) { $description .= ' '.implode(', ', array_merge( $issuesDescription ?? [], $prsDescription ?? [], isset($assigneesDescription) ? ['['.implode(', ', $assigneesDescription).']'] : [], )); } return $description; } /** * Fires after the test method description is printed. */ public static function afterTestMethodDescription(TestResult $result): void { if (($context = $result->context) === []) { return; } renderUsing(self::$output); [ 'notes' => $notes, ] = $context; foreach ($notes as $note) { render(sprintf(<<<'HTML'
// %s
HTML, $note, )); } } } ================================================ FILE: src/Concerns/Expectable.php ================================================ */ public function expect(mixed $value): Expectation { return new Expectation($value); } } ================================================ FILE: src/Concerns/Extendable.php ================================================ */ private static array $extends = []; /** * Register a new extend. * * @param-closure-this T $extend */ public function extend(string $name, Closure $extend): void { static::$extends[$name] = $extend; } /** * Checks if given extend name is registered. */ public static function hasExtend(string $name): bool { return array_key_exists($name, static::$extends); } } ================================================ FILE: src/Concerns/Logging/WritesToConsole.php ================================================ writePestTestOutput($message, 'fg-green, bold', '✓'); } /** * Writes the given error message to the console. */ private function writeError(string $message): void { $this->writePestTestOutput($message, 'fg-red, bold', '⨯'); } /** * Writes the given warning message to the console. */ private function writeWarning(string $message): void { $this->writePestTestOutput($message, 'fg-yellow, bold', '-'); } /** * Writes the give message to the console. */ private function writePestTestOutput(string $message, string $color, string $symbol): void { $this->writeWithColor($color, "$symbol ", false); $this->write($message); $this->writeNewLine(); } } ================================================ FILE: src/Concerns/Pipeable.php ================================================ > */ private static array $pipes = []; /** * The list of interceptors. * * @var array> */ private static array $interceptors = []; /** * Register a pipe to be applied before an expectation is checked. */ public function pipe(string $name, Closure $pipe): void { self::$pipes[$name][] = $pipe; } /** * Register an interceptor that should replace an existing expectation. * * @param string|Closure(mixed $value, mixed ...$arguments):bool $filter */ public function intercept(string $name, string|Closure $filter, Closure $handler): void { if (is_string($filter)) { $filter = fn ($value): bool => $value instanceof $filter; } self::$interceptors[$name][] = $handler; $this->pipe($name, function ($next, ...$arguments) use ($handler, $filter): void { /* @phpstan-ignore-next-line */ if ($filter($this->value, ...$arguments)) { // @phpstan-ignore-next-line $handler->bindTo($this, $this::class)(...$arguments); return; } $next(); }); } /** * Get the list of pipes by the given name. * * @return array */ private function pipes(string $name, object $context, string $scope): array { return array_map(fn (Closure $pipe): Closure => $pipe->bindTo($context, $scope), self::$pipes[$name] ?? []); } } ================================================ FILE: src/Concerns/Retrievable.php ================================================ |object $value * @param TRetrievableValue|null $default * @return TRetrievableValue|null */ private function retrieve(string $key, mixed $value, mixed $default = null): mixed { if (is_array($value)) { return $value[$key] ?? $default; } // @phpstan-ignore-next-line return $value->$key ?? $default; } } ================================================ FILE: src/Concerns/Testable.php ================================================ */ private static array $__latestIssues = []; /** * The test's PRs. * * @var array */ private static array $__latestPrs = []; /** * The test's describing, if any. * * @var array */ public array $__describing = []; /** * Whether the test has ran or not. */ public bool $__ran = false; /** * The test's test closure. */ private Closure $__test; /** * The test's before each closure. */ private ?Closure $__beforeEach = null; /** * The test's after each closure. */ private ?Closure $__afterEach = null; /** * The test's before all closure. */ private static ?Closure $__beforeAll = null; /** * The test's after all closure. */ private static ?Closure $__afterAll = null; /** * The list of snapshot changes, if any. */ private array $__snapshotChanges = []; /** * Resets the test case static properties. */ public static function flush(): void { self::$__beforeAll = null; self::$__afterAll = null; } /** * Adds a new "note" to the Test Case. */ public function note(array|string $note): self { $note = is_array($note) ? $note : [$note]; self::$__latestNotes = array_merge(self::$__latestNotes, $note); return $this; } /** * Adds a new "setUpBeforeClass" to the Test Case. */ public function __addBeforeAll(?Closure $hook): void { if (! $hook instanceof Closure) { return; } self::$__beforeAll = (self::$__beforeAll instanceof Closure) ? ChainableClosure::boundStatically(self::$__beforeAll, $hook) : $hook; } /** * Adds a new "tearDownAfterClass" to the Test Case. */ public function __addAfterAll(?Closure $hook): void { if (! $hook instanceof Closure) { return; } self::$__afterAll = (self::$__afterAll instanceof Closure) ? ChainableClosure::boundStatically(self::$__afterAll, $hook) : $hook; } /** * Adds a new "setUp" to the Test Case. */ public function __addBeforeEach(?Closure $hook): void { $this->__addHook('__beforeEach', $hook); } /** * Adds a new "tearDown" to the Test Case. */ public function __addAfterEach(?Closure $hook): void { $this->__addHook('__afterEach', $hook); } /** * Adds a new "hook" to the Test Case. */ private function __addHook(string $property, ?Closure $hook): void { if (! $hook instanceof Closure) { return; } $this->{$property} = ($this->{$property} instanceof Closure) ? ChainableClosure::bound($this->{$property}, $hook) : $hook; } /** * This method is called before the first test of this Test Case is run. */ public static function setUpBeforeClass(): void { parent::setUpBeforeClass(); $beforeAll = TestSuite::getInstance()->beforeAll->get(self::$__filename); if (self::$__beforeAll instanceof Closure) { $beforeAll = ChainableClosure::boundStatically(self::$__beforeAll, $beforeAll); } try { call_user_func(Closure::bind($beforeAll, null, self::class)); } catch (Throwable $e) { Panic::with($e); } } /** * This method is called after the last test of this Test Case is run. */ public static function tearDownAfterClass(): void { $afterAll = TestSuite::getInstance()->afterAll->get(self::$__filename); if (self::$__afterAll instanceof Closure) { $afterAll = ChainableClosure::boundStatically(self::$__afterAll, $afterAll); } call_user_func(Closure::bind($afterAll, null, self::class)); parent::tearDownAfterClass(); } /** * Gets executed before the Test Case. */ protected function setUp(...$arguments): void { TestSuite::getInstance()->test = $this; $method = TestSuite::getInstance()->tests->get(self::$__filename)->getMethod($this->name()); $description = $method->description; if ($this->dataName()) { $description = str_contains((string) $description, ':dataset') ? str_replace(':dataset', str_replace('dataset ', '', $this->dataName()), (string) $description) : $description.' with '.$this->dataName(); } $description = htmlspecialchars(html_entity_decode((string) $description), ENT_NOQUOTES); if ($method->repetitions > 1) { $matches = []; preg_match('/\((.*?)\)/', $description, $matches); if (count($matches) > 1) { if (str_contains($description, 'with '.$matches[0].' /')) { $description = str_replace('with '.$matches[0].' /', '', $description); } else { $description = str_replace('with '.$matches[0], '', $description); } } $description .= ' @ repetition '.($matches[1].' of '.$method->repetitions); } $this->__description = self::$__latestDescription = $description; self::$__latestAssignees = $method->assignees; self::$__latestNotes = $method->notes; self::$__latestIssues = $method->issues; self::$__latestPrs = $method->prs; parent::setUp(); $beforeEach = TestSuite::getInstance()->beforeEach->get(self::$__filename)[1]; if ($this->__beforeEach instanceof Closure) { $beforeEach = ChainableClosure::bound($this->__beforeEach, $beforeEach); } $this->__callClosure($beforeEach, $arguments); } /** * Initialize test case properties from TestSuite. */ public function __initializeTestCase(): void { // Return if the test case has already been initialized if (isset($this->__test)) { return; } $name = $this->name(); $test = TestSuite::getInstance()->tests->get(self::$__filename); if ($test->hasMethod($name)) { $method = $test->getMethod($name); $this->__description = self::$__latestDescription = $method->description; self::$__latestAssignees = $method->assignees; self::$__latestNotes = $method->notes; self::$__latestIssues = $method->issues; self::$__latestPrs = $method->prs; $this->__describing = $method->describing; $this->__test = $method->getClosure(); $method->setUp($this); } } /** * Gets executed after the Test Case. */ protected function tearDown(...$arguments): void { $afterEach = TestSuite::getInstance()->afterEach->get(self::$__filename); if ($this->__afterEach instanceof Closure) { $afterEach = ChainableClosure::bound($this->__afterEach, $afterEach); } try { $this->__callClosure($afterEach, func_get_args()); } finally { parent::tearDown(); TestSuite::getInstance()->test = null; $method = TestSuite::getInstance()->tests->get(self::$__filename)->getMethod($this->name()); $method->tearDown($this); } } /** * Executes the Test Case current test. * * @throws Throwable */ private function __runTest(Closure $closure, ...$args): mixed { $arguments = $this->__resolveTestArguments($args); $this->__ensureDatasetArgumentNameAndNumberMatches($arguments); return $this->__callClosure($closure, $arguments); } /** * Resolve the passed arguments. Any Closures will be bound to the testcase and resolved. * * @throws Throwable */ private function __resolveTestArguments(array $arguments): array { $method = TestSuite::getInstance()->tests->get(self::$__filename)->getMethod($this->name()); 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); $arguments[] = $firstArgument; } $underlyingTest = Reflection::getFunctionVariable($this->__test, 'closure'); $testParameterTypes = array_values(Reflection::getFunctionArguments($underlyingTest)); if (count($arguments) !== 1) { foreach ($arguments as $argumentIndex => $argumentValue) { if (! $argumentValue instanceof Closure) { continue; } if (in_array($testParameterTypes[$argumentIndex], [Closure::class, 'callable', 'mixed'])) { continue; } $arguments[$argumentIndex] = $this->__callClosure($argumentValue, []); } return $arguments; } if (! isset($arguments[0]) || ! $arguments[0] instanceof Closure) { return $arguments; } if (isset($testParameterTypes[0]) && in_array($testParameterTypes[0], [Closure::class, 'callable'])) { return $arguments; } $boundDatasetResult = $this->__callClosure($arguments[0], []); if (count($testParameterTypes) === 1) { return [$boundDatasetResult]; } if (! is_array($boundDatasetResult)) { return [$boundDatasetResult]; } return array_values($boundDatasetResult); } /** * Ensures dataset items count matches underlying test case required parameters * * @throws ReflectionException * @throws DatasetArgumentsMismatch */ private function __ensureDatasetArgumentNameAndNumberMatches(array $arguments): void { if ($arguments === []) { return; } $underlyingTest = Reflection::getFunctionVariable($this->__test, 'closure'); $testReflection = new ReflectionFunction($underlyingTest); $requiredParametersCount = $testReflection->getNumberOfRequiredParameters(); $suppliedParametersCount = count($arguments); $datasetParameterNames = array_keys($arguments); $testParameterNames = array_map( fn (ReflectionParameter $reflectionParameter): string => $reflectionParameter->getName(), array_filter($testReflection->getParameters(), fn (ReflectionParameter $reflectionParameter): bool => ! $reflectionParameter->isOptional()), ); if (array_diff($testParameterNames, $datasetParameterNames) === []) { return; } if (isset($testParameterNames[0]) && $suppliedParametersCount >= $requiredParametersCount) { return; } throw new DatasetArgumentsMismatch($requiredParametersCount, $suppliedParametersCount); } /** * @throws Throwable */ private function __callClosure(Closure $closure, array $arguments): mixed { 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 { return new Preset; } #[PostCondition] protected function __MarkTestIncompleteIfSnapshotHaveChanged(): void { if (count($this->__snapshotChanges) === 0) { return; } $this->markTestIncomplete(implode('. ', $this->__snapshotChanges)); } /** * The printable test case name. */ public static function getPrintableTestCaseName(): string { return preg_replace('/P\\\/', '', self::class, 1); } /** * The printable test case method name. */ public function getPrintableTestCaseMethodName(): string { return $this->__description; } /** * The latest printable test case method name. */ public static function getLatestPrintableTestCaseMethodName(): string { return self::$__latestDescription ?? ''; } /** * The printable test case method context. */ public static function getPrintableContext(): array { return [ 'assignees' => self::$__latestAssignees, 'issues' => self::$__latestIssues, 'prs' => self::$__latestPrs, 'notes' => self::$__latestNotes, ]; } /** * Opens a shell for the test case. */ public function shell(): void { Shell::open(); } } ================================================ FILE: src/Configuration/Presets.php ================================================ issues = "https://github.com/{$project}/issues/%s"; $this->prs = "https://github.com/{$project}/pull/%s"; $this->assignees = 'https://github.com/%s'; return $this; } /** * Sets the test project to GitLab. */ public function gitlab(string $project): self { $this->issues = "https://gitlab.com/{$project}/issues/%s"; $this->prs = "https://gitlab.com/{$project}/merge_requests/%s"; $this->assignees = 'https://gitlab.com/%s'; return $this; } /** * Sets the test project to Bitbucket. */ public function bitbucket(string $project): self { $this->issues = "https://bitbucket.org/{$project}/issues/%s"; $this->prs = "https://bitbucket.org/{$project}/pull-requests/%s"; $this->assignees = 'https://bitbucket.org/%s'; return $this; } /** * Sets the test project to Jira. */ public function jira(string $namespace, string $project): self { $this->issues = "https://{$namespace}.atlassian.net/browse/{$project}-%s"; $this->assignees = "https://{$namespace}.atlassian.net/secure/ViewProfile.jspa?name=%s"; return $this; } /** * Sets the test project to custom. */ public function custom(string $issues, string $prs, string $assignees): self { $this->issues = $issues; $this->prs = $prs; $this->assignees = $assignees; return $this; } } ================================================ FILE: src/Configuration.php ================================================ 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 { 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 { return new UsesCall( $this->filename, array_values($classAndTraits) ); } /** * Depending on where is called, it will extend the given classes and traits globally or locally. */ public function extends(string ...$classAndTraits): UsesCall { return $this->extend(...$classAndTraits); } /** * Depending on where is called, it will add the given groups globally or locally. */ public function group(string ...$groups): UsesCall { return (new UsesCall($this->filename, []))->group(...$groups); } /** * Marks all tests in the current file to be run exclusively. */ public function only(): void { (new BeforeEachCall(TestSuite::getInstance(), $this->filename))->only(); } /** * Depending on where is called, it will extend the given classes and traits globally or locally. */ public function use(string ...$classAndTraits): UsesCall { 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 { return $this->extends(...$classAndTraits); } /** * Gets the printer configuration. */ public function printer(): Configuration\Printer { return new Configuration\Printer; } /** * Gets the presets configuration. */ public function presets(): Configuration\Presets { return new Configuration\Presets; } /** * Gets the project configuration. */ public function project(): Configuration\Project { return Configuration\Project::getInstance(); } /** * Gets the browser configuration. */ public function browser(): Browser\Configuration { return new Browser\Configuration; } /** * Proxies calls to the uses method. * * @param array $arguments */ public function __call(string $name, array $arguments): mixed { return $this->uses()->$name(...$arguments); // @phpstan-ignore-line } } ================================================ FILE: src/Console/Help.php ================================================ */ private const array HELP_MESSAGES = [ 'Pest Options:', ' --init Initialise a standard Pest configuration', ' --coverage Enable coverage and output to standard output', ' --min= Set the minimum required coverage percentage (), and fail if not met', ' --group= Only runs tests from the specified group(s)', ]; /** * Creates a new Console Command instance. */ public function __construct(private OutputInterface $output) { // .. } /** * Executes the Console Command. */ public function __invoke(): void { foreach (self::HELP_MESSAGES as $message) { $this->output->writeln($message); } } } ================================================ FILE: src/Console/Thanks.php ================================================ */ private const array FUNDING_MESSAGES = [ 'Star' => 'https://github.com/pestphp/pest', 'YouTube' => 'https://youtube.com/@nunomaduro', 'TikTok' => 'https://tiktok.com/@enunomaduro', 'Twitch' => 'https://twitch.tv/nunomaduro', 'LinkedIn' => 'https://linkedin.com/in/nunomaduro', 'Instagram' => 'https://instagram.com/enunomaduro', 'X' => 'https://x.com/enunomaduro', 'Sponsor' => 'https://github.com/sponsors/nunomaduro', ]; /** * Creates a new Console Command instance. */ public function __construct( private InputInterface $input, private OutputInterface $output ) { // .. } /** * Executes the Console Command. */ public function __invoke(): void { $bootstrapper = new BootView($this->output); $bootstrapper->boot(); $wantsToSupport = false; if (getenv('PEST_NO_SUPPORT') !== 'true' && $this->input->isInteractive()) { $wantsToSupport = (new SymfonyQuestionHelper)->ask( new ArrayInput([]), $this->output, new ConfirmationQuestion( ' Wanna show Pest some love by starring it on GitHub?', false, ) ); View::render('components.new-line'); foreach (self::FUNDING_MESSAGES as $message => $link) { View::render('components.two-column-detail', [ 'left' => $message, 'right' => $link, ]); } View::render('components.new-line'); } if ($wantsToSupport === true) { if (PHP_OS_FAMILY === 'Darwin') { exec('open https://github.com/pestphp/pest'); } if (PHP_OS_FAMILY === 'Windows') { exec('start https://github.com/pestphp/pest'); } if (PHP_OS_FAMILY === 'Linux') { exec('xdg-open https://github.com/pestphp/pest'); } } } } ================================================ FILE: src/Contracts/ArchPreset.php ================================================ $arguments * @return array */ public function handleArguments(array $arguments): array; } ================================================ FILE: src/Contracts/Plugins/HandlesOriginalArguments.php ================================================ $arguments */ public function handleOriginalArguments(array $arguments): void; } ================================================ FILE: src/Contracts/Plugins/Terminable.php ================================================ $attributes */ public static function code(iterable $attributes): string { return implode(PHP_EOL, array_map(function (Attribute $attribute): string { $name = $attribute->name; if ($attribute->arguments === []) { return " #[\\{$name}]"; } $arguments = array_map(fn (string $argument): string => var_export($argument, true), iterator_to_array($attribute->arguments)); return sprintf(' #[\\%s(%s)]', $name, implode(', ', $arguments)); }, iterator_to_array($attributes))); } } ================================================ FILE: src/Exceptions/AfterAllAlreadyExist.php ================================================ $arguments */ public function __construct(string $file, string $name, array $arguments) { parent::__construct(sprintf( 'A test with the description [%s] has [%d] argument(s) ([%s]) and no dataset(s) provided in [%s]', $name, count($arguments), implode(', ', array_map(static fn (string $arg, string $type): string => sprintf('%s $%s', $type, $arg), array_keys($arguments), $arguments)), $file, )); } } ================================================ FILE: src/Exceptions/ExpectationNotFound.php ================================================ $methods * * @throws self */ public static function fromMethods(array $methods): never { throw new self(sprintf('Expectation [%s] is not valid.', implode('->', $methods))); } } ================================================ FILE: src/Exceptions/InvalidExpectationValue.php ================================================ writeln([ '', ' INFO No "dirty" tests found.', '', ]); } /** * The exit code to be used. */ public function exitCode(): int { return 0; } } ================================================ FILE: src/Exceptions/ShouldNotHappen.php ================================================ getMessage(); parent::__construct(sprintf(<<<'EOF' This should not happen - please create an new issue here: https://github.com/pestphp/pest/issues Issue: %s PHP version: %s Operating system: %s EOF , $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 { return new ShouldNotHappen(new Exception($message)); } } ================================================ FILE: src/Exceptions/TestAlreadyExist.php ================================================ description, $method->filename ) ); } } ================================================ FILE: src/Exceptions/TestDescriptionMissing.php ================================================ * @mixin PendingArchExpectation */ final class Expectation { /** @use Extendable> */ use Extendable; use Pipeable; use Retrievable; /** * Creates a new expectation. * * @param TValue $value */ public function __construct( public mixed $value ) { // .. } /** * Creates a new expectation. * * @template TAndValue * * @param TAndValue $value * @return self */ public function and(mixed $value): Expectation { return $value instanceof self ? $value : new self($value); } /** * Creates a new expectation with the decoded JSON value. * * @return self|bool> */ public function json(): Expectation { if (! is_string($this->value)) { InvalidExpectationValue::expected('string'); } $this->toBeJson(); /** @var array|bool $value */ $value = json_decode($this->value, true, 512, JSON_THROW_ON_ERROR); return $this->and($value); } /** * Dump the expectation value. * * @return self */ public function dump(mixed ...$arguments): self { if (function_exists('dump')) { dump($this->value, ...$arguments); } else { var_dump($this->value); } return $this; } /** * Dump the expectation value and end the script. * * @return never */ public function dd(mixed ...$arguments): void { if (function_exists('dd')) { dd($this->value, ...$arguments); } var_dump($this->value); exit(1); } /** * Dump the expectation value when the result of the condition is truthy. * * @param (Closure(TValue): bool)|bool $condition * @return self */ public function ddWhen(Closure|bool $condition, mixed ...$arguments): Expectation { $condition = $condition instanceof Closure ? $condition($this->value) : $condition; if (! $condition) { return $this; } $this->dd(...$arguments); } /** * Dump the expectation value when the result of the condition is falsy. * * @param (Closure(TValue): bool)|bool $condition * @return self */ public function ddUnless(Closure|bool $condition, mixed ...$arguments): Expectation { $condition = $condition instanceof Closure ? $condition($this->value) : $condition; if ($condition) { return $this; } $this->dd(...$arguments); } /** * Send the expectation value to Ray along with all given arguments. * * @return self */ public function ray(mixed ...$arguments): self { if (function_exists('ray')) { ray($this->value, ...$arguments); } return $this; } /** * Creates the opposite expectation for the value. * * @return OppositeExpectation */ public function not(): OppositeExpectation { return new OppositeExpectation($this); } /** * Creates an expectation on each item of the iterable "value". * * @return EachExpectation */ public function each(?callable $callback = null): EachExpectation { if (! is_iterable($this->value)) { throw new BadMethodCallException('Expectation value is not iterable.'); } if (is_callable($callback)) { foreach ($this->value as $key => $item) { $callback(new self($item), $key); } } return new EachExpectation($this); } /** * Allows you to specify a sequential set of expectations for each item in a iterable "value". * * @template TSequenceValue * * @param (callable(self, self): void)|TSequenceValue ...$callbacks * @return self */ public function sequence(mixed ...$callbacks): self { if (! is_iterable($this->value)) { throw new BadMethodCallException('Expectation value is not iterable.'); } if ($callbacks === []) { throw new InvalidArgumentException('No sequence expectations defined.'); } $index = $valuesCount = 0; foreach ($this->value as $key => $value) { $valuesCount++; if ($callbacks[$index] instanceof Closure) { $callbacks[$index](new self($value), new self($key)); } else { (new self($value))->toEqual($callbacks[$index]); } $index = isset($callbacks[$index + 1]) ? $index + 1 : 0; } if ($valuesCount < count($callbacks)) { throw new OutOfRangeException('Sequence expectations are more than the iterable items.'); } return $this; } /** * If the subject matches one of the given "expressions", the expression callback will run. * * @template TMatchSubject of array-key * * @param (callable(): TMatchSubject)|TMatchSubject $subject * @param array): mixed)|TValue> $expressions * @return self */ public function match(mixed $subject, array $expressions): self { $subject = $subject instanceof Closure ? $subject() : $subject; $matched = false; foreach ($expressions as $key => $callback) { if ($subject != $key) { // @pest-arch-ignore-line continue; } $matched = true; if (is_callable($callback)) { $callback(new self($this->value)); continue; } $this->and($this->value)->toEqual($callback); break; } if ($matched === false) { throw new ExpectationFailedException('Unhandled match value.'); } return $this; } /** * Apply the callback if the given "condition" is falsy. * * @param (callable(): bool)|bool $condition * @param callable(Expectation): mixed $callback * @return self */ public function unless(callable|bool $condition, callable $callback): Expectation { $condition = is_callable($condition) ? $condition : static fn (): bool => $condition; return $this->when(! $condition(), $callback); } /** * Apply the callback if the given "condition" is truthy. * * @param (callable(): bool)|bool $condition * @param callable(self): mixed $callback * @return self */ public function when(callable|bool $condition, callable $callback): self { $condition = is_callable($condition) ? $condition : static fn (): bool => $condition; if ($condition()) { $callback($this->and($this->value)); } return $this; } /** * Dynamically calls methods on the class or creates a new higher order expectation. * * @param array $parameters * @return Expectation|HigherOrderExpectation, TValue> */ public function __call(string $method, array $parameters): Expectation|HigherOrderExpectation|PendingArchExpectation|ArchExpectation { if (! self::hasMethod($method)) { if (! is_object($this->value) && method_exists(PendingArchExpectation::class, $method)) { $pendingArchExpectation = new PendingArchExpectation($this, []); return $pendingArchExpectation->$method(...$parameters); // @phpstan-ignore-line } if (! is_object($this->value)) { throw new BadMethodCallException(sprintf( 'Method "%s" does not exist in %s.', $method, gettype($this->value) )); } /* @phpstan-ignore-next-line */ return new HigherOrderExpectation($this, call_user_func_array($this->value->$method(...), $parameters)); } $closure = $this->getExpectationClosure($method); $reflectionClosure = new \ReflectionFunction($closure); $expectation = $reflectionClosure->getClosureThis(); if ($reflectionClosure->getReturnType()?->__toString() === ArchExpectation::class) { return $closure(...$parameters); } assert(is_object($expectation)); ExpectationPipeline::for($closure) ->send(...$parameters) ->through($this->pipes($method, $expectation, Expectation::class)) ->run(); return $this; } /** * Creates a new expectation closure from the given name. * * @throws ExpectationNotFound */ private function getExpectationClosure(string $name): Closure { if (method_exists(Mixins\Expectation::class, $name)) { // @phpstan-ignore-next-line return Closure::fromCallable([new Mixins\Expectation($this->value), $name]); } if (self::hasExtend($name)) { $extend = self::$extends[$name]->bindTo($this, Expectation::class); if ($extend != false) { // @pest-arch-ignore-line return $extend; } } throw ExpectationNotFound::fromName($name); } /** * Dynamically calls methods on the class without any arguments or creates a new higher order expectation. * * @return Expectation|OppositeExpectation|EachExpectation|HigherOrderExpectation, TValue|null>|TValue */ public function __get(string $name): mixed { if (! self::hasMethod($name)) { if (! is_object($this->value) && method_exists(PendingArchExpectation::class, $name)) { /* @phpstan-ignore-next-line */ return $this->{$name}(); } /* @phpstan-ignore-next-line */ return new HigherOrderExpectation($this, $this->retrieve($name, $this->value)); } /* @phpstan-ignore-next-line */ return $this->{$name}(); } /** * Checks if the given expectation method exists. */ public static function hasMethod(string $name): bool { return method_exists(self::class, $name) || method_exists(Mixins\Expectation::class, $name) || self::hasExtend($name); } /** * Matches any value. */ public function any(): Any { return new Any; } /** * Asserts that the given expectation target use the given dependencies. * * @param array|string $targets */ public function toUse(array|string $targets): ArchExpectation { return ToUse::make($this, $targets); } /** * Asserts that the given expectation target does have the given permissions */ public function toHaveFileSystemPermissions(string $permissions): ArchExpectation { return Targeted::make( $this, fn (ObjectDescription $object): bool => substr(sprintf('%o', fileperms($object->path)), -4) === $permissions, sprintf('permissions to be [%s]', $permissions), FileLineFinder::where(fn (string $line): bool => str_contains($line, ' count(file($object->path)) < $lines, // @phpstan-ignore-line sprintf('to have less than %d lines of code', $lines), FileLineFinder::where(fn (string $line): bool => str_contains($line, ' isset($object->reflectionClass) === false || array_filter( Reflection::getMethodsFromReflectionClass($object->reflectionClass), fn (ReflectionMethod $method): bool => (enum_exists($object->name) === false || in_array($method->name, ['from', 'tryFrom', 'cases'], true) === false) && realpath($method->getFileName() ?: '/') === realpath($object->path) // @phpstan-ignore-line && $method->getDocComment() === false, ) === [], 'to have methods with documentation / annotations', FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')) ); } /** * Asserts that the given expectation target have all properties documented. */ public function toHavePropertiesDocumented(): ArchExpectation { return Targeted::make( $this, fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false || array_filter( Reflection::getPropertiesFromReflectionClass($object->reflectionClass), fn (ReflectionProperty $property): bool => (enum_exists($object->name) === false || in_array($property->name, ['value', 'name'], true) === false) && realpath($property->getDeclaringClass()->getFileName() ?: '/') === realpath($object->path) // @phpstan-ignore-line && $property->isPromoted() === false && $property->getDocComment() === false, ) === [], 'to have properties with documentation / annotations', FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')) ); } /** * Asserts that the given expectation target use the "declare(strict_types=1)" declaration. */ public function toUseStrictTypes(): ArchExpectation { return Targeted::make( $this, fn (ObjectDescription $object): bool => (bool) preg_match('/^<\?php\s*(\/\*[\s\S]*?\*\/|\/\/[^\r\n]*(?:\r?\n|$)|\s)*declare\s*\(\s*strict_types\s*=\s*1\s*\)\s*;/m', (string) file_get_contents($object->path)), 'to use strict types', FileLineFinder::where(fn (string $line): bool => str_contains($line, ' ! str_contains((string) file_get_contents($object->path), ' == ') && ! str_contains((string) file_get_contents($object->path), ' != '), 'to use strict equality', FileLineFinder::where(fn (string $line): bool => str_contains($line, ' == ') || str_contains($line, ' != ')), ); } /** * Asserts that the given expectation target is final. */ public function toBeFinal(): ArchExpectation { return Targeted::make( $this, fn (ObjectDescription $object): bool => ! enum_exists($object->name) && isset($object->reflectionClass) && $object->reflectionClass->isFinal(), 'to be final', FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); } /** * Asserts that the given expectation target is readonly. */ public function toBeReadonly(): ArchExpectation { return Targeted::make( $this, fn (ObjectDescription $object): bool => ! enum_exists($object->name) && isset($object->reflectionClass) && $object->reflectionClass->isReadOnly() && assert(true), // @phpstan-ignore-line 'to be readonly', FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); } /** * Asserts that the given expectation target is trait. */ public function toBeTrait(): ArchExpectation { return Targeted::make( $this, fn (ObjectDescription $object): bool => isset($object->reflectionClass) && $object->reflectionClass->isTrait(), 'to be trait', FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); } /** * Asserts that the given expectation targets are traits. */ public function toBeTraits(): ArchExpectation { return $this->toBeTrait(); } /** * Asserts that the given expectation target is abstract. */ public function toBeAbstract(): ArchExpectation { return Targeted::make( $this, fn (ObjectDescription $object): bool => isset($object->reflectionClass) && $object->reflectionClass->isAbstract(), 'to be abstract', FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); } /** * Asserts that the given expectation target has a specific method. * * @param array|string $method */ public function toHaveMethod(array|string $method): ArchExpectation { $methods = is_array($method) ? $method : [$method]; return Targeted::make( $this, 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)), FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); } /** * Asserts that the given expectation target has a specific methods. * * @param array $methods */ public function toHaveMethods(array $methods): ArchExpectation { return $this->toHaveMethod($methods); } /** * Not supported. */ public function toHavePublicMethodsBesides(): void { throw InvalidExpectation::fromMethods(['toHavePublicMethodsBesides']); } /** * Not supported. */ public function toHavePublicMethods(): void { throw InvalidExpectation::fromMethods(['toHavePublicMethods']); } /** * Not supported. */ public function toHaveProtectedMethodsBesides(): void { throw InvalidExpectation::fromMethods(['toHaveProtectedMethodsBesides']); } /** * Not supported. */ public function toHaveProtectedMethods(): void { throw InvalidExpectation::fromMethods(['toHaveProtectedMethods']); } /** * Not supported. */ public function toHavePrivateMethodsBesides(): void { throw InvalidExpectation::fromMethods(['toHavePrivateMethodsBesides']); } /** * Not supported. */ public function toHavePrivateMethods(): void { throw InvalidExpectation::fromMethods(['toHavePrivateMethods']); } /** * Asserts that the given expectation target is enum. */ public function toBeEnum(): ArchExpectation { return Targeted::make( $this, fn (ObjectDescription $object): bool => isset($object->reflectionClass) && $object->reflectionClass->isEnum(), 'to be enum', FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); } /** * Asserts that the given expectation targets are enums. */ public function toBeEnums(): ArchExpectation { return $this->toBeEnum(); } /** * Asserts that the given expectation target is a class. */ public function toBeClass(): ArchExpectation { return Targeted::make( $this, fn (ObjectDescription $object): bool => class_exists($object->name) && ! enum_exists($object->name), 'to be class', FileLineFinder::where(fn (string $line): bool => true), ); } /** * Asserts that the given expectation targets are classes. */ public function toBeClasses(): ArchExpectation { return $this->toBeClass(); } /** * Asserts that the given expectation target is interface. */ public function toBeInterface(): ArchExpectation { return Targeted::make( $this, fn (ObjectDescription $object): bool => isset($object->reflectionClass) && $object->reflectionClass->isInterface(), 'to be interface', FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); } /** * Asserts that the given expectation targets are interfaces. */ public function toBeInterfaces(): ArchExpectation { return $this->toBeInterface(); } /** * Asserts that the given expectation target to be subclass of the given class. */ public function toExtend(string $class): ArchExpectation { return Targeted::make( $this, fn (ObjectDescription $object): bool => isset($object->reflectionClass) && ($class === $object->reflectionClass->getName() || $object->reflectionClass->isSubclassOf($class)), sprintf("to extend '%s'", $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 { return Targeted::make( $this, fn (ObjectDescription $object): bool => $object->reflectionClass->getParentClass() === false, 'to extend nothing', FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); } /** * Asserts that the given expectation target to use the given trait. */ public function toUseTrait(string $trait): ArchExpectation { return $this->toUseTraits($trait); } /** * Asserts that the given expectation target to use the given traits. * * @param array|string $traits */ public function toUseTraits(array|string $traits): ArchExpectation { $traits = is_array($traits) ? $traits : [$traits]; return Targeted::make( $this, function (ObjectDescription $object) use ($traits): bool { foreach ($traits as $trait) { if (isset($object->reflectionClass) === false) { return false; } if (! in_array($trait, $object->reflectionClass->getTraitNames(), true)) { return false; } } return true; }, "to use traits '".implode("', '", $traits)."'", FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); } /** * Asserts that the given expectation target to not implement any interfaces. */ public function toImplementNothing(): ArchExpectation { return Targeted::make( $this, fn (ObjectDescription $object): bool => isset($object->reflectionClass) && $object->reflectionClass->getInterfaceNames() === [], 'to implement nothing', FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); } /** * Asserts that the given expectation target to only implement the given interfaces. * * @param array|string $interfaces */ public function toOnlyImplement(array|string $interfaces): ArchExpectation { $interfaces = is_array($interfaces) ? $interfaces : [$interfaces]; return Targeted::make( $this, fn (ObjectDescription $object): bool => isset($object->reflectionClass) && (count($interfaces) === count($object->reflectionClass->getInterfaceNames())) && array_diff($interfaces, $object->reflectionClass->getInterfaceNames()) === [], "to only implement '".implode("', '", $interfaces)."'", FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); } /** * Asserts that the given expectation target to have the given prefix. */ public function toHavePrefix(string $prefix): ArchExpectation { return Targeted::make( $this, fn (ObjectDescription $object): bool => isset($object->reflectionClass) && str_starts_with($object->reflectionClass->getShortName(), $prefix), "to have prefix '{$prefix}'", 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 { return Targeted::make( $this, fn (ObjectDescription $object): bool => isset($object->reflectionClass) && str_ends_with($object->reflectionClass->getName(), $suffix), "to have suffix '{$suffix}'", FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); } /** * Asserts that the given expectation target to implement the given interfaces. * * @param array|string $interfaces */ public function toImplement(array|string $interfaces): ArchExpectation { $interfaces = is_array($interfaces) ? $interfaces : [$interfaces]; return Targeted::make( $this, function (ObjectDescription $object) use ($interfaces): bool { foreach ($interfaces as $interface) { if (! isset($object->reflectionClass) || ! $object->reflectionClass->implementsInterface($interface)) { return false; } } return true; }, "to implement '".implode("', '", $interfaces)."'", FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); } /** * Asserts that the given expectation target "only" use on the given dependencies. * * @param array|string $targets */ public function toOnlyUse(array|string $targets): ArchExpectation { return ToOnlyUse::make($this, $targets); } /** * Asserts that the given expectation target does not use any dependencies. */ public function toUseNothing(): ArchExpectation { return ToUseNothing::make($this); } /** * Asserts that the source code of the given expectation target does not include suspicious characters. */ public function toHaveSuspiciousCharacters(): ArchExpectation { throw InvalidExpectation::fromMethods(['toHaveSuspiciousCharacters']); } /** * Not supported. */ public function toBeUsed(): void { throw InvalidExpectation::fromMethods(['toBeUsed']); } /** * Asserts that the given expectation dependency is used by the given targets. * * @param array|string $targets */ public function toBeUsedIn(array|string $targets): ArchExpectation { return ToBeUsedIn::make($this, $targets); } /** * Asserts that the given expectation dependency is "only" used by the given targets. * * @param array|string $targets */ public function toOnlyBeUsedIn(array|string $targets): ArchExpectation { return ToOnlyBeUsedIn::make($this, $targets); } /** * Asserts that the given expectation dependency is not used. */ public function toBeUsedInNothing(): ArchExpectation { return ToBeUsedInNothing::make($this); } /** * Asserts that the given expectation dependency is an invokable class. */ public function toBeInvokable(): ArchExpectation { return Targeted::make( $this, fn (ObjectDescription $object): bool => isset($object->reflectionClass) && $object->reflectionClass->hasMethod('__invoke'), 'to be invokable', FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')) ); } /** * Asserts that the given expectation is iterable and contains snake_case keys. * * @return self */ public function toHaveSnakeCaseKeys(string $message = ''): self { if (! is_iterable($this->value)) { InvalidExpectationValue::expected('iterable'); } foreach ($this->value as $k => $item) { if (is_string($k)) { $this->and($k)->toBeSnakeCase($message); } if (is_array($item)) { $this->and($item)->toHaveSnakeCaseKeys($message); } } return $this; } /** * Asserts that the given expectation is iterable and contains kebab-case keys. * * @return self */ public function toHaveKebabCaseKeys(string $message = ''): self { if (! is_iterable($this->value)) { InvalidExpectationValue::expected('iterable'); } foreach ($this->value as $k => $item) { if (is_string($k)) { $this->and($k)->toBeKebabCase($message); } if (is_array($item)) { $this->and($item)->toHaveKebabCaseKeys($message); } } return $this; } /** * Asserts that the given expectation is iterable and contains camelCase keys. * * @return self */ public function toHaveCamelCaseKeys(string $message = ''): self { if (! is_iterable($this->value)) { InvalidExpectationValue::expected('iterable'); } foreach ($this->value as $k => $item) { if (is_string($k)) { $this->and($k)->toBeCamelCase($message); } if (is_array($item)) { $this->and($item)->toHaveCamelCaseKeys($message); } } return $this; } /** * Asserts that the given expectation is iterable and contains StudlyCase keys. * * @return self */ public function toHaveStudlyCaseKeys(string $message = ''): self { if (! is_iterable($this->value)) { InvalidExpectationValue::expected('iterable'); } foreach ($this->value as $k => $item) { if (is_string($k)) { $this->and($k)->toBeStudlyCase($message); } if (is_array($item)) { $this->and($item)->toHaveStudlyCaseKeys($message); } } return $this; } /** * Asserts that the given expectation target to have the given attribute. */ public function toHaveAttribute(string $attribute): ArchExpectation { return Targeted::make( $this, fn (ObjectDescription $object): bool => isset($object->reflectionClass) && $object->reflectionClass->getAttributes($attribute) !== [], "to have attribute '{$attribute}'", FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); } /** * Asserts that the given expectation target has a constructor method. */ public function toHaveConstructor(): ArchExpectation { return $this->toHaveMethod('__construct'); } /** * Asserts that the given expectation target has a destructor method. */ public function toHaveDestructor(): ArchExpectation { return $this->toHaveMethod('__destruct'); } /** * Asserts that the given expectation target is a backed enum of given type. */ private function toBeBackedEnum(string $backingType): ArchExpectation { return Targeted::make( $this, fn (ObjectDescription $object): bool => isset($object->reflectionClass) && $object->reflectionClass->isEnum() && (new ReflectionEnum($object->name))->isBacked() // @phpstan-ignore-line && (string) (new ReflectionEnum($object->name))->getBackingType() === $backingType, // @phpstan-ignore-line 'to be '.$backingType.' backed enum', FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); } /** * Asserts that the given expectation targets are string backed enums. */ public function toBeStringBackedEnums(): ArchExpectation { return $this->toBeStringBackedEnum(); } /** * Asserts that the given expectation targets are int backed enums. */ public function toBeIntBackedEnums(): ArchExpectation { return $this->toBeIntBackedEnum(); } /** * Asserts that the given expectation target is a string backed enum. */ public function toBeStringBackedEnum(): ArchExpectation { return $this->toBeBackedEnum('string'); } /** * Asserts that the given expectation target is an int backed enum. */ public function toBeIntBackedEnum(): ArchExpectation { return $this->toBeBackedEnum('int'); } } ================================================ FILE: src/Expectations/EachExpectation.php ================================================ */ final class EachExpectation { /** * Indicates if the expectation is the opposite. */ private bool $opposite = false; /** * Creates an expectation on each item of the iterable "value". * * @param Expectation $original */ public function __construct(private readonly Expectation $original) {} /** * Creates a new expectation. * * @template TAndValue * * @param TAndValue $value * @return Expectation */ public function and(mixed $value): Expectation { return $this->original->and($value); } /** * Creates the opposite expectation for the value. * * @return self */ public function not(): self { $this->opposite = true; return $this; } /** * Dynamically calls methods on the class with the given arguments on each item. * * @param array $arguments * @return self */ public function __call(string $name, array $arguments): self { foreach ($this->original->value as $item) { /* @phpstan-ignore-next-line */ $this->opposite ? expect($item)->not()->$name(...$arguments) : expect($item)->$name(...$arguments); } $this->opposite = false; return $this; } /** * Dynamically calls methods on the class without any arguments on each item. * * @return self */ public function __get(string $name): self { /* @phpstan-ignore-next-line */ return $this->$name(); } } ================================================ FILE: src/Expectations/HigherOrderExpectation.php ================================================ */ final class HigherOrderExpectation { use Retrievable; /** * @var Expectation|EachExpectation */ private Expectation|EachExpectation $expectation; /** * Indicates if the expectation is the opposite. */ private bool $opposite = false; /** * Indicates if the expectation should reset the value. */ private bool $shouldReset = false; /** * Creates a new higher order expectation. * * @param Expectation $original * @param TValue $value */ public function __construct(private readonly Expectation $original, mixed $value) { $this->expectation = $this->expect($value); } /** * Creates the opposite expectation for the value. * * @return self */ public function not(): self { $this->opposite = ! $this->opposite; return $this; } /** * Creates a new Expectation. * * @template TExpectValue * * @param TExpectValue $value * @return Expectation */ public function expect(mixed $value): Expectation { return new Expectation($value); } /** * Creates a new expectation. * * @template TExpectValue * * @param TExpectValue $value * @return Expectation */ public function and(mixed $value): Expectation { return $this->expect($value); } /** * Scope an expectation callback to the current value in * the HigherOrderExpectation chain. * * @param Closure(Expectation): void $expectation * @return HigherOrderExpectation */ public function scoped(Closure $expectation): self { $expectation->__invoke($this->expectation); return new self($this->original, $this->original->value); } /** * Creates a new expectation with the decoded JSON value. * * @return self|bool> */ public function json(): self { return new self($this->original, $this->expectation->json()->value); } /** * Dynamically calls methods on the class with the given arguments. * * @param array $arguments * @return self|self */ public function __call(string $name, array $arguments): self { if (! $this->expectationHasMethod($name)) { /* @phpstan-ignore-next-line */ return new self($this->original, $this->getValue()->$name(...$arguments)); } return $this->performAssertion($name, $arguments); } /** * Accesses properties in the value or in the expectation. * * @return self|self */ public function __get(string $name): self { if ($name === 'not') { return $this->not(); } if (! $this->expectationHasMethod($name)) { /** @var array|object $value */ $value = $this->getValue(); return new self($this->original, $this->retrieve($name, $value)); } return $this->performAssertion($name, []); } /** * Determines if the original expectation has the given method name. */ private function expectationHasMethod(string $name): bool { if (method_exists($this->original, $name)) { return true; } if ($this->original::hasMethod($name)) { return true; } return $this->original::hasExtend($name); } /** * Retrieve the applicable value based on the current reset condition. * * @return TOriginalValue|TValue */ private function getValue(): mixed { return $this->shouldReset ? $this->original->value : $this->expectation->value; } /** * Performs the given assertion with the current expectation. * * @param array $arguments * @return self */ private function performAssertion(string $name, array $arguments): self { /* @phpstan-ignore-next-line */ $this->expectation = ($this->opposite ? $this->expectation->not() : $this->expectation)->{$name}(...$arguments); $this->opposite = false; $this->shouldReset = true; return $this; } } ================================================ FILE: src/Expectations/OppositeExpectation.php ================================================ */ final readonly class OppositeExpectation { /** * Creates a new opposite expectation. * * @param Expectation $original */ public function __construct(private Expectation $original) {} /** * Asserts that the value array not has the provided $keys. * * @param array> $keys * @return Expectation */ public function toHaveKeys(array $keys): Expectation { foreach ($keys as $k => $key) { try { if (is_array($key)) { $this->toHaveKeys(array_keys(Arr::dot($key, $k.'.'))); } else { $this->original->toHaveKey($key); } } catch (ExpectationFailedException) { continue; } $this->throwExpectationFailedException('toHaveKey', [$key]); } return $this->original; } /** * Asserts that the given expectation target does not use any of the given dependencies. * * @param array|string $targets */ public function toUse(array|string $targets): ArchExpectation { /** @var Expectation|string> $original */ $original = $this->original; return GroupArchExpectation::fromExpectations($original, array_map(fn (string $target): SingleArchExpectation => ToUse::make($original, $target)->opposite( fn () => $this->throwExpectationFailedException('toUse', $target), ), is_string($targets) ? [$targets] : $targets)); } /** * Asserts that the given expectation target does not have the given permissions */ public function toHaveFileSystemPermissions(string $permissions): ArchExpectation { /** @var Expectation|string> $original */ $original = $this->original; return Targeted::make( $original, fn (ObjectDescription $object): bool => substr(sprintf('%o', fileperms($object->path)), -4) !== $permissions, sprintf('permissions not to be [%s]', $permissions), FileLineFinder::where(fn (string $line): bool => str_contains($line, '|string> $original */ $original = $this->original; return Targeted::make( $original, fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false || array_filter( Reflection::getMethodsFromReflectionClass($object->reflectionClass), fn (ReflectionMethod $method): bool => (enum_exists($object->name) === false || in_array($method->name, ['from', 'tryFrom', 'cases'], true) === false) && realpath($method->getFileName() ?: '/') === realpath($object->path) // @phpstan-ignore-line && $method->getDocComment() !== false, ) === [], 'to have methods without documentation / annotations', FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')) ); } /** * Not supported. */ public function toHavePropertiesDocumented(): ArchExpectation { /** @var Expectation|string> $original */ $original = $this->original; return Targeted::make( $original, fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false || array_filter( Reflection::getPropertiesFromReflectionClass($object->reflectionClass), fn (ReflectionProperty $property): bool => (enum_exists($object->name) === false || in_array($property->name, ['value', 'name'], true) === false) && realpath($property->getDeclaringClass()->getFileName() ?: '/') === realpath($object->path) // @phpstan-ignore-line && $property->isPromoted() === false && $property->getDocComment() !== false, ) === [], 'to have properties without documentation / annotations', FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')) ); } /** * Asserts that the given expectation target does not use the "declare(strict_types=1)" declaration. */ public function toUseStrictTypes(): ArchExpectation { /** @var Expectation|string> $original */ $original = $this->original; return Targeted::make( $original, fn (ObjectDescription $object): bool => ! (bool) preg_match('/^<\?php\s+declare\(.*?strict_types\s?=\s?1.*?\);/', (string) file_get_contents($object->path)), 'not to use strict types', FileLineFinder::where(fn (string $line): bool => str_contains($line, '|string> $original */ $original = $this->original; return Targeted::make( $original, fn (ObjectDescription $object): bool => ! str_contains((string) file_get_contents($object->path), ' === ') && ! str_contains((string) file_get_contents($object->path), ' !== '), 'to use strict equality', FileLineFinder::where(fn (string $line): bool => str_contains($line, ' === ') || str_contains($line, ' !== ')), ); } /** * Asserts that the given expectation target is not final. */ public function toBeFinal(): ArchExpectation { /** @var Expectation|string> $original */ $original = $this->original; return Targeted::make( $original, fn (ObjectDescription $object): bool => ! enum_exists($object->name) && (isset($object->reflectionClass) === false || ! $object->reflectionClass->isFinal()), 'not to be final', FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); } /** * Asserts that the given expectation target is not readonly. */ public function toBeReadonly(): ArchExpectation { /** @var Expectation|string> $original */ $original = $this->original; return Targeted::make( $original, fn (ObjectDescription $object): bool => ! enum_exists($object->name) && (isset($object->reflectionClass) === false || ! $object->reflectionClass->isReadOnly()) && assert(true), // @phpstan-ignore-line 'not to be readonly', FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); } /** * Asserts that the given expectation target is not trait. */ public function toBeTrait(): ArchExpectation { /** @var Expectation|string> $original */ $original = $this->original; return Targeted::make( $original, fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false || ! $object->reflectionClass->isTrait(), 'not to be trait', FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); } /** * Asserts that the given expectation targets are not traits. */ public function toBeTraits(): ArchExpectation { return $this->toBeTrait(); } /** * Asserts that the given expectation target is not abstract. */ public function toBeAbstract(): ArchExpectation { /** @var Expectation|string> $original */ $original = $this->original; return Targeted::make( $original, fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false || ! $object->reflectionClass->isAbstract(), 'not to be abstract', FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); } /** * Asserts that the given expectation target does not have a specific method. * * @param array|string $method */ public function toHaveMethod(array|string $method): ArchExpectation { $methods = is_array($method) ? $method : [$method]; /** @var Expectation|string> $original */ $original = $this->original; return Targeted::make( $original, fn (ObjectDescription $object): bool => array_filter( $methods, fn (string $method): bool => isset($object->reflectionClass) === false || $object->reflectionClass->hasMethod($method), ) === [], 'to not have methods: '.implode(', ', $methods), FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); } /** * Asserts that the given expectation target does not have suspicious characters. */ public function toHaveSuspiciousCharacters(): ArchExpectation { if (! class_exists(Spoofchecker::class)) { throw new MissingDependency(__FUNCTION__, 'ext-intl >= 2.0'); } $checker = new Spoofchecker; /** @var Expectation|string> $original */ $original = $this->original; return Targeted::make( $original, fn (ObjectDescription $object): bool => ! $checker->isSuspicious((string) file_get_contents($object->path)), 'to not include suspicious characters', FileLineFinder::where(fn (string $line): bool => $checker->isSuspicious($line)), ); } /** * Asserts that the given expectation target does not have the given methods. * * @param array $methods */ public function toHaveMethods(array $methods): ArchExpectation { return $this->toHaveMethod($methods); } /** * Asserts that the given expectation target not to have the public methods besides the given methods. * * @param array|string $methods */ public function toHavePublicMethodsBesides(array|string $methods): ArchExpectation { $methods = is_array($methods) ? $methods : [$methods]; $state = new stdClass; /** @var Expectation|string> $original */ $original = $this->original; return Targeted::make( $original, function (ObjectDescription $object) use ($methods, &$state): bool { $reflectionMethods = isset($object->reflectionClass) ? Reflection::getMethodsFromReflectionClass($object->reflectionClass, ReflectionMethod::IS_PUBLIC) : []; foreach ($reflectionMethods as $reflectionMethod) { if (! in_array($reflectionMethod->name, $methods, true)) { $state->contains = 'public function '.$reflectionMethod->name; return false; } } return true; }, $methods === [] ? 'not to have public methods' : sprintf("not to have public methods besides '%s'", implode("', '", $methods)), 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 { return $this->toHavePublicMethodsBesides([]); } /** * Asserts that the given expectation target not to have the protected methods besides the given methods. * * @param array|string $methods */ public function toHaveProtectedMethodsBesides(array|string $methods): ArchExpectation { $methods = is_array($methods) ? $methods : [$methods]; $state = new stdClass; /** @var Expectation|string> $original */ $original = $this->original; return Targeted::make( $original, function (ObjectDescription $object) use ($methods, &$state): bool { $reflectionMethods = isset($object->reflectionClass) ? Reflection::getMethodsFromReflectionClass($object->reflectionClass, ReflectionMethod::IS_PROTECTED) : []; foreach ($reflectionMethods as $reflectionMethod) { if (! in_array($reflectionMethod->name, $methods, true)) { $state->contains = 'protected function '.$reflectionMethod->name; return false; } } return true; }, $methods === [] ? 'not to have protected methods' : sprintf("not to have protected methods besides '%s'", implode("', '", $methods)), 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 { return $this->toHaveProtectedMethodsBesides([]); } /** * Asserts that the given expectation target not to have the private methods besides the given methods. * * @param array|string $methods */ public function toHavePrivateMethodsBesides(array|string $methods): ArchExpectation { $methods = is_array($methods) ? $methods : [$methods]; $state = new stdClass; /** @var Expectation|string> $original */ $original = $this->original; return Targeted::make( $original, function (ObjectDescription $object) use ($methods, &$state): bool { $reflectionMethods = isset($object->reflectionClass) ? Reflection::getMethodsFromReflectionClass($object->reflectionClass, ReflectionMethod::IS_PRIVATE) : []; foreach ($reflectionMethods as $reflectionMethod) { if (! in_array($reflectionMethod->name, $methods, true)) { $state->contains = 'private function '.$reflectionMethod->name; return false; } } return true; }, $methods === [] ? 'not to have private methods' : sprintf("not to have private methods besides '%s'", implode("', '", $methods)), 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 { return $this->toHavePrivateMethodsBesides([]); } /** * Asserts that the given expectation target is not enum. */ public function toBeEnum(): ArchExpectation { /** @var Expectation|string> $original */ $original = $this->original; return Targeted::make( $original, fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false || ! $object->reflectionClass->isEnum(), 'not to be enum', FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); } /** * Asserts that the given expectation targets are not enums. */ public function toBeEnums(): ArchExpectation { return $this->toBeEnum(); } /** * Asserts that the given expectation targets is not class. */ public function toBeClass(): ArchExpectation { /** @var Expectation|string> $original */ $original = $this->original; return Targeted::make( $original, fn (ObjectDescription $object): bool => ! class_exists($object->name), 'not to be class', FileLineFinder::where(fn (string $line): bool => true), ); } /** * Asserts that the given expectation targets are not classes. */ public function toBeClasses(): ArchExpectation { return $this->toBeClass(); } /** * Asserts that the given expectation target is not interface. */ public function toBeInterface(): ArchExpectation { /** @var Expectation|string> $original */ $original = $this->original; return Targeted::make( $original, fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false || ! $object->reflectionClass->isInterface(), 'not to be interface', FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); } /** * Asserts that the given expectation targets are not interfaces. */ public function toBeInterfaces(): ArchExpectation { return $this->toBeInterface(); } /** * Asserts that the given expectation target to be not subclass of the given class. */ public function toExtend(string $class): ArchExpectation { /** @var Expectation|string> $original */ $original = $this->original; return Targeted::make( $original, fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false || ! $object->reflectionClass->isSubclassOf($class), sprintf("not to extend '%s'", $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 { /** @var Expectation|string> $original */ $original = $this->original; return Targeted::make( $original, fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false || $object->reflectionClass->getParentClass() !== false, 'to extend a class', FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); } /** * Asserts that the given expectation target not to use the given trait. */ public function toUseTrait(string $trait): ArchExpectation { return $this->toUseTraits($trait); } /** * Asserts that the given expectation target not to use the given traits. * * @param array|string $traits */ public function toUseTraits(array|string $traits): ArchExpectation { $traits = is_array($traits) ? $traits : [$traits]; /** @var Expectation|string> $original */ $original = $this->original; return Targeted::make( $original, function (ObjectDescription $object) use ($traits): bool { foreach ($traits as $trait) { if (isset($object->reflectionClass) && in_array($trait, $object->reflectionClass->getTraitNames(), true)) { return false; } } return true; }, "not to use traits '".implode("', '", $traits)."'", FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); } /** * Asserts that the given expectation target not to implement the given interfaces. * * @param array|string $interfaces */ public function toImplement(array|string $interfaces): ArchExpectation { $interfaces = is_array($interfaces) ? $interfaces : [$interfaces]; /** @var Expectation|string> $original */ $original = $this->original; return Targeted::make( $original, function (ObjectDescription $object) use ($interfaces): bool { foreach ($interfaces as $interface) { if (isset($object->reflectionClass) && $object->reflectionClass->implementsInterface($interface)) { return false; } } return true; }, "not to implement '".implode("', '", $interfaces)."'", FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); } /** * Asserts that the given expectation target to not implement any interfaces. */ public function toImplementNothing(): ArchExpectation { /** @var Expectation|string> $original */ $original = $this->original; return Targeted::make( $original, fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false || $object->reflectionClass->getInterfaceNames() !== [], 'to implement an interface', FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); } /** * Not supported. */ public function toOnlyImplement(): void { throw InvalidExpectation::fromMethods(['not', 'toOnlyImplement']); } /** * Asserts that the given expectation target to not have the given prefix. */ public function toHavePrefix(string $prefix): ArchExpectation { /** @var Expectation|string> $original */ $original = $this->original; return Targeted::make( $original, fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false || ! str_starts_with($object->reflectionClass->getShortName(), $prefix), "not to have prefix '{$prefix}'", 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 { /** @var Expectation|string> $original */ $original = $this->original; return Targeted::make( $original, fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false || ! str_ends_with($object->reflectionClass->getName(), $suffix), "not to have suffix '{$suffix}'", FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); } /** * Not supported. */ public function toOnlyUse(): void { throw InvalidExpectation::fromMethods(['not', 'toOnlyUse']); } /** * Not supported. */ public function toUseNothing(): void { throw InvalidExpectation::fromMethods(['not', 'toUseNothing']); } /** * Asserts that the given expectation dependency is not used. */ public function toBeUsed(): ArchExpectation { /** @var Expectation|string> $original */ $original = $this->original; return ToBeUsedInNothing::make($original); } /** * Asserts that the given expectation dependency is not used by any of the given targets. * * @param array|string $targets */ public function toBeUsedIn(array|string $targets): ArchExpectation { /** @var Expectation|string> $original */ $original = $this->original; return GroupArchExpectation::fromExpectations($original, array_map(fn (string $target): GroupArchExpectation => ToBeUsedIn::make($original, $target)->opposite( fn () => $this->throwExpectationFailedException('toBeUsedIn', $target), ), is_string($targets) ? [$targets] : $targets)); } public function toOnlyBeUsedIn(): void { throw InvalidExpectation::fromMethods(['not', 'toOnlyBeUsedIn']); } /** * Asserts that the given expectation dependency is not used. */ public function toBeUsedInNothing(): void { throw InvalidExpectation::fromMethods(['not', 'toBeUsedInNothing']); } /** * Asserts that the given expectation dependency is not an invokable class. */ public function toBeInvokable(): ArchExpectation { /** @var Expectation|string> $original */ $original = $this->original; return Targeted::make( $original, fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false || ! $object->reflectionClass->hasMethod('__invoke'), 'to not be invokable', FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')) ); } /** * Asserts that the given expectation target not to have the given attribute. */ public function toHaveAttribute(string $attribute): ArchExpectation { /** @var Expectation|string> $original */ $original = $this->original; return Targeted::make( $original, fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false || $object->reflectionClass->getAttributes($attribute) === [], "to not have attribute '{$attribute}'", FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')) ); } /** * Handle dynamic method calls into the original expectation. * * @param array $arguments * @return Expectation|Expectation|never */ public function __call(string $name, array $arguments): Expectation { try { if (! is_object($this->original->value) && method_exists(PendingArchExpectation::class, $name)) { throw InvalidExpectation::fromMethods(['not', $name]); } /* @phpstan-ignore-next-line */ $this->original->{$name}(...$arguments); } catch (ExpectationFailedException|AssertionFailedError) { return $this->original; } $this->throwExpectationFailedException($name, $arguments); } /** * Handle dynamic properties gets into the original expectation. * * @return Expectation|Expectation|never */ public function __get(string $name): Expectation { try { if (! is_object($this->original->value) && method_exists(PendingArchExpectation::class, $name)) { throw InvalidExpectation::fromMethods(['not', $name]); } $this->original->{$name}; // @phpstan-ignore-line } catch (ExpectationFailedException) { return $this->original; } $this->throwExpectationFailedException($name); } /** * Creates a new expectation failed exception with a nice readable message. * * @param array|string $arguments */ public function throwExpectationFailedException(string $name, array|string $arguments = []): never { $arguments = is_array($arguments) ? $arguments : [$arguments]; $exporter = Exporter::default(); $toString = fn (mixed $argument): string => $exporter->shortenedExport($argument); throw new ExpectationFailedException(sprintf( 'Expecting %s not %s %s.', $toString($this->original->value), strtolower((string) preg_replace('/(? $toString($argument), $arguments)), )); } /** * Asserts that the given expectation target does not have a constructor method. */ public function toHaveConstructor(): ArchExpectation { return $this->toHaveMethod('__construct'); } /** * Asserts that the given expectation target does not have a destructor method. */ public function toHaveDestructor(): ArchExpectation { return $this->toHaveMethod('__destruct'); } /** * Asserts that the given expectation target is not a backed enum of given type. */ private function toBeBackedEnum(string $backingType): ArchExpectation { /** @var Expectation|string> $original */ $original = $this->original; return Targeted::make( $original, fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false || ! $object->reflectionClass->isEnum() || ! (new \ReflectionEnum($object->name))->isBacked() // @phpstan-ignore-line || (string) (new \ReflectionEnum($object->name))->getBackingType() !== $backingType, // @phpstan-ignore-line 'not to be '.$backingType.' backed enum', FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); } /** * Asserts that the given expectation targets are not string backed enums. */ public function toBeStringBackedEnums(): ArchExpectation { return $this->toBeStringBackedEnum(); } /** * Asserts that the given expectation targets are not int backed enums. */ public function toBeIntBackedEnums(): ArchExpectation { return $this->toBeIntBackedEnum(); } /** * Asserts that the given expectation target is not a string backed enum. */ public function toBeStringBackedEnum(): ArchExpectation { return $this->toBeBackedEnum('string'); } /** * Asserts that the given expectation target is not an int backed enum. */ public function toBeIntBackedEnum(): ArchExpectation { return $this->toBeBackedEnum('int'); } } ================================================ FILE: src/Factories/Attribute.php ================================================ $arguments */ public function __construct(public string $name, public iterable $arguments) { // } } ================================================ FILE: src/Factories/Concerns/HigherOrderable.php ================================================ chains = new HigherOrderMessageCollection; $this->factoryProxies = new HigherOrderMessageCollection; $this->proxies = new HigherOrderMessageCollection; } } ================================================ FILE: src/Factories/Covers/CoversClass.php ================================================ */ public array $attributes = []; /** * The FQN of the Test Case class. * * @var class-string */ public string $class = TestCase::class; /** * The list of class methods. * * @var array */ public array $methods = []; /** * The list of class traits. * * @var array */ public array $traits = [ Concerns\Testable::class, Concerns\Expectable::class, ]; /** * Creates a new Factory instance. */ public function __construct( public string $filename ) { $this->bootHigherOrderable(); } public function make(): void { $methods = $this->methods; if ($methods !== []) { $this->evaluate($this->filename, $methods); } } /** * Creates a Test Case class using a runtime evaluate. * * @param array $methods */ public function evaluate(string $filename, array $methods): void { if ('\\' === DIRECTORY_SEPARATOR) { // In case Windows, strtolower drive name, like in UsesCall. $filename = (string) preg_replace_callback('~^(?P[a-z]+:\\\)~i', static fn (array $match): string => strtolower($match['drive']), $filename); } $filename = str_replace('\\\\', '\\', addslashes((string) realpath($filename))); $rootPath = TestSuite::getInstance()->rootPath; $relativePath = str_replace($rootPath.DIRECTORY_SEPARATOR, '', $filename); $relativePath = ltrim($relativePath, DIRECTORY_SEPARATOR); $basename = basename($relativePath, '.php'); $dotPos = strpos($basename, '.'); if ($dotPos !== false) { $basename = substr($basename, 0, $dotPos); } $relativePath = dirname(ucfirst($relativePath)).DIRECTORY_SEPARATOR.$basename; $relativePath = str_replace(DIRECTORY_SEPARATOR, '\\', $relativePath); // Strip out any %-encoded octets. $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); // Limit to A-Z, a-z, 0-9, '_', '-'. $relativePath = (string) preg_replace('/[^A-Za-z0-9\\\\]/', '', $relativePath); $classFQN = 'P\\'.$relativePath; if (class_exists($classFQN)) { return; } $hasPrintableTestCaseClassFQN = sprintf('\%s', HasPrintableTestCaseName::class); $traitsCode = sprintf('use %s;', implode(', ', array_map( static fn (string $trait): string => sprintf('\%s', $trait), $this->traits)) ); $partsFQN = explode('\\', $classFQN); $className = array_pop($partsFQN); $namespace = implode('\\', $partsFQN); $baseClass = sprintf('\%s', $this->class); if (trim($className) === '') { $className = 'InvalidTestName'.Str::random(); } $this->attributes = [ new Attribute( TestDox::class, [$this->filename], ), ...$this->attributes, ]; $attributesCode = Attributes::code($this->attributes); $methodsCode = implode('', array_map( fn (TestCaseMethodFactory $methodFactory): string => $methodFactory->buildForEvaluation(), $methods )); try { $classCode = <<description === null) { throw new TestDescriptionMissing($method->filename); } if (array_key_exists($method->description, $this->methods)) { throw new TestAlreadyExist($method->filename, $method->description); } if ( $method->closure instanceof \Closure && (new \ReflectionFunction($method->closure))->isStatic() ) { throw new TestClosureMustNotBeStatic($method); } if (! $method->receivesArguments()) { if (! $method->closure instanceof \Closure) { throw ShouldNotHappen::fromMessage('The test closure may not be empty.'); } $arguments = Reflection::getFunctionArguments($method->closure); if ($arguments !== []) { throw new DatasetMissing($method->filename, $method->description, $arguments); } } $this->methods[$method->description] = $method; } /** * Checks if a test case has a method. */ public function hasMethod(string $methodName): bool { foreach ($this->methods as $method) { if ($method->description === null) { throw ShouldNotHappen::fromMessage('The test description may not be empty.'); } if ($methodName === Str::evaluable($method->description)) { return true; } } return false; } /** * Gets a Method by the given name. */ public function getMethod(string $methodName): TestCaseMethodFactory { foreach ($this->methods as $method) { if ($method->description === null) { throw ShouldNotHappen::fromMessage('The test description may not be empty.'); } if ($methodName === Str::evaluable($method->description)) { return $method; } } throw ShouldNotHappen::fromMessage(sprintf('Method %s not found.', $methodName)); } } ================================================ FILE: src/Factories/TestCaseMethodFactory.php ================================================ */ public array $attributes = []; /** * The test's describing, if any. * * @var array */ public array $describing = []; /** * The test's description, if any. */ public ?string $description = null; /** * The test's number of repetitions. */ public int $repetitions = 1; /** * Determines if the test is a "todo". */ public bool $todo = false; /** * The associated issue numbers. * * @var array */ public array $issues = []; /** * The test assignees. * * @var array */ public array $assignees = []; /** * The associated PRs numbers. * * @var array */ public array $prs = []; /** * The test's notes. * * @var array */ public array $notes = []; /** * The test's datasets. * * @var array|string> */ public array $datasets = []; /** * The test's dependencies. * * @var array */ public array $depends = []; /** * The test's groups. * * @var array */ public array $groups = []; /** * @see This property is not actually used in the codebase, it's only here to make Rector happy. */ public bool $__ran = false; /** * Creates a new test case method factory instance. */ public function __construct( public string $filename, public ?Closure $closure, ) { $this->closure ??= function (): void { (Assert::getCount() > 0 || $this->doesNotPerformAssertions()) ?: self::markTestIncomplete(); // @phpstan-ignore-line }; $this->bootHigherOrderable(); } /** * Sets the test's hooks, and runs any proxy to the test case. */ public function setUp(TestCase $concrete): void { $concrete::flush(); // @phpstan-ignore-line if ($this->description === null) { throw ShouldNotHappen::fromMessage('Description can not be empty.'); } $testCase = TestSuite::getInstance()->tests->get($this->filename); assert($testCase instanceof TestCaseFactory); $testCase->factoryProxies->proxy($concrete); $this->factoryProxies->proxy($concrete); } /** * Flushes the test case. */ public function tearDown(TestCase $concrete): void { $concrete::flush(); // @phpstan-ignore-line } /** * Creates the test's closure. */ public function getClosure(): Closure { $closure = $this->closure; $testCase = TestSuite::getInstance()->tests->get($this->filename); assert($testCase instanceof TestCaseFactory); $method = $this; return function (...$arguments) use ($testCase, $method, $closure): mixed { /* @var TestCase $this */ $testCase->proxies->proxy($this); $method->proxies->proxy($this); $testCase->chains->chain($this); $method->chains->chain($this); $this->__ran = true; return \Pest\Support\Closure::bind($closure, $this, self::class)(...$arguments); }; } /** * Determine if the test case will receive argument input from Pest, or not. */ public function receivesArguments(): bool { return $this->datasets !== [] || $this->depends !== [] || $this->repetitions > 1; } /** * Creates a PHPUnit method as a string ready for evaluation. */ public function buildForEvaluation(): string { if ($this->description === null) { throw ShouldNotHappen::fromMessage('The test description may not be empty.'); } $methodName = Str::evaluable($this->description); $datasetsCode = ''; $this->attributes = [ new Attribute( Test::class, [], ), new Attribute( TestDox::class, [str_replace('*/', '{@*}', $this->description)], ), ...$this->attributes, ]; foreach ($this->depends as $depend) { $depend = Str::evaluable($this->describing === [] ? $depend : Str::describe($this->describing, $depend)); $this->attributes[] = new Attribute( Depends::class, [$depend], ); } if ($this->datasets !== [] || $this->repetitions > 1) { $dataProviderName = $methodName.'_dataset'; $this->attributes[] = new Attribute( DataProvider::class, [$dataProviderName], ); $datasetsCode = $this->buildDatasetForEvaluation($methodName, $dataProviderName); } $attributesCode = Attributes::code($this->attributes); return <<__runTest( \$this->__test, ...\$arguments, ); } $datasetsCode PHP; } /** * Creates a PHPUnit Data Provider as a string ready for evaluation. */ private function buildDatasetForEvaluation(string $methodName, string $dataProviderName): string { $datasets = $this->datasets; if ($this->repetitions > 1) { $datasets = [range(1, $this->repetitions), ...$datasets]; } DatasetsRepository::with($this->filename, $methodName, $datasets); return << */ function expect(mixed $value = null): Expectation { return new Expectation($value); } } if (! function_exists('beforeAll')) { /** * Runs the given closure before all tests in the current file. */ function beforeAll(Closure $closure): void { if (DescribeCall::describing() !== []) { $filename = Backtrace::file(); throw new BeforeAllWithinDescribe($filename); } TestSuite::getInstance()->beforeAll->set($closure); } } if (! function_exists('beforeEach')) { /** * Runs the given closure before each test in the current file. * * @param-closure-this TestCase $closure * * @return HigherOrderTapProxy|Expectable|TestCall|TestCase|mixed */ function beforeEach(?Closure $closure = null): BeforeEachCall { $filename = Backtrace::file(); return new BeforeEachCall(TestSuite::getInstance(), $filename, $closure); } } if (! function_exists('dataset')) { /** * Registers the given dataset. * * @param Closure|iterable $dataset */ function dataset(string $name, Closure|iterable $dataset): void { $scope = DatasetInfo::scope(Backtrace::datasetsFile()); DatasetsRepository::set($name, $dataset, $scope); } } 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. * * @return HigherOrderTapProxy|Expectable|TestCall|TestCase|mixed */ function describe(string $description, Closure $tests): DescribeCall { $filename = Backtrace::testFile(); return new DescribeCall(TestSuite::getInstance(), $filename, new Description($description), $tests); } } if (! function_exists('uses')) { /** * The uses function binds the given * arguments to test closures. * * @param class-string ...$classAndTraits */ function uses(string ...$classAndTraits): UsesCall { $filename = Backtrace::file(); return new UsesCall($filename, array_values($classAndTraits)); } } if (! function_exists('pest')) { /** * Creates a new Pest configuration instance. */ function pest(): Configuration { return new Configuration(Backtrace::file()); } } 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 TestCase $closure * * @return Expectable|TestCall|TestCase|mixed */ function test(?string $description = null, ?Closure $closure = null): HigherOrderTapProxy|TestCall { if ($description === null && TestSuite::getInstance()->test instanceof TestCase) { return new HigherOrderTapProxy(TestSuite::getInstance()->test); } $filename = Backtrace::testFile(); return new TestCall(TestSuite::getInstance(), $filename, $description, $closure); } } 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 TestCase $closure * * @return Expectable|TestCall|TestCase|mixed */ function it(string $description, ?Closure $closure = null): TestCall { $description = sprintf('it %s', $description); /** @var TestCall $test */ $test = test($description, $closure); return $test; } } if (! function_exists('todo')) { /** * Creates a new test that is marked as "todo". * * @return Expectable|TestCall|TestCase|mixed */ function todo(string $description): TestCall { $test = test($description); assert($test instanceof TestCall); return $test->todo(); } } if (! function_exists('afterEach')) { /** * Runs the given closure after each test in the current file. * * @param-closure-this TestCase $closure * * @return Expectable|HigherOrderTapProxy|TestCall|mixed */ function afterEach(?Closure $closure = null): AfterEachCall { $filename = Backtrace::file(); return new AfterEachCall(TestSuite::getInstance(), $filename, $closure); } } if (! function_exists('afterAll')) { /** * Runs the given closure after all tests in the current file. */ function afterAll(Closure $closure): void { if (DescribeCall::describing() !== []) { $filename = Backtrace::file(); throw new AfterAllWithinDescribe($filename); } TestSuite::getInstance()->afterAll->set($closure); } } if (! function_exists('covers')) { /** * Specifies which classes, or functions, a test case covers. * * @param array|string $classesOrFunctions */ function covers(array|string ...$classesOrFunctions): void { $filename = Backtrace::file(); $beforeEachCall = (new BeforeEachCall(TestSuite::getInstance(), $filename)); $beforeEachCall->covers(...$classesOrFunctions); $beforeEachCall->group('__pest_mutate_only'); /** @var MutationTestRunner $runner */ $runner = Container::getInstance()->get(MutationTestRunner::class); /** @var ConfigurationRepository $configurationRepository */ $configurationRepository = Container::getInstance()->get(ConfigurationRepository::class); $everything = $configurationRepository->cliConfiguration->toArray()['everything'] ?? false; $classes = $configurationRepository->cliConfiguration->toArray()['classes'] ?? false; $paths = $configurationRepository->cliConfiguration->toArray()['paths'] ?? false; if ($runner->isEnabled() && ! $everything && ! is_array($classes) && ! is_array($paths)) { $beforeEachCall->only('__pest_mutate_only'); } } } if (! function_exists('mutates')) { /** * Specifies which classes, enums, or traits a test case mutates. * * @param array|string $targets */ function mutates(array|string ...$targets): void { $filename = Backtrace::file(); $beforeEachCall = (new BeforeEachCall(TestSuite::getInstance(), $filename)); $beforeEachCall->group('__pest_mutate_only'); /** @var MutationTestRunner $runner */ $runner = Container::getInstance()->get(MutationTestRunner::class); /** @var ConfigurationRepository $configurationRepository */ $configurationRepository = Container::getInstance()->get(ConfigurationRepository::class); $everything = $configurationRepository->cliConfiguration->toArray()['everything'] ?? false; $classes = $configurationRepository->cliConfiguration->toArray()['classes'] ?? false; $paths = $configurationRepository->cliConfiguration->toArray()['paths'] ?? false; if ($runner->isEnabled() && ! $everything && ! is_array($classes) && ! is_array($paths)) { $beforeEachCall->only('__pest_mutate_only'); } /** @var ConfigurationRepository $configurationRepository */ $configurationRepository = Container::getInstance()->get(ConfigurationRepository::class); $paths = $configurationRepository->cliConfiguration->toArray()['paths'] ?? false; if (! is_array($paths)) { $configurationRepository->globalConfiguration('default')->class(...$targets); // @phpstan-ignore-line } } } if (! function_exists('fixture')) { /** * Returns the absolute path to a fixture file. */ function fixture(string $file): string { $file = implode(DIRECTORY_SEPARATOR, [ TestSuite::getInstance()->rootPath, TestSuite::getInstance()->testPath, 'Fixtures', str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $file), ]); $fileRealPath = realpath($file); if ($fileRealPath === false) { throw new InvalidArgumentException( 'The fixture file ['.$file.'] does not exist.', ); } return $fileRealPath; } } if (! function_exists('visit')) { /** * Browse to the given URL. * * @template TUrl of array|string * * @param TUrl $url * @param array $options * @return (TUrl is array ? ArrayablePendingAwaitablePage : PendingAwaitablePage) */ function visit(array|string $url, array $options = []): ArrayablePendingAwaitablePage|PendingAwaitablePage { if (! class_exists(Pest\Browser\Configuration::class)) { PluginBrowser::install(); exit(0); } // @phpstan-ignore-next-line return test()->visit($url, $options); } } ================================================ FILE: src/Installers/PluginBrowser.php ================================================ */ private const array BOOTSTRAPPERS = [ Bootstrappers\BootOverrides::class, Bootstrappers\BootSubscribers::class, Bootstrappers\BootFiles::class, Bootstrappers\BootView::class, Bootstrappers\BootKernelDump::class, Bootstrappers\BootExcludeList::class, ]; /** * Creates a new Kernel instance. */ public function __construct( private Application $application, private OutputInterface $output, ) { // } /** * Boots the Kernel. */ public static function boot(TestSuite $testSuite, InputInterface $input, OutputInterface $output): self { $container = Container::getInstance(); $container ->add(TestSuite::class, $testSuite) ->add(InputInterface::class, $input) ->add(OutputInterface::class, $output) ->add(Container::class, $container); $kernel = new self( new Application, $output, ); register_shutdown_function($kernel->shutdown(...)); foreach (self::BOOTSTRAPPERS as $bootstrapper) { $bootstrapper = Container::getInstance()->get($bootstrapper); assert($bootstrapper instanceof Bootstrapper); $bootstrapper->boot(); } CallsBoot::execute(); Container::getInstance()->add(self::class, $kernel); return $kernel; } /** * Runs the application, and returns the exit code. * * @param array $originalArguments * @param array $arguments */ public function handle(array $originalArguments, array $arguments): int { CallsHandleOriginalArguments::execute($originalArguments); $arguments = CallsHandleArguments::execute($arguments); try { $this->application->run($arguments); } catch (NoDirtyTestsFound) { $this->output->writeln([ '', ' INFO No tests found.', '', ]); } $configuration = Registry::get(); $result = Facade::result(); return CallsAddsOutput::execute( Result::exitCode($configuration, $result), ); } /** * Terminate the Kernel. */ public function terminate(): void { $preBufferOutput = Container::getInstance()->get(KernelDump::class); assert($preBufferOutput instanceof KernelDump); $preBufferOutput->terminate(); CallsTerminable::execute(); } /** * Shutdowns unexpectedly the Kernel. */ public function shutdown(): void { $this->terminate(); if (is_array($error = error_get_last())) { if (! in_array($error['type'], [E_ERROR, E_CORE_ERROR], true)) { return; } $message = $error['message']; $file = $error['file']; $line = $error['line']; try { $writer = new Writer(null, $this->output); $throwable = new FatalException($message); Reflection::setPropertyValue($throwable, 'line', $line); Reflection::setPropertyValue($throwable, 'file', $file); $inspector = new Inspector($throwable); $writer->write($inspector); } catch (Throwable) { // @phpstan-ignore-line View::render('components.badge', [ 'type' => 'ERROR', 'content' => sprintf('%s in %s:%d', $message, $file, $line), ]); } exit(1); } } } ================================================ FILE: src/KernelDump.php ================================================ buffer .= $message; return ''; }); } /** * Disable the output buffering. */ public function disable(): void { @ob_clean(); if ($this->buffer !== '') { $this->flush(); } } /** * Terminate the output buffering. */ public function terminate(): void { $this->disable(); } /** * Flushes the buffer. */ private function flush(): void { View::renderUsing($this->output); if ($this->isOpeningHeadline($this->buffer)) { $this->buffer = implode(PHP_EOL, array_slice(explode(PHP_EOL, $this->buffer), 2)); } $type = 'INFO'; if ($this->isInternalError($this->buffer)) { $type = 'ERROR'; $this->buffer = str_replace( sprintf('An error occurred inside PHPUnit.%s%sMessage: ', PHP_EOL, PHP_EOL), '', $this->buffer, ); } $this->buffer = trim($this->buffer); $this->buffer = rtrim($this->buffer, '.').'.'; $lines = explode(PHP_EOL, $this->buffer); $lines = array_reverse($lines); $firstLine = array_pop($lines); $lines = array_reverse($lines); View::render('components.badge', [ 'type' => $type, 'content' => $firstLine, ]); $this->output->writeln($lines); $this->buffer = ''; } /** * Checks if the given output contains an opening headline. */ private function isOpeningHeadline(string $output): bool { return str_contains($output, 'by Sebastian Bergmann and contributors.'); } /** * Checks if the given output contains an opening headline. */ private function isInternalError(string $output): bool { return str_contains($output, 'An error occurred inside PHPUnit.') || str_contains($output, 'Fatal error'); } } ================================================ FILE: src/Logging/Converter.php ================================================ stateGenerator = new StateGenerator; } /** * Gets the test case method name. */ public function getTestCaseMethodName(Test $test): string { if (! $test instanceof TestMethod) { throw ShouldNotHappen::fromMessage('Not an instance of TestMethod'); } return $test->testDox()->prettifiedMethodName(); } /** * Gets the test case location. */ public function getTestCaseLocation(Test $test): string { if (! $test instanceof TestMethod) { throw ShouldNotHappen::fromMessage('Not an instance of TestMethod'); } $path = $test->testDox()->prettifiedClassName(); $relativePath = $this->toRelativePath($path); // TODO: Get the description without the dataset. $description = $test->testDox()->prettifiedMethodName(); return "$relativePath::$description"; } /** * Gets the exception message. */ public function getExceptionMessage(Throwable $throwable): string { if (is_a($throwable->className(), FrameworkException::class, true)) { return $throwable->message(); } $buffer = $throwable->className(); $throwableMessage = $throwable->message(); if ($throwableMessage !== '') { $buffer .= ": $throwableMessage"; } return $buffer; } /** * Gets the exception details. */ public function getExceptionDetails(Throwable $throwable): string { $buffer = $this->getStackTrace($throwable); while ($throwable->hasPrevious()) { $throwable = $throwable->previous(); $buffer .= sprintf( "\nCaused by\n%s\n%s", $throwable->description(), $this->getStackTrace($throwable) ); } return $buffer; } /** * Gets the stack trace. */ public function getStackTrace(Throwable $throwable): string { $stackTrace = $throwable->stackTrace(); // Split stacktrace per frame. $frames = explode("\n", $stackTrace); // Remove empty lines $frames = array_filter($frames); // clean the paths of each frame. $frames = array_map( $this->toRelativePath(...), $frames ); // Format stacktrace as `at ` $frames = array_map( fn (string $frame): string => "at $frame", $frames ); return implode("\n", $frames); } /** * Gets the test suite name. */ public function getTestSuiteName(TestSuite $testSuite): string { if ($testSuite instanceof TestSuiteForTestMethodWithDataProvider) { $firstTest = $this->getFirstTest($testSuite); if ($firstTest instanceof TestMethod) { return $this->getTestMethodNameWithoutDatasetSuffix($firstTest); } } $name = $testSuite->name(); if (! str_starts_with($name, self::PREFIX)) { return $name; } return Str::after($name, self::PREFIX); } /** * Gets the trimmed test class name. */ public function getTrimmedTestClassName(TestMethod $test): string { return Str::after($test->className(), self::PREFIX); } /** * Gets the test suite location. */ public function getTestSuiteLocation(TestSuite $testSuite): ?string { $firstTest = $this->getFirstTest($testSuite); if (! $firstTest instanceof TestMethod) { return null; } $path = $firstTest->testDox()->prettifiedClassName(); $classRelativePath = $this->toRelativePath($path); if ($testSuite instanceof TestSuiteForTestMethodWithDataProvider) { $methodName = $this->getTestMethodNameWithoutDatasetSuffix($firstTest); return "$classRelativePath::$methodName"; } return $classRelativePath; } /** * Gets the prettified test method name without dataset-related suffix. */ private function getTestMethodNameWithoutDatasetSuffix(TestMethod $testMethod): string { return Str::beforeLast($testMethod->testDox()->prettifiedMethodName(), ' with data set '); } /** * Gets the first test from the test suite. */ private function getFirstTest(TestSuite $testSuite): ?TestMethod { $tests = $testSuite->tests()->asArray(); // TODO: figure out how to get the file path without a test being there. if ($tests === []) { return null; } $firstTest = $tests[0]; if (! $firstTest instanceof TestMethod) { throw ShouldNotHappen::fromMessage('Not an instance of TestMethod'); } return $firstTest; } /** * Gets the test suite size. */ public function getTestSuiteSize(TestSuite $testSuite): int { return $testSuite->count(); } /** * Transforms the given path in relative path. */ private function toRelativePath(string $path): string { // Remove cwd from the path. return str_replace("$this->rootPath".DIRECTORY_SEPARATOR, '', $path); } /** * Get the test result. */ public function getStateFromResult(PhpUnitTestResult $result): State { $events = [ ...$result->testErroredEvents(), ...$result->testFailedEvents(), ...$result->testSkippedEvents(), ...array_merge(...array_values($result->testConsideredRiskyEvents())), ...$result->testMarkedIncompleteEvents(), ]; $numberOfNotPassedTests = count( array_unique( array_map( function (AfterLastTestMethodErrored|BeforeFirstTestMethodErrored|Errored|Failed|Skipped|ConsideredRisky|MarkedIncomplete $event): string { if ($event instanceof BeforeFirstTestMethodErrored || $event instanceof AfterLastTestMethodErrored) { return $event->testClassName(); } return $this->getTestCaseLocation($event->test()); }, $events ) ) ); $numberOfPassedTests = $result->numberOfTestsRun() - $numberOfNotPassedTests; return $this->stateGenerator->fromPhpUnitTestResult($numberOfPassedTests, $result); } } ================================================ FILE: src/Logging/TeamCity/ServiceMessage.php ================================================ $parameters */ public function __construct( private readonly string $type, private readonly array $parameters, ) {} public function toString(): string { $paramsToString = ''; foreach ([...$this->parameters, 'flowId' => self::$flowId] as $key => $value) { $value = $this->escapeServiceMessage((string) $value); $paramsToString .= " $key='$value'"; } return "##teamcity[$this->type$paramsToString]"; } public static function testSuiteStarted(string $name, ?string $location): self { return new self('testSuiteStarted', [ 'name' => $name, 'locationHint' => $location === null ? null : "pest_qn://$location", ]); } public static function testSuiteCount(int $count): self { return new self('testCount', [ 'count' => $count, ]); } public static function testSuiteFinished(string $name): self { return new self('testSuiteFinished', [ 'name' => $name, ]); } public static function testStarted(string $name, string $location): self { return new self('testStarted', [ 'name' => $name, 'locationHint' => "pest_qn://$location", ]); } /** * @param int $duration in milliseconds */ public static function testFinished(string $name, int $duration): self { return new self('testFinished', [ 'name' => $name, 'duration' => $duration, ]); } public static function testStdOut(string $name, string $data): self { if (! str_ends_with($data, "\n")) { $data .= "\n"; } return new self('testStdOut', [ 'name' => $name, 'out' => $data, ]); } public static function testFailed(string $name, string $message, string $details): self { return new self('testFailed', [ 'name' => $name, 'message' => $message, 'details' => $details, ]); } public static function testStdErr(string $name, string $data): self { if (! str_ends_with($data, "\n")) { $data .= "\n"; } return new self('testStdErr', [ 'name' => $name, 'out' => $data, ]); } public static function testIgnored(string $name, string $message, ?string $details = null): self { return new self('testIgnored', [ 'name' => $name, 'message' => $message, 'details' => $details, ]); } public static function comparisonFailure(string $name, string $message, string $details, string $actual, string $expected): self { return new self('testFailed', [ 'name' => $name, 'message' => $message, 'details' => $details, 'type' => 'comparisonFailure', 'actual' => $actual, 'expected' => $expected, ]); } private function escapeServiceMessage(string $text): string { return str_replace( ['|', "'", "\n", "\r", ']', '['], ['||', "|'", '|n', '|r', '|]', '|['], $text ); } public static function setFlowId(int $flowId): void { self::$flowId = $flowId; } } ================================================ FILE: src/Logging/TeamCity/Subscriber/Subscriber.php ================================================ logger; } } ================================================ FILE: src/Logging/TeamCity/Subscriber/TestConsideredRiskySubscriber.php ================================================ logger()->testConsideredRisky($event); } } ================================================ FILE: src/Logging/TeamCity/Subscriber/TestErroredSubscriber.php ================================================ logger()->testErrored($event); } } ================================================ FILE: src/Logging/TeamCity/Subscriber/TestExecutionFinishedSubscriber.php ================================================ logger()->testExecutionFinished($event); } } ================================================ FILE: src/Logging/TeamCity/Subscriber/TestFailedSubscriber.php ================================================ logger()->testFailed($event); } } ================================================ FILE: src/Logging/TeamCity/Subscriber/TestFinishedSubscriber.php ================================================ logger()->testFinished($event); } } ================================================ FILE: src/Logging/TeamCity/Subscriber/TestPreparedSubscriber.php ================================================ logger()->testPrepared($event); } } ================================================ FILE: src/Logging/TeamCity/Subscriber/TestSkippedSubscriber.php ================================================ logger()->testSkipped($event); } } ================================================ FILE: src/Logging/TeamCity/Subscriber/TestSuiteFinishedSubscriber.php ================================================ logger()->testSuiteFinished($event); } } ================================================ FILE: src/Logging/TeamCity/Subscriber/TestSuiteStartedSubscriber.php ================================================ logger()->testSuiteStarted($event); } } ================================================ FILE: src/Logging/TeamCity/TeamCityLogger.php ================================================ */ private array $testEvents = []; /** * @throws EventFacadeIsSealedException * @throws UnknownSubscriberTypeException */ public function __construct( private readonly OutputInterface $output, private readonly Converter $converter, private readonly ?int $flowId, private readonly bool $withoutDuration, ) { $this->registerSubscribers(); $this->setFlowId(); } public function testSuiteStarted(TestSuiteStarted $event): void { $message = ServiceMessage::testSuiteStarted( $this->converter->getTestSuiteName($event->testSuite()), $this->converter->getTestSuiteLocation($event->testSuite()) ); $this->output($message); if (! $this->isSummaryTestCountPrinted) { $this->isSummaryTestCountPrinted = true; $message = ServiceMessage::testSuiteCount( $this->converter->getTestSuiteSize($event->testSuite()) ); $this->output($message); } } public function testSuiteFinished(TestSuiteFinished $event): void { $message = ServiceMessage::testSuiteFinished( $this->converter->getTestSuiteName($event->testSuite()), ); $this->output($message); } public function testPrepared(Prepared $event): void { $message = ServiceMessage::testStarted( $this->converter->getTestCaseMethodName($event->test()), $this->converter->getTestCaseLocation($event->test()), ); $this->output($message); $this->time = $event->telemetryInfo()->time(); } public function testMarkedIncomplete(): never { throw ShouldNotHappen::fromMessage('testMarkedIncomplete not implemented.'); } public function testSkipped(Skipped $event): void { $this->whenFirstEventForTest($event->test(), function () use ($event): void { $message = ServiceMessage::testIgnored( $this->converter->getTestCaseMethodName($event->test()), 'This test was ignored.' ); $this->output($message); }); } /** * This will trigger in the following scenarios * - When an exception is thrown */ public function testErrored(Errored $event): void { $this->whenFirstEventForTest($event->test(), function () use ($event): void { $testName = $this->converter->getTestCaseMethodName($event->test()); $message = $this->converter->getExceptionMessage($event->throwable()); $details = $this->converter->getExceptionDetails($event->throwable()); $message = ServiceMessage::testFailed( $testName, $message, $details, ); $this->output($message); }); } /** * This will trigger in the following scenarios * - When an assertion fails */ public function testFailed(Failed $event): void { $this->whenFirstEventForTest($event->test(), function () use ($event): void { $testName = $this->converter->getTestCaseMethodName($event->test()); $message = $this->converter->getExceptionMessage($event->throwable()); $details = $this->converter->getExceptionDetails($event->throwable()); if ($event->hasComparisonFailure()) { $comparison = $event->comparisonFailure(); $message = ServiceMessage::comparisonFailure( $testName, $message, $details, $comparison->actual(), $comparison->expected() ); } else { $message = ServiceMessage::testFailed( $testName, $message, $details, ); } $this->output($message); }); } /** * This will trigger in the following scenarios * - When no assertions in a test */ public function testConsideredRisky(ConsideredRisky $event): void { $this->whenFirstEventForTest($event->test(), function () use ($event): void { $message = ServiceMessage::testIgnored( $this->converter->getTestCaseMethodName($event->test()), $event->message() ); $this->output($message); }); } public function testFinished(Finished $event): void { if (! $this->time instanceof HRTime) { throw ShouldNotHappen::fromMessage('Start time has not been set.'); } $testName = $this->converter->getTestCaseMethodName($event->test()); $duration = $event->telemetryInfo()->time()->duration($this->time)->asFloat(); if ($this->withoutDuration) { $duration = 100; } $message = ServiceMessage::testFinished( $testName, (int) ($duration * 1000) ); $this->output($message); } public function testExecutionFinished(ExecutionFinished $event): void { $result = TestResultFacade::result(); $state = $this->converter->getStateFromResult($result); assert($this->output instanceof ConsoleOutput); $style = new Style($this->output); $telemetry = $event->telemetryInfo(); if ($this->withoutDuration) { $reflector = new ReflectionClass($telemetry); $property = $reflector->getProperty('current'); $snapshot = $property->getValue($telemetry); assert($snapshot instanceof Snapshot); $telemetry = new Info( $snapshot, Duration::fromSecondsAndNanoseconds(1, 0), $telemetry->memoryUsageSinceStart(), $telemetry->durationSincePrevious(), $telemetry->memoryUsageSincePrevious(), ); } $style->writeRecap($state, $telemetry, $result); } public function output(ServiceMessage $message): void { $this->output->writeln("{$message->toString()}"); } /** * @throws EventFacadeIsSealedException * @throws UnknownSubscriberTypeException */ private function registerSubscribers(): void { $subscribers = [ new TestSuiteStartedSubscriber($this), new TestSuiteFinishedSubscriber($this), new TestPreparedSubscriber($this), new TestFinishedSubscriber($this), new TestErroredSubscriber($this), new TestFailedSubscriber($this), new TestSkippedSubscriber($this), new TestConsideredRiskySubscriber($this), new TestExecutionFinishedSubscriber($this), ]; Facade::instance()->registerSubscribers(...$subscribers); } private function setFlowId(): void { if ($this->flowId === null) { return; } ServiceMessage::setFlowId($this->flowId); } private function whenFirstEventForTest(Test $test, callable $callback): void { $testIdentifier = $this->converter->getTestCaseLocation($test); if (! isset($this->testEvents[$testIdentifier])) { $this->testEvents[$testIdentifier] = true; $callback(); } } } ================================================ FILE: src/Matchers/Any.php ================================================ */ final class Expectation { /** * The exporter instance, if any. */ private ?Exporter $exporter = null; /** * Creates a new expectation. * * @param TValue $value */ public function __construct( public mixed $value ) { // .. } /** * Asserts that two variables have the same type and * value. Used on objects, it asserts that two * variables reference the same object. * * @return self */ public function toBe(mixed $expected, string $message = ''): self { Assert::assertSame($expected, $this->value, $message); return $this; } /** * Asserts that the value is empty. * * @return self */ public function toBeEmpty(string $message = ''): self { Assert::assertEmpty($this->value, $message); return $this; } /** * Asserts that the value is true. * * @return self */ public function toBeTrue(string $message = ''): self { Assert::assertTrue($this->value, $message); return $this; } /** * Asserts that the value is truthy. * * @return self */ public function toBeTruthy(string $message = ''): self { Assert::assertTrue((bool) $this->value, $message); return $this; } /** * Asserts that the value is false. * * @return self */ public function toBeFalse(string $message = ''): self { Assert::assertFalse($this->value, $message); return $this; } /** * Asserts that the value is falsy. * * @return self */ public function toBeFalsy(string $message = ''): self { Assert::assertFalse((bool) $this->value, $message); return $this; } /** * Asserts that the value is greater than $expected. * * @return self */ public function toBeGreaterThan(int|float|string|DateTimeInterface $expected, string $message = ''): self { Assert::assertGreaterThan($expected, $this->value, $message); return $this; } /** * Asserts that the value is greater than or equal to $expected. * * @return self */ public function toBeGreaterThanOrEqual(int|float|string|DateTimeInterface $expected, string $message = ''): self { Assert::assertGreaterThanOrEqual($expected, $this->value, $message); return $this; } /** * Asserts that the value is less than or equal to $expected. * * @return self */ public function toBeLessThan(int|float|string|DateTimeInterface $expected, string $message = ''): self { Assert::assertLessThan($expected, $this->value, $message); return $this; } /** * Asserts that the value is less than $expected. * * @return self */ public function toBeLessThanOrEqual(int|float|string|DateTimeInterface $expected, string $message = ''): self { Assert::assertLessThanOrEqual($expected, $this->value, $message); return $this; } /** * Asserts that $needle is an element of the value. * * @return self */ public function toContain(mixed ...$needles): self { foreach ($needles as $needle) { if (is_string($this->value)) { Assert::assertStringContainsString((string) $needle, $this->value); } else { if (! is_iterable($this->value)) { InvalidExpectationValue::expected('iterable'); } Assert::assertContains($needle, $this->value); } } return $this; } /** * Asserts that $needle equal an element of the value. * * @return self */ public function toContainEqual(mixed ...$needles): self { if (! is_iterable($this->value)) { InvalidExpectationValue::expected('iterable'); } foreach ($needles as $needle) { Assert::assertContainsEquals($needle, $this->value); } return $this; } /** * Asserts that the value starts with $expected. * * @param non-empty-string $expected * @return self */ public function toStartWith(string $expected, string $message = ''): self { if (! is_string($this->value)) { InvalidExpectationValue::expected('string'); } Assert::assertStringStartsWith($expected, $this->value, $message); return $this; } /** * Asserts that the value ends with $expected. * * @param non-empty-string $expected * @return self */ public function toEndWith(string $expected, string $message = ''): self { if (! is_string($this->value)) { InvalidExpectationValue::expected('string'); } Assert::assertStringEndsWith($expected, $this->value, $message); return $this; } /** * Asserts that $number matches value's Length. * * @return self */ public function toHaveLength(int $number, string $message = ''): self { if (is_string($this->value)) { Assert::assertEquals($number, mb_strlen($this->value), $message); return $this; } if (is_iterable($this->value)) { return $this->toHaveCount($number, $message); } if (is_object($this->value)) { $array = method_exists($this->value, 'toArray') ? $this->value->toArray() : (array) $this->value; Assert::assertCount($number, $array, $message); return $this; } throw new BadMethodCallException('Expectation value length is not countable.'); } /** * Asserts that $count matches the number of elements of the value. * * @return self */ public function toHaveCount(int $count, string $message = ''): self { if (! is_countable($this->value) && ! is_iterable($this->value)) { InvalidExpectationValue::expected('countable|iterable'); } Assert::assertCount($count, $this->value, $message); return $this; } /** * Asserts that the size of the value and $expected are the same. * * @param Countable|iterable $expected * @return self */ public function toHaveSameSize(Countable|iterable $expected, string $message = ''): self { if (! is_countable($this->value) && ! is_iterable($this->value)) { InvalidExpectationValue::expected('countable|iterable'); } Assert::assertSameSize($expected, $this->value, $message); return $this; } /** * Asserts that the value contains the property $name. * * @return self */ public function toHaveProperty(string $name, mixed $value = new Any, string $message = ''): self { $this->toBeObject(); // @phpstan-ignore-next-line Assert::assertTrue(property_exists($this->value, $name), $message); if (! $value instanceof Any) { /* @phpstan-ignore-next-line */ Assert::assertEquals($value, $this->value->{$name}, $message); } return $this; } /** * Asserts that the value contains the provided properties $names. * * @param iterable|iterable $names * @return self */ public function toHaveProperties(iterable $names, string $message = ''): self { foreach ($names as $name => $value) { is_int($name) ? $this->toHaveProperty($value, message: $message) : $this->toHaveProperty($name, $value, $message); // @phpstan-ignore-line } return $this; } /** * Asserts that two variables have the same value. * * @return self */ public function toEqual(mixed $expected, string $message = ''): self { Assert::assertEquals($expected, $this->value, $message); return $this; } /** * Asserts that two variables have the same value. * The contents of $expected and the $this->value are * canonicalized before they are compared. For instance, when the two * variables $expected and $this->value are arrays, then these arrays * are sorted before they are compared. When $expected and $this->value * are objects, each object is converted to an array containing all * private, protected and public attributes. * * @return self */ public function toEqualCanonicalizing(mixed $expected, string $message = ''): self { Assert::assertEqualsCanonicalizing($expected, $this->value, $message); return $this; } /** * Asserts that the absolute difference between the value and $expected * is lower than $delta. * * @return self */ public function toEqualWithDelta(mixed $expected, float $delta, string $message = ''): self { Assert::assertEqualsWithDelta($expected, $this->value, $delta, $message); return $this; } /** * Asserts that the value is one of the given values. * * @param iterable $values * @return self */ public function toBeIn(iterable $values, string $message = ''): self { Assert::assertContains($this->value, $values, $message); return $this; } /** * Asserts that the value is infinite. * * @return self */ public function toBeInfinite(string $message = ''): self { Assert::assertInfinite($this->value, $message); return $this; } /** * Asserts that the value is an instance of $class. * * @param class-string $class * @return self */ public function toBeInstanceOf(string $class, string $message = ''): self { Assert::assertInstanceOf($class, $this->value, $message); return $this; } /** * Asserts that the value is an array. * * @return self */ public function toBeArray(string $message = ''): self { Assert::assertIsArray($this->value, $message); return $this; } /** * Asserts that the value is a list. * * @return self */ public function toBeList(string $message = ''): self { Assert::assertIsList($this->value, $message); return $this; } /** * Asserts that the value is of type bool. * * @return self */ public function toBeBool(string $message = ''): self { Assert::assertIsBool($this->value, $message); return $this; } /** * Asserts that the value is of type callable. * * @return self */ public function toBeCallable(string $message = ''): self { Assert::assertIsCallable($this->value, $message); return $this; } /** * Asserts that the value is of type float. * * @return self */ public function toBeFloat(string $message = ''): self { Assert::assertIsFloat($this->value, $message); return $this; } /** * Asserts that the value is of type int. * * @return self */ public function toBeInt(string $message = ''): self { Assert::assertIsInt($this->value, $message); return $this; } /** * Asserts that the value is of type iterable. * * @return self */ public function toBeIterable(string $message = ''): self { Assert::assertIsIterable($this->value, $message); return $this; } /** * Asserts that the value is of type numeric. * * @return self */ public function toBeNumeric(string $message = ''): self { Assert::assertIsNumeric($this->value, $message); return $this; } /** * Asserts that the value contains only digits. * * @return self */ public function toBeDigits(string $message = ''): self { Assert::assertTrue(ctype_digit((string) $this->value), $message); return $this; } /** * Asserts that the value is of type object. * * @return self */ public function toBeObject(string $message = ''): self { Assert::assertIsObject($this->value, $message); return $this; } /** * Asserts that the value is of type resource. * * @return self */ public function toBeResource(string $message = ''): self { Assert::assertIsResource($this->value, $message); return $this; } /** * Asserts that the value is of type scalar. * * @return self */ public function toBeScalar(string $message = ''): self { Assert::assertIsScalar($this->value, $message); return $this; } /** * Asserts that the value is of type string. * * @return self */ public function toBeString(string $message = ''): self { Assert::assertIsString($this->value, $message); return $this; } /** * Asserts that the value is a JSON string. * * @return self */ public function toBeJson(string $message = ''): self { Assert::assertIsString($this->value, $message); Assert::assertJson($this->value, $message); return $this; } /** * Asserts that the value is NAN. * * @return self */ public function toBeNan(string $message = ''): self { Assert::assertNan($this->value, $message); return $this; } /** * Asserts that the value is null. * * @return self */ public function toBeNull(string $message = ''): self { Assert::assertNull($this->value, $message); return $this; } /** * Asserts that the value array has the provided $key. * * @return self */ public function toHaveKey(string|int $key, mixed $value = new Any, string $message = ''): self { if (is_object($this->value) && method_exists($this->value, 'toArray')) { $array = $this->value->toArray(); } else { $array = (array) $this->value; } try { Assert::assertTrue(Arr::has($array, $key)); /* @phpstan-ignore-next-line */ } catch (ExpectationFailedException $exception) { if ($message === '') { $message = "Failed asserting that an array has the key '$key'"; } throw new ExpectationFailedException($message, $exception->getComparisonFailure()); } if (! $value instanceof Any) { Assert::assertEquals($value, Arr::get($array, $key), $message); } return $this; } /** * Asserts that the value array has the provided $keys. * * @param array> $keys * @return self */ public function toHaveKeys(array $keys, string $message = ''): self { foreach ($keys as $k => $key) { if (is_array($key)) { $this->toHaveKeys(array_keys(Arr::dot($key, $k.'.')), $message); } else { $this->toHaveKey($key, message: $message); } } return $this; } /** * Asserts that the value is a directory. * * @return self */ public function toBeDirectory(string $message = ''): self { if (! is_string($this->value)) { InvalidExpectationValue::expected('string'); } Assert::assertDirectoryExists($this->value, $message); return $this; } /** * Asserts that the value is a directory and is readable. * * @return self */ public function toBeReadableDirectory(string $message = ''): self { if (! is_string($this->value)) { InvalidExpectationValue::expected('string'); } Assert::assertDirectoryIsReadable($this->value, $message); return $this; } /** * Asserts that the value is a directory and is writable. * * @return self */ public function toBeWritableDirectory(string $message = ''): self { if (! is_string($this->value)) { InvalidExpectationValue::expected('string'); } Assert::assertDirectoryIsWritable($this->value, $message); return $this; } /** * Asserts that the value is a file. * * @return self */ public function toBeFile(string $message = ''): self { if (! is_string($this->value)) { InvalidExpectationValue::expected('string'); } Assert::assertFileExists($this->value, $message); return $this; } /** * Asserts that the value is a file and is readable. * * @return self */ public function toBeReadableFile(string $message = ''): self { if (! is_string($this->value)) { InvalidExpectationValue::expected('string'); } Assert::assertFileIsReadable($this->value, $message); return $this; } /** * Asserts that the value is a file and is writable. * * @return self */ public function toBeWritableFile(string $message = ''): self { if (! is_string($this->value)) { InvalidExpectationValue::expected('string'); } Assert::assertFileIsWritable($this->value, $message); return $this; } /** * Asserts that the value array matches the given array subset. * * @param iterable $array * @return self */ public function toMatchArray(iterable $array, string $message = ''): self { if (is_object($this->value) && method_exists($this->value, 'toArray')) { $valueAsArray = $this->value->toArray(); } else { $valueAsArray = (array) $this->value; } foreach ($array as $key => $value) { Assert::assertArrayHasKey($key, $valueAsArray, $message); $assertMessage = $message !== '' ? $message : sprintf( 'Failed asserting that an array has a key %s with the value %s.', $this->export($key), $this->export($valueAsArray[$key]), ); Assert::assertEquals($value, $valueAsArray[$key], $assertMessage); } return $this; } /** * Asserts that the value object matches a subset * of the properties of an given object. * * @param iterable $object * @return self */ public function toMatchObject(object|iterable $object, string $message = ''): self { foreach ((array) $object as $property => $value) { if (! is_object($this->value) && ! is_string($this->value)) { InvalidExpectationValue::expected('object|string'); } Assert::assertTrue(property_exists($this->value, $property), $message); /* @phpstan-ignore-next-line */ $propertyValue = $this->value->{$property}; $assertMessage = $message !== '' ? $message : sprintf( 'Failed asserting that an object has a property %s with the value %s.', $this->export($property), $this->export($propertyValue), ); Assert::assertEquals($value, $propertyValue, $assertMessage); } return $this; } /** * Asserts that the value "stringable" matches the given snapshot.. * * @return self */ public function toMatchSnapshot(string $message = ''): self { $snapshots = TestSuite::getInstance()->snapshots; $snapshots->startNewExpectation(); $testCase = TestSuite::getInstance()->test; assert($testCase instanceof TestCase); $string = match (true) { is_string($this->value) => $this->value, 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 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), is_object($this->value) && method_exists($this->value, 'toArray') => json_encode($this->value->toArray(), JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT), default => InvalidExpectationValue::expected('array|object|string'), }; if ($snapshots->has()) { [$filename, $content] = $snapshots->get(); Assert::assertSame( strtr($content, ["\r\n" => "\n", "\r" => "\n"]), strtr($string, ["\r\n" => "\n", "\r" => "\n"]), $message === '' ? "Failed asserting that the string value matches its snapshot ($filename)." : $message ); } else { $filename = $snapshots->save($string); TestSuite::getInstance()->registerSnapshotChange("Snapshot created at [$filename]"); } return $this; } /** * Asserts that the value matches a regular expression. * * @return self */ public function toMatch(string $expression, string $message = ''): self { if (! is_string($this->value)) { InvalidExpectationValue::expected('string'); } Assert::assertMatchesRegularExpression($expression, $this->value, $message); return $this; } /** * Asserts that the value matches a constraint. * * @return self */ public function toMatchConstraint(Constraint $constraint, string $message = ''): self { Assert::assertThat($this->value, $constraint, $message); return $this; } /** * @param class-string $class * @return self */ public function toContainOnlyInstancesOf(string $class, string $message = ''): self { if (! is_iterable($this->value)) { InvalidExpectationValue::expected('iterable'); } Assert::assertContainsOnlyInstancesOf($class, $this->value, $message); return $this; } /** * Asserts that executing value throws an exception. * * @param (Closure(Throwable): mixed)|string $exception * @return self */ public function toThrow(callable|string|Throwable $exception, ?string $exceptionMessage = null, string $message = ''): self { $callback = NullClosure::create(); if ($exception instanceof Closure) { $callback = $exception; $parameters = (new ReflectionFunction($exception))->getParameters(); if (count($parameters) !== 1) { throw new InvalidArgumentException('The given closure must have a single parameter type-hinted as the class string.'); } if (! ($type = $parameters[0]->getType()) instanceof ReflectionNamedType) { throw new InvalidArgumentException('The given closure\'s parameter must be type-hinted as the class string.'); } $exception = $type->getName(); } try { ($this->value)(); } catch (Throwable $e) { if ($exception instanceof Throwable) { expect($e) ->toBeInstanceOf($exception::class, $message) ->and($e->getMessage())->toBe($exceptionMessage ?? $exception->getMessage(), $message); return $this; } if (! class_exists($exception)) { if ($e instanceof Error && "Class \"$exception\" not found" === $e->getMessage()) { Assert::assertTrue(true); throw $e; } Assert::assertStringContainsString($exception, $e->getMessage(), $message); return $this; } if ($exceptionMessage !== null) { Assert::assertStringContainsString($exceptionMessage, $e->getMessage(), $message); } Assert::assertInstanceOf($exception, $e, $message); $callback($e); return $this; } Assert::assertTrue(true); if (! $exception instanceof Throwable && ! class_exists($exception)) { throw new ExpectationFailedException("Exception with message \"$exception\" not thrown."); } throw new ExpectationFailedException("Exception \"$exception\" not thrown."); } /** * Exports the given value. */ private function export(mixed $value): string { if (! $this->exporter instanceof Exporter) { $this->exporter = Exporter::default(); } return $this->exporter->shortenedExport($value); } /** * Asserts that the value is uppercase. * * @return self */ public function toBeUppercase(string $message = ''): self { Assert::assertTrue(ctype_upper((string) $this->value), $message); return $this; } /** * Asserts that the value is lowercase. * * @return self */ public function toBeLowercase(string $message = ''): self { Assert::assertTrue(ctype_lower((string) $this->value), $message); return $this; } /** * Asserts that the value is alphanumeric. * * @return self */ public function toBeAlphaNumeric(string $message = ''): self { Assert::assertTrue(ctype_alnum((string) $this->value), $message); return $this; } /** * Asserts that the value is alpha. * * @return self */ public function toBeAlpha(string $message = ''): self { Assert::assertTrue(ctype_alpha((string) $this->value), $message); return $this; } /** * Asserts that the value is snake_case. * * @return self */ public function toBeSnakeCase(string $message = ''): self { $value = (string) $this->value; if ($message === '') { $message = "Failed asserting that {$value} is snake_case."; } Assert::assertTrue((bool) preg_match('/^[\p{Ll}_]+$/u', $value), $message); return $this; } /** * Asserts that the value is kebab-case. * * @return self */ public function toBeKebabCase(string $message = ''): self { $value = (string) $this->value; if ($message === '') { $message = "Failed asserting that {$value} is kebab-case."; } Assert::assertTrue((bool) preg_match('/^[\p{Ll}-]+$/u', $value), $message); return $this; } /** * Asserts that the value is camelCase. * * @return self */ public function toBeCamelCase(string $message = ''): self { $value = (string) $this->value; if ($message === '') { $message = "Failed asserting that {$value} is camelCase."; } Assert::assertTrue((bool) preg_match('/^\p{Ll}[\p{Ll}\p{Lu}]+$/u', $value), $message); return $this; } /** * Asserts that the value is StudlyCase. * * @return self */ public function toBeStudlyCase(string $message = ''): self { $value = (string) $this->value; if ($message === '') { $message = "Failed asserting that {$value} is StudlyCase."; } Assert::assertTrue((bool) preg_match('/^\p{Lu}+\p{Ll}[\p{Ll}\p{Lu}]+$/u', $value), $message); return $this; } /** * Asserts that the value is UUID. * * @return self */ public function toBeUuid(string $message = ''): self { if (! is_string($this->value)) { InvalidExpectationValue::expected('string'); } Assert::assertTrue(Str::isUuid($this->value), $message); return $this; } /** * Asserts that the value is between 2 specified values * * @return self */ public function toBeBetween(int|float|DateTimeInterface $lowestValue, int|float|DateTimeInterface $highestValue, string $message = ''): self { Assert::assertGreaterThanOrEqual($lowestValue, $this->value, $message); Assert::assertLessThanOrEqual($highestValue, $this->value, $message); return $this; } /** * Asserts that the value is a url * * @return self */ public function toBeUrl(string $message = ''): self { if ($message === '') { $message = "Failed asserting that {$this->value} is a url."; } Assert::assertTrue(Str::isUrl((string) $this->value), $message); return $this; } /** * Asserts that the value can be converted to a slug * * @return self */ public function toBeSlug(string $message = ''): self { if ($message === '') { $message = "Failed asserting that {$this->value} can be converted to a slug."; } $slug = Str::slugify((string) $this->value); Assert::assertNotEmpty($slug, $message); return $this; } } ================================================ FILE: src/Panic.php ================================================ getPrevious())) { $throwable = $previous; } $panic = new self($throwable); $panic->handle(); exit(1); } /** * Handles the panic. */ private function handle(): void { try { $output = Container::getInstance()->get(OutputInterface::class); } catch (Throwable) { $output = new ConsoleOutput; } assert($output instanceof OutputInterface); if ($this->throwable instanceof Contracts\Panicable) { $this->throwable->render($output); exit($this->throwable->exitCode()); } $writer = new Writer(null, $output); $inspector = new Inspector($this->throwable); $writer->write($inspector); $output->writeln(''); exit(1); } } ================================================ FILE: src/PendingCalls/AfterEachCall.php ================================================ closure = $closure instanceof Closure ? $closure : NullClosure::create(); $this->proxies = new HigherOrderMessageCollection; $this->describing = DescribeCall::describing(); } /** * Creates the Call. */ public function __destruct() { $describing = $this->describing; $proxies = $this->proxies; $afterEachTestCase = ChainableClosure::boundWhen( fn (): bool => $describing === [] || in_array(Arr::last($describing), $this->__describing, true), ChainableClosure::bound(fn () => $proxies->chain($this), $this->closure)->bindTo($this, self::class), )->bindTo($this, self::class); assert($afterEachTestCase instanceof Closure); $this->testSuite->afterEach->set( $this->filename, $this, $afterEachTestCase, ); } /** * Saves the calls to be used on the target. * * @param array $arguments */ public function __call(string $name, array $arguments): self { $this->proxies ->add(Backtrace::file(), Backtrace::line(), $name, $arguments); return $this; } } ================================================ FILE: src/PendingCalls/BeforeEachCall.php ================================================ closure = $closure instanceof Closure ? $closure : NullClosure::create(); $this->testCallProxies = new HigherOrderMessageCollection; $this->testCaseProxies = new HigherOrderMessageCollection; $this->describing = DescribeCall::describing(); } /** * Creates the Call. */ public function __destruct() { $describing = $this->describing; $testCaseProxies = $this->testCaseProxies; $beforeEachTestCall = function (TestCall $testCall) use ($describing): void { if ($this->describing !== []) { if (Arr::last($describing) !== Arr::last($this->describing)) { return; } if (! in_array(Arr::last($describing), $testCall->describing, true)) { return; } } $this->testCallProxies->chain($testCall); }; $beforeEachTestCase = ChainableClosure::boundWhen( fn (): bool => $describing === [] || in_array(Arr::last($describing), $this->__describing, true), ChainableClosure::bound(fn () => $testCaseProxies->chain($this), $this->closure)->bindTo($this, self::class), )->bindTo($this, self::class); assert($beforeEachTestCase instanceof Closure); $this->testSuite->beforeEach->set( $this->filename, $this, $beforeEachTestCall, $beforeEachTestCase, ); } /** * Runs the given closure after the test. */ public function after(Closure $closure): self { if ($this->describing === []) { throw new AfterBeforeTestFunction($this->filename); } return $this->__call('after', [$closure]); } /** * Saves the calls to be used on the target. * * @param array $arguments */ public function __call(string $name, array $arguments): self { if (method_exists(TestCall::class, $name)) { $this->testCallProxies ->add(Backtrace::file(), Backtrace::line(), $name, $arguments); return $this; } $this->testCaseProxies ->add(Backtrace::file(), Backtrace::line(), $name, $arguments); return $this; } } ================================================ FILE: src/PendingCalls/Concerns/Describable.php ================================================ */ public array $__describing; /** * The describing of the test case. * * @var array */ public array $describing = []; } ================================================ FILE: src/PendingCalls/DescribeCall.php ================================================ */ private static array $describing = []; /** * The describe "before each" call. */ private ?BeforeEachCall $currentBeforeEachCall = null; /** * Creates a new Pending Call. */ public function __construct( public readonly TestSuite $testSuite, public readonly string $filename, public readonly Description $description, public readonly Closure $tests ) { // } /** * What is the current describing. * * @return array */ public static function describing(): array { return self::$describing; } /** * Creates the Call. */ public function __destruct() { unset($this->currentBeforeEachCall); self::$describing[] = $this->description; try { ($this->tests)(); } finally { array_pop(self::$describing); } } /** * Dynamically calls methods on each test call. * * @param array $arguments */ public function __call(string $name, array $arguments): self { $filename = Backtrace::file(); if (! $this->currentBeforeEachCall instanceof BeforeEachCall) { $this->currentBeforeEachCall = new BeforeEachCall(TestSuite::getInstance(), $filename); $this->currentBeforeEachCall->describing[] = $this->description; } $this->currentBeforeEachCall->{$name}(...$arguments); return $this; } } ================================================ FILE: src/PendingCalls/TestCall.php ================================================ */ private array $testCaseFactoryAttributes = []; /** * The Test Case Factory. */ public readonly TestCaseMethodFactory $testCaseMethod; /** * If test call is descriptionLess. */ private readonly bool $descriptionLess; /** * Creates a new Pending Call. */ public function __construct( private readonly TestSuite $testSuite, private readonly string $filename, private ?string $description = null, ?Closure $closure = null ) { $this->testCaseMethod = new TestCaseMethodFactory($filename, $closure); $this->descriptionLess = $description === null; $this->describing = DescribeCall::describing(); $this->testSuite->beforeEach->get($this->filename)[0]($this); } /** * Runs the given closure after the test. */ public function after(Closure $closure): self { if ($this->description === null) { throw new TestDescriptionMissing($this->filename); } $description = $this->describing === [] ? $this->description : Str::describe($this->describing, $this->description); $filename = $this->filename; $when = function () use ($closure, $filename, $description): void { if ($this::$__filename !== $filename) { // @phpstan-ignore-line return; } if ($this->__description !== $description) { // @phpstan-ignore-line return; } if ($this->__ran !== true) { // @phpstan-ignore-line return; } $closure->call($this); }; new AfterEachCall($this->testSuite, $this->filename, $when->bindTo(new \stdClass)); return $this; } /** * Asserts that the test fails with the given message. */ public function fails(?string $message = null): self { return $this->throws(AssertionFailedError::class, $message); } /** * Asserts that the test throws the given `$exceptionClass` when called. */ public function throws(string|int $exception, ?string $exceptionMessage = null, ?int $exceptionCode = null): self { if (is_int($exception)) { $exceptionCode = $exception; } elseif (class_exists($exception)) { $this->testCaseMethod ->proxies ->add(Backtrace::file(), Backtrace::line(), 'expectException', [$exception]); } else { $exceptionMessage = $exception; } if (is_string($exceptionMessage)) { $this->testCaseMethod ->proxies ->add(Backtrace::file(), Backtrace::line(), 'expectExceptionMessage', [$exceptionMessage]); } if (is_int($exceptionCode)) { $this->testCaseMethod ->proxies ->add(Backtrace::file(), Backtrace::line(), 'expectExceptionCode', [$exceptionCode]); } return $this; } /** * Asserts that the test throws the given `$exceptionClass` when called if the given condition is true. * * @param (callable(): bool)|bool $condition */ public function throwsIf(callable|bool $condition, string|int $exception, ?string $exceptionMessage = null, ?int $exceptionCode = null): self { $condition = is_callable($condition) ? $condition : static fn (): bool => $condition; if ($condition()) { return $this->throws($exception, $exceptionMessage, $exceptionCode); } return $this; } /** * Asserts that the test throws the given `$exceptionClass` when called if the given condition is false. * * @param (callable(): bool)|bool $condition */ public function throwsUnless(callable|bool $condition, string|int $exception, ?string $exceptionMessage = null, ?int $exceptionCode = null): self { $condition = is_callable($condition) ? $condition : static fn (): bool => $condition; if (! $condition()) { return $this->throws($exception, $exceptionMessage, $exceptionCode); } return $this; } /** * Runs the current test multiple times with each item of the given `iterable`. * * @param Closure|iterable|string $data */ public function with(Closure|iterable|string ...$data): self { foreach ($data as $dataset) { $this->testCaseMethod->datasets[] = $dataset; } return $this; } /** * Sets the test depends. */ public function depends(string ...$depends): self { foreach ($depends as $depend) { $this->testCaseMethod->depends[] = $depend; } return $this; } /** * Sets the test group(s). */ public function group(string ...$groups): self { foreach ($groups as $group) { $this->testCaseMethod->attributes[] = new Attribute( Group::class, [$group], ); } return $this; } /** * Filters the test suite by "only" tests. */ public function only(): self { Only::enable($this, ...func_get_args()); return $this; } /** * Skips the current test. */ public function skip(Closure|bool|string $conditionOrMessage = true, string $message = ''): self { $condition = is_string($conditionOrMessage) ? NullClosure::create() : $conditionOrMessage; $condition = is_callable($condition) ? $condition : fn (): bool => $condition; $message = is_string($conditionOrMessage) ? $conditionOrMessage : $message; /** @var callable(): bool $condition */ $condition = $condition->bindTo(null); $this->testCaseMethod ->chains ->addWhen($condition, $this->filename, Backtrace::line(), 'markTestSkipped', [$message]); return $this; } /** * Skips the current test on the given PHP version. */ public function skipOnPhp(string $version): self { if (mb_strlen($version) < 2) { throw new InvalidArgumentException('The version must start with [<] or [>].'); } if (str_starts_with($version, '>=') || str_starts_with($version, '<=')) { $operator = substr($version, 0, 2); $version = substr($version, 2); } elseif (str_starts_with($version, '>') || str_starts_with($version, '<')) { $operator = $version[0]; $version = substr($version, 1); // ensure starts with number: } elseif (is_numeric($version[0])) { $operator = '=='; } else { throw new InvalidArgumentException('The version must start with [<, >, <=, >=] or a number.'); } return $this->skip(version_compare(PHP_VERSION, $version, $operator), sprintf('This test is skipped on PHP [%s%s].', $operator, $version)); } /** * Skips the current test if the given test is running on Windows. */ public function skipOnWindows(): self { return $this->skipOnOs('Windows', 'This test is skipped on [Windows].'); } /** * Skips the current test if the given test is running on Mac OS. */ public function skipOnMac(): self { return $this->skipOnOs('Darwin', 'This test is skipped on [Mac].'); } /** * Skips the current test if the given test is running on Linux. */ public function skipOnLinux(): self { return $this->skipOnOs('Linux', 'This test is skipped on [Linux].'); } /** * Skips the current test if the given test is running on the given operating systems. */ private function skipOnOs(string $osFamily, string $message): self { return $osFamily === PHP_OS_FAMILY ? $this->skip($message) : $this; } /** * Weather the current test is running on a CI environment. */ private function runningOnCI(): bool { foreach ([ 'CI', 'GITHUB_ACTIONS', 'GITLAB_CI', 'CIRCLECI', 'TRAVIS', 'APPVEYOR', 'BITBUCKET_BUILD_NUMBER', 'BUILDKITE', 'TEAMCITY_VERSION', 'JENKINS_URL', 'SYSTEM_COLLECTIONURI', 'CI_NAME', 'TASKCLUSTER_ROOT_URL', 'DRONE', 'WERCKER', 'NEVERCODE', 'SEMAPHORE', 'NETLIFY', 'NOW_BUILDER', ] as $env) { if (getenv($env) !== false) { return true; } } return Environment::name() === Environment::CI; } /** * Skips the current test when running on a CI environments. */ public function skipOnCI(): self { if ($this->runningOnCI()) { return $this->skip('This test is skipped on [CI].'); } return $this; } public function skipLocally(): self { if ($this->runningOnCI() === false) { return $this->skip('This test is skipped [locally].'); } return $this; } /** * Skips the current test unless the given test is running on Windows. */ public function onlyOnWindows(): self { return $this->skipOnMac()->skipOnLinux(); } /** * Skips the current test unless the given test is running on Mac. */ public function onlyOnMac(): self { return $this->skipOnWindows()->skipOnLinux(); } /** * Skips the current test unless the given test is running on Linux. */ public function onlyOnLinux(): self { return $this->skipOnWindows()->skipOnMac(); } /** * Repeats the current test the given number of times. */ public function repeat(int $times): self { if ($times < 1) { throw new InvalidArgumentException('The number of repetitions must be greater than 0.'); } $this->testCaseMethod->repetitions = $times; return $this; } /** * Marks the test as "todo". */ public function todo(// @phpstan-ignore-line array|string|null $note = null, array|string|null $assignee = null, array|string|int|null $issue = null, array|string|int|null $pr = null, ): self { $this->skip('__TODO__'); $this->testCaseMethod->todo = true; if ($issue !== null) { $this->issue($issue); } if ($pr !== null) { $this->pr($pr); } if ($assignee !== null) { $this->assignee($assignee); } if ($note !== null) { $this->note($note); } return $this; } /** * Sets the test as "work in progress". */ public function wip(// @phpstan-ignore-line array|string|null $note = null, array|string|null $assignee = null, array|string|int|null $issue = null, array|string|int|null $pr = null, ): self { if ($issue !== null) { $this->issue($issue); } if ($pr !== null) { $this->pr($pr); } if ($assignee !== null) { $this->assignee($assignee); } if ($note !== null) { $this->note($note); } return $this; } /** * Sets the test as "done". */ public function done(// @phpstan-ignore-line array|string|null $note = null, array|string|null $assignee = null, array|string|int|null $issue = null, array|string|int|null $pr = null, ): self { if ($issue !== null) { $this->issue($issue); } if ($pr !== null) { $this->pr($pr); } if ($assignee !== null) { $this->assignee($assignee); } if ($note !== null) { $this->note($note); } return $this; } /** * Associates the test with the given issue(s). * * @param array|string|int $number */ public function issue(array|string|int $number): self { $number = is_array($number) ? $number : [$number]; $number = array_map(fn (string|int $number): int => (int) ltrim((string) $number, '#'), $number); $this->testCaseMethod->issues = array_merge($this->testCaseMethod->issues, $number); return $this; } /** * Associates the test with the given ticket(s). (Alias for `issue`) * * @param array|string|int $number */ public function ticket(array|string|int $number): self { return $this->issue($number); } /** * Sets the test assignee(s). * * @param array|string $assignee */ public function assignee(array|string $assignee): self { $assignees = is_array($assignee) ? $assignee : [$assignee]; $this->testCaseMethod->assignees = array_unique(array_merge($this->testCaseMethod->assignees, $assignees)); return $this; } /** * Associates the test with the given pull request(s). * * @param array|string|int $number */ public function pr(array|string|int $number): self { $number = is_array($number) ? $number : [$number]; $number = array_map(fn (string|int $number): int => (int) ltrim((string) $number, '#'), $number); $this->testCaseMethod->prs = array_unique(array_merge($this->testCaseMethod->prs, $number)); return $this; } /** * Adds a note to the test. * * @param array|string $note */ public function note(array|string $note): self { $notes = is_array($note) ? $note : [$note]; $this->testCaseMethod->notes = array_unique(array_merge($this->testCaseMethod->notes, $notes)); return $this; } /** * Sets the covered classes or methods. * * @param array|string $classesOrFunctions */ public function covers(array|string ...$classesOrFunctions): self { /** @var array $classesOrFunctions */ $classesOrFunctions = array_reduce($classesOrFunctions, fn ($carry, $item): array => is_array($item) ? array_merge($carry, $item) : array_merge($carry, [$item]), []); // @pest-ignore-type foreach ($classesOrFunctions as $classOrFunction) { $isClass = class_exists($classOrFunction) || interface_exists($classOrFunction) || enum_exists($classOrFunction); $isTrait = trait_exists($classOrFunction); $isFunction = function_exists($classOrFunction); if (! $isClass && ! $isTrait && ! $isFunction) { throw new InvalidArgumentException(sprintf('No class, trait or method named "%s" has been found.', $classOrFunction)); } if ($isClass) { $this->coversClass($classOrFunction); } elseif ($isTrait) { $this->coversTrait($classOrFunction); } else { $this->coversFunction($classOrFunction); } } return $this; } /** * Sets the covered classes. */ public function coversClass(string ...$classes): self { foreach ($classes as $class) { $this->testCaseFactoryAttributes[] = new Attribute( CoversClass::class, [$class], ); } /** @var ConfigurationRepository $configurationRepository */ $configurationRepository = Container::getInstance()->get(ConfigurationRepository::class); $paths = $configurationRepository->cliConfiguration->toArray()['paths'] ?? false; if (! is_array($paths)) { $configurationRepository->globalConfiguration('default')->class(...$classes); // @phpstan-ignore-line } return $this; } /** * Sets the covered classes. */ public function coversTrait(string ...$traits): self { foreach ($traits as $trait) { $this->testCaseFactoryAttributes[] = new Attribute( CoversTrait::class, [$trait], ); } /** @var ConfigurationRepository $configurationRepository */ $configurationRepository = Container::getInstance()->get(ConfigurationRepository::class); $paths = $configurationRepository->cliConfiguration->toArray()['paths'] ?? false; if (! is_array($paths)) { $configurationRepository->globalConfiguration('default')->class(...$traits); // @phpstan-ignore-line } return $this; } /** * Sets the covered functions. */ public function coversFunction(string ...$functions): self { foreach ($functions as $function) { $this->testCaseFactoryAttributes[] = new Attribute( CoversFunction::class, [$function], ); } return $this; } /** * Adds one or more references to the tested method or class. This helps * to link test cases to the source code for easier navigation. * * @param array|class-string ...$classes */ public function references(string|array ...$classes): self { assert($classes !== []); return $this; } /** * Adds one or more references to the tested method or class. This helps * to link test cases to the source code for easier navigation. * * @param array|class-string ...$classes */ public function see(string|array ...$classes): self { return $this->references(...$classes); } /** * Informs the test runner that no expectations happen in this test, * and its purpose is simply to check whether the given code can * be executed without throwing exceptions. */ public function throwsNoExceptions(): self { $this->testCaseMethod->proxies->add(Backtrace::file(), Backtrace::line(), 'expectNotToPerformAssertions', []); return $this; } /** * Saves the property accessors to be used on the target. */ public function __get(string $name): self { return $this->addChain(Backtrace::file(), Backtrace::line(), $name); } /** * Saves the calls to be used on the target. * * @param array $arguments */ public function __call(string $name, array $arguments): self { return $this->addChain(Backtrace::file(), Backtrace::line(), $name, $arguments); } /** * Add a chain to the test case factory. Omitting the arguments will treat it as a property accessor. * * @param array|null $arguments */ private function addChain(string $file, int $line, string $name, ?array $arguments = null): self { $exporter = Exporter::default(); $this->testCaseMethod ->chains ->add($file, $line, $name, $arguments); if ($this->descriptionLess) { Exporter::default(); if ($this->description !== null) { $this->description .= ' → '; } $this->description .= $arguments === null ? $name : sprintf('%s %s', $name, $exporter->shortenedRecursiveExport($arguments)); } return $this; } /** * Creates the Call. */ public function __destruct() { if ($this->description === null) { throw new TestDescriptionMissing($this->filename); } if ($this->describing !== []) { $this->testCaseMethod->describing = $this->describing; $this->testCaseMethod->description = Str::describe($this->describing, $this->description); } else { $this->testCaseMethod->description = $this->description; } $this->testSuite->tests->set($this->testCaseMethod); if (! is_null($testCase = $this->testSuite->tests->get($this->filename))) { $attributesToMerge = array_filter( $this->testCaseFactoryAttributes, fn (Attribute $attributeToMerge): bool => array_filter($testCase->attributes, fn (Attribute $attribute): bool => serialize($attributeToMerge) === serialize($attribute)) === [] ); $testCase->attributes = array_merge($testCase->attributes, $attributesToMerge); } } } ================================================ FILE: src/PendingCalls/UsesCall.php ================================================ `beforeAll` * - `1` => `beforeEach` * - `2` => `afterEach` * - `3` => `afterAll` * * @var array */ private array $hooks = []; /** * Holds the targets of the uses. * * @var array */ private array $targets; /** * Holds the groups of the uses. * * @var array */ private array $groups = []; /** * Creates a new Pending Call. * * @param array $classAndTraits */ public function __construct( private readonly string $filename, private array $classAndTraits ) { $this->targets = [$filename]; } /** * @deprecated Use `pest()->printer()->compact()` instead. */ public function compact(): self { DefaultPrinter::compact(true); return $this; } /** * Specifies the class or traits to use. * * @alias extend */ public function use(string ...$classAndTraits): self { return $this->extend(...$classAndTraits); } /** * Specifies the class or traits to use. */ public function extend(string ...$classAndTraits): self { $this->classAndTraits = array_merge($this->classAndTraits, array_values($classAndTraits)); return $this; } /** * The directories or file where the class or traits should be used. */ public function in(string ...$targets): self { $targets = array_map(function (string $path): string { $startChar = DIRECTORY_SEPARATOR; if ('\\' === DIRECTORY_SEPARATOR || preg_match('~\A[A-Z]:(?![^/\\\\])~i', $path) > 0) { $path = (string) preg_replace_callback('~^(?P[a-z]+:\\\)~i', fn (array $match): string => strtolower($match['drive']), $path); $startChar = strtolower((string) preg_replace('~^([a-z]+:\\\).*$~i', '$1', __DIR__)); } return str_starts_with($path, $startChar) ? $path : implode(DIRECTORY_SEPARATOR, [ is_dir($this->filename) ? $this->filename : dirname($this->filename), $path, ]); }, $targets); $this->targets = array_reduce($targets, function (array $accumulator, string $target): array { if (($matches = glob($target)) !== false) { foreach ($matches as $file) { $accumulator[] = (string) realpath($file); } } return $accumulator; }, []); return $this; } /** * Sets the test group(s). */ public function group(string ...$groups): self { $this->groups = array_values($groups); return $this; } /** * Sets the global beforeAll test hook. */ public function beforeAll(Closure $hook): self { $this->hooks[0] = $hook; return $this; } /** * Sets the global beforeEach test hook. */ public function beforeEach(Closure $hook): self { $this->hooks[1] = $hook; return $this; } /** * Sets the global afterEach test hook. */ public function afterEach(Closure $hook): self { $this->hooks[2] = $hook; return $this; } /** * Sets the global afterAll test hook. */ public function afterAll(Closure $hook): self { $this->hooks[3] = $hook; return $this; } /** * Creates the Call. */ public function __destruct() { TestSuite::getInstance()->tests->use( $this->classAndTraits, $this->groups, $this->targets, $this->hooks, ); } } ================================================ FILE: src/Pest.php ================================================ testPath.DIRECTORY_SEPARATOR.$file; } ================================================ FILE: src/Plugin.php ================================================ * * @internal */ public static array $callables = []; /** * Lazy loads an `uses` call on the context of plugins. * * @param class-string ...$traits */ public static function uses(string ...$traits): void { self::$callables[] = function () use ($traits): void { uses(...$traits)->in(TestSuite::getInstance()->rootPath); }; } } ================================================ FILE: src/Plugins/Actions/CallsAddsOutput.php ================================================ addOutput($exitCode); } return $exitCode; } } ================================================ FILE: src/Plugins/Actions/CallsBoot.php ================================================ boot(); } } } ================================================ FILE: src/Plugins/Actions/CallsHandleArguments.php ================================================ $argv * @return array */ public static function execute(array $argv): array { $plugins = Loader::getPlugins(Plugins\HandlesArguments::class); /** @var Plugins\HandlesArguments $plugin */ foreach ($plugins as $plugin) { $argv = $plugin->handleArguments($argv); } return $argv; } } ================================================ FILE: src/Plugins/Actions/CallsHandleOriginalArguments.php ================================================ $argv */ public static function execute(array $argv): void { $plugins = Loader::getPlugins(Plugins\HandlesOriginalArguments::class); /** @var Plugins\HandlesOriginalArguments $plugin */ foreach ($plugins as $plugin) { $plugin->handleOriginalArguments($argv); } } } ================================================ FILE: src/Plugins/Actions/CallsTerminable.php ================================================ terminate(); } } } ================================================ FILE: src/Plugins/Bail.php ================================================ hasArgument('--bail', $arguments)) { $arguments = $this->popArgument('--bail', $arguments); $arguments = $this->pushArgument('--stop-on-failure', $arguments); $arguments = $this->pushArgument('--stop-on-error', $arguments); } return $arguments; } } ================================================ FILE: src/Plugins/Cache.php ================================================ hasArgument('--cache-directory', $arguments)) { $cliConfiguration = (new CliConfigurationBuilder)->fromParameters([]); $configurationFile = (new XmlConfigurationFileFinder)->find($cliConfiguration); $xmlConfiguration = DefaultConfiguration::create(); if (is_string($configurationFile)) { $xmlConfiguration = (new Loader)->load($configurationFile); } if (! $xmlConfiguration->phpunit()->hasCacheDirectory()) { $arguments = $this->pushArgument('--cache-directory', $arguments); $arguments = $this->pushArgument((string) realpath(self::TEMPORARY_FOLDER), $arguments); } } if (! $this->hasArgument('--parallel', $arguments)) { return $this->pushArgument('--cache-result', $arguments); } return $arguments; } } ================================================ FILE: src/Plugins/Concerns/HandleArguments.php ================================================ $arguments */ public function hasArgument(string $argument, array $arguments): bool { foreach ($arguments as $arg) { if ($arg === $argument) { return true; } if (str_starts_with((string) $arg, "$argument=")) { // @phpstan-ignore-line return true; } } return false; } /** * Adds the given argument and value to the list of arguments. * * @param array $arguments * @return array */ public function pushArgument(string $argument, array $arguments): array { $arguments[] = $argument; return $arguments; } /** * Pops the given argument from the arguments. * * @param array $arguments * @return array */ public function popArgument(string $argument, array $arguments): array { $arguments = array_flip($arguments); unset($arguments[$argument]); return array_values(array_flip($arguments)); } } ================================================ FILE: src/Plugins/Configuration.php ================================================ hasArgument('--configuration', $arguments) || $this->hasArgument('-c', $arguments) || $this->hasCustomConfigurationFile()) { return $arguments; } $arguments = $this->pushArgument('--configuration', $arguments); return $this->pushArgument((string) realpath($this->fromGeneratedConfigurationFile()), $arguments); } /** * Get the configuration file from the generated configuration file. */ private function fromGeneratedConfigurationFile(): string { $path = $this->getTempPhpunitXmlPath(); if (file_exists($path)) { unlink($path); } $doc = new DOMDocument; $doc->load(self::BASE_PHPUNIT_FILE); $contents = $doc->saveXML(); assert(is_int(file_put_contents($path, $contents))); return $path; } /** * Check if the configuration file is custom. */ private function hasCustomConfigurationFile(): bool { $cliConfiguration = (new CliConfigurationBuilder)->fromParameters([]); $configurationFile = (new XmlConfigurationFileFinder)->find($cliConfiguration); return is_string($configurationFile); } /** * Get the temporary phpunit.xml path. */ private function getTempPhpunitXmlPath(): string { return getcwd().'/.pest.xml'; } /** * Terminates the plugin. */ public function terminate(): void { $path = $this->getTempPhpunitXmlPath(); if (file_exists($path)) { unlink($path); } } } ================================================ FILE: src/Plugins/Coverage.php ================================================ getOption(self::COVERAGE_OPTION)) { $this->coverage = true; $originals[] = '--coverage-php'; $originals[] = \Pest\Support\Coverage::getPath(); if (! \Pest\Support\Coverage::isAvailable()) { if (\Pest\Support\Coverage::usingXdebug()) { $this->output->writeln([ '', " ERROR Unable to get coverage using Xdebug. Did you set Xdebug's coverage mode?", '', ]); } else { $this->output->writeln([ '', ' ERROR No code coverage driver is available.', '', ]); } exit(1); } } if ($input->getOption(self::MIN_OPTION) !== null) { /** @var int|float $minOption */ $minOption = $input->getOption(self::MIN_OPTION); $this->coverageMin = (float) $minOption; } if ($input->getOption(self::EXACTLY_OPTION) !== null) { /** @var int|float $exactlyOption */ $exactlyOption = $input->getOption(self::EXACTLY_OPTION); $this->coverageExactly = (float) $exactlyOption; } if ($_SERVER['COLLISION_PRINTER_COMPACT'] ?? false) { $this->compact = true; } return $originals; } /** * {@inheritdoc} */ public function addOutput(int $exitCode): int { if (Parallel::isWorker()) { return $exitCode; } if ($exitCode === 0 && $this->coverage) { if (! \Pest\Support\Coverage::isAvailable()) { $this->output->writeln( "\n ERROR No code coverage driver is available.", ); exit(1); } $coverage = \Pest\Support\Coverage::report($this->output, $this->compact); $exitCode = (int) ($coverage < $this->coverageMin); if ($exitCode === 0 && $this->coverageExactly !== null) { $comparableCoverage = $this->computeComparableCoverage($coverage); $comparableCoverageExactly = $this->computeComparableCoverage($this->coverageExactly); $exitCode = $comparableCoverage === $comparableCoverageExactly ? 0 : 1; if ($exitCode === 1) { $this->output->writeln(sprintf( "\n FAIL Code coverage not exactly %s %%, currently %s %%.", number_format($this->coverageExactly, 1), number_format(floor($coverage * 10) / 10, 1), )); } } elseif ($exitCode === 1) { $this->output->writeln(sprintf( "\n FAIL Code coverage below expected %s %%, currently %s %%.", number_format($this->coverageMin, 1), number_format(floor($coverage * 10) / 10, 1) )); } $this->output->writeln(['']); } return $exitCode; } /** * Computes the comparable coverage to a percentage with one decimal. */ private function computeComparableCoverage(float $coverage): float { return floor($coverage * 10) / 10; } } ================================================ FILE: src/Plugins/Environment.php ================================================ $argument) { if ($argument === '--ci') { unset($arguments[$index]); self::$name = self::CI; } } return array_values($arguments); } /** * Gets the environment name. */ public static function name(?string $name = null): string { if (is_string($name)) { self::$name = $name; } return self::$name ?? self::LOCAL; } } ================================================ FILE: src/Plugins/Help.php ================================================ hasArgument('--help', $arguments)) { View::render('version', [ 'version' => version(), ]); View::render('usage'); foreach ($this->getContent() as $title => $options) { if ($title === 'Usage') { continue; } $this->output->writeln([ '', sprintf(' %s OPTIONS:', mb_strtoupper($title)), ]); foreach ($options as $option) { if (! array_key_exists('arg', $option)) { continue; } [ 'arg' => $argument, 'desc' => $description, ] = $option; assert(is_string($argument)); if (trim($argument) === '--process-isolation') { continue; } View::render('components.two-column-detail', [ 'left' => $this->colorizeOptions($argument), 'right' => preg_replace(['//'], ['[', ']'], $description), ]); } } $this->output->write('', true); exit(0); } return $arguments; } /** * Colorizes the given string options. */ private function colorizeOptions(string $argument): string { return (string) preg_replace( ['//', '/(-+[\w-]+)/'], ['[', ']', '$1'], $argument ); } /** * @return array>> */ private function getContent(): array { $helpReflection = new PHPUnitHelp; // @phpstan-ignore-next-line $content = (fn (): array => $this->elements())->call($helpReflection); $content['Configuration'] = [...[[ 'arg' => '--init', 'desc' => 'Initialise a standard Pest configuration', ]], ...$content['Configuration']]; $content['Execution'] = [...[ [ 'arg' => '--parallel', 'desc' => 'Run tests in parallel', ], [ 'arg' => '--update-snapshots', 'desc' => 'Update snapshots for tests using the "toMatchSnapshot" expectation', ], ], ...$content['Execution']]; $content['Selection'] = [[ 'arg' => '--bail', 'desc' => 'Stop execution upon first not-passed test', ], [ 'arg' => '--todos', 'desc' => 'Output to standard output the list of todos', ], [ 'arg' => '--notes', 'desc' => 'Output to standard output tests with notes', ], [ ], [ 'arg' => '--issue', 'desc' => 'Output to standard output tests with the given issue number', ], [ ], [ 'arg' => '--pr', 'desc' => 'Output to standard output tests with the given pull request number', ], [ ], [ 'arg' => '--pull-request', 'desc' => 'Output to standard output tests with the given pull request number (alias for --pr)', ], [ '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'], ...[ [ 'arg' => '--compact', 'desc' => 'Replace default result output with Compact format', ], ]]; $content['Code Coverage'] = [[ 'arg' => '--coverage ', 'desc' => 'Generate code coverage report and output to standard output', ], [ 'arg' => '--coverage --min', 'desc' => 'Set the minimum required coverage percentage, and fail if not met', ], ...$content['Code Coverage']]; $content['Mutation Testing'] = [[ 'arg' => '--mutate ', 'desc' => 'Runs mutation testing, to understand the quality of your tests', ], [ 'arg' => '--mutate --parallel', 'desc' => 'Runs mutation testing in parallel', ], [ 'arg' => '--mutate --min', 'desc' => 'Set the minimum required mutation score, and fail if not met', ], [ 'arg' => '--mutate --id', 'desc' => 'Run only the mutation with the given ID. But E.g. --id=ecb35ab30ffd3491. Note, you need to provide the same options as the original run', ], [ 'arg' => '--mutate --covered-only', 'desc' => 'Only generate mutations for classes that are covered by tests', ], [ 'arg' => '--mutate --bail', 'desc' => 'Stop mutation testing execution upon first untested or uncovered mutation', ], [ 'arg' => '--mutate --class', 'desc' => 'Generate mutations for the given class(es). E.g. --class=App\\\\Models', ], [ 'arg' => '--mutate --ignore', 'desc' => 'Ignore the given class(es) when generating mutations. E.g. --ignore=App\\\\Http\\\\Requests', ], [ 'arg' => '--mutate --clear-cache', 'desc' => 'Clear the mutation cache', ], [ 'arg' => '--mutate --no-cache', 'desc' => 'Clear the mutation cache', ], [ 'arg' => '--mutate --ignore-min-score-on-zero-mutations', 'desc' => 'Ignore the minimum score requirement when there are no mutations', ], [ 'arg' => '--mutate --covered-only', 'desc' => 'Only generate mutations for classes that are covered by tests', ], [ 'arg' => '--mutate --everything', 'desc' => 'Generate mutations for all classes, even if they are not covered by tests', ], [ 'arg' => '--mutate --profile', 'desc' => 'Output to standard output the top ten slowest mutations', ], [ 'arg' => '--mutate --retry', 'desc' => 'Run untested or uncovered mutations first and stop execution upon first error or failure', ], [ 'arg' => '--mutate --stop-on-uncovered', 'desc' => 'Stop mutation testing execution upon first untested mutation', ], [ 'arg' => '--mutate --stop-on-untested', 'desc' => 'Stop mutation testing execution upon first untested mutation', ]]; $content['Profiling'] = [ [ 'arg' => '--profile ', 'desc' => 'Output to standard output the top ten slowest tests', ], ]; unset($content['Miscellaneous']); return $content; } } ================================================ FILE: src/Plugins/Init.php ================================================ 'phpunit.xml', 'Pest.php.stub' => 'tests/Pest.php', 'TestCase.php.stub' => 'tests/TestCase.php', 'Unit/ExampleTest.php.stub' => 'tests/Unit/ExampleTest.php', 'Feature/ExampleTest.php.stub' => 'tests/Feature/ExampleTest.php', ]; /** * Creates a new Plugin instance. */ public function __construct( private TestSuite $testSuite, private InputInterface $input, private OutputInterface $output ) { // .. } /** * {@inheritdoc} */ public function handleArguments(array $arguments): array { if (! array_key_exists(1, $arguments)) { return $arguments; } if ($arguments[1] !== self::INIT_OPTION) { return $arguments; } unset($arguments[1]); $this->init(); exit(0); } /** * Initializes the tests directory. */ public function init(): void { $testsBaseDir = "{$this->testSuite->rootPath}/tests"; if (! is_dir($testsBaseDir)) { mkdir($testsBaseDir); } View::render('components.badge', [ 'type' => 'INFO', 'content' => 'Preparing tests directory.', ]); foreach (self::STUBS as $from => $to) { if ($this->isLaravelInstalled()) { $fromPath = __DIR__."/../../stubs/init-laravel/{$from}"; } else { $fromPath = __DIR__."/../../stubs/init/{$from}"; } $toPath = "{$this->testSuite->rootPath}/{$to}"; if (file_exists($toPath)) { View::render('components.two-column-detail', [ 'left' => $to, 'right' => 'File already exists.', ]); continue; } if (! is_dir(dirname($toPath))) { mkdir(dirname($toPath)); } copy($fromPath, $toPath); View::render('components.two-column-detail', [ 'left' => $to, 'right' => 'File created.', ]); } View::render('components.new-line'); (new Thanks($this->input, $this->output))(); } /** * Checks if laravel is installed through Composer */ private function isLaravelInstalled(): bool { return InstalledVersions::isInstalled('laravel/framework'); } } ================================================ FILE: src/Plugins/Memory.php ================================================ enabled = $this->hasArgument('--memory', $arguments); return $this->popArgument('--memory', $arguments); } /** * {@inheritdoc} */ public function addOutput(int $exitCode): int { if ($this->enabled) { $this->output->writeln(sprintf( ' Memory: %s MB', round(memory_get_usage(true) / 1000 ** 2, 3) )); } return $exitCode; } } ================================================ FILE: src/Plugins/Only.php ================================================ group($group); } else { $testCall->attributes[] = new Attribute( Group::class, [$group], ); } if (Environment::name() === Environment::CI || Parallel::isWorker()) { return; } $lockFile = self::TEMPORARY_FOLDER.DIRECTORY_SEPARATOR.'only.lock'; if (file_exists($lockFile) && $group === '__pest_only') { file_put_contents($lockFile, $group); return; } if (! file_exists($lockFile)) { touch($lockFile); file_put_contents($lockFile, $group); } } /** * Checks if "only" mode is enabled. */ public static function isEnabled(): bool { $lockFile = self::TEMPORARY_FOLDER.DIRECTORY_SEPARATOR.'only.lock'; return file_exists($lockFile); } /** * Returns the group name. */ public static function group(): string { $lockFile = self::TEMPORARY_FOLDER.DIRECTORY_SEPARATOR.'only.lock'; if (! file_exists($lockFile)) { return '__pest_only'; } return file_get_contents($lockFile) ?: '__pest_only'; // @phpstan-ignore-line } /** * {@inheritDoc} */ public function terminate(): void { if (Parallel::isWorker()) { return; } $lockFile = self::TEMPORARY_FOLDER.DIRECTORY_SEPARATOR.'only.lock'; if (file_exists($lockFile)) { unlink($lockFile); } } } ================================================ FILE: src/Plugins/Parallel/Contracts/HandlersWorkerArguments.php ================================================ $arguments * @return array */ public function handleWorkerArguments(array $arguments): array; } ================================================ FILE: src/Plugins/Parallel/Handlers/Laravel.php ================================================ whenUsingLaravel($arguments, function (array $arguments): array { $this->ensureRunnerIsResolvable(); $arguments = $this->ensureEnvironmentVariables($arguments); return $this->ensureRunner($arguments); }); } /** * Executes the given closure when running Laravel. * * @param array $arguments * @param Closure(array): array $closure * @return array */ private function whenUsingLaravel(array $arguments, Closure $closure): array { $isLaravelApplication = InstalledVersions::isInstalled('laravel/framework', false); $isLaravelPackage = class_exists(TestCase::class); if ($isLaravelApplication && ! $isLaravelPackage) { return $closure($arguments); } return $arguments; } /** * Ensures the runner is resolvable. */ private function ensureRunnerIsResolvable(): void { ParallelRunner::resolveRunnerUsing( // @phpstan-ignore-line fn (Options $options, OutputInterface $output): RunnerInterface => new WrapperRunner($options, $output) ); } /** * Ensures the environment variables are set. * * @param array $arguments * @return array */ private function ensureEnvironmentVariables(array $arguments): array { $_ENV['LARAVEL_PARALLEL_TESTING'] = 1; if ($this->hasArgument('--recreate-databases', $arguments)) { $_ENV['LARAVEL_PARALLEL_TESTING_RECREATE_DATABASES'] = 1; } if ($this->hasArgument('--drop-databases', $arguments)) { $_ENV['LARAVEL_PARALLEL_TESTING_DROP_DATABASES'] = 1; } $arguments = $this->popArgument('--recreate-databases', $arguments); return $this->popArgument('--drop-databases', $arguments); } /** * Ensure the runner is set. * * @param array $arguments * @return array */ private function ensureRunner(array $arguments): array { foreach ($arguments as $value) { if (str_starts_with($value, '--runner')) { $arguments = $this->popArgument($value, $arguments); } } return $this->pushArgument('--runner=\Illuminate\Testing\ParallelRunner', $arguments); } } ================================================ FILE: src/Plugins/Parallel/Handlers/Parallel.php ================================================ $this->popArgument($arg, $args), $arguments); return $this->pushArgument('--runner='.WrapperRunner::class, $args); } } ================================================ FILE: src/Plugins/Parallel/Handlers/Pest.php ================================================ isOpeningHeadline($message)) { return; } parent::doWrite($message, $newline); } /** * Removes the opening headline, witch is not needed. */ private function isOpeningHeadline(string $message): bool { return str_contains($message, 'by Sebastian Bergmann and contributors.'); } } ================================================ FILE: src/Plugins/Parallel/Paratest/ResultPrinter.php ================================================ */ private array $tailPositions; public function __construct( private readonly OutputInterface $output, private readonly Options $options ) { $this->printer = new readonly class($this->output) implements Printer { public function __construct( private OutputInterface $output, ) {} public function print(string $buffer): void { $buffer = OutputFormatter::escape($buffer); if (str_starts_with($buffer, "\nGenerating code coverage report")) { return; } if (str_starts_with($buffer, 'done [')) { return; } $this->output->write(OutputFormatter::escape($buffer)); } public function flush(): void {} }; $this->compactPrinter = CompactPrinter::default(); if (! $this->options->configuration->hasLogfileTeamcity()) { return; } $teamcityLogFileHandle = fopen($this->options->configuration->logfileTeamcity(), 'ab+'); assert($teamcityLogFileHandle !== false); $this->teamcityLogFileHandle = $teamcityLogFileHandle; } /** @param list $teamcityFiles */ public function printFeedback( SplFileInfo $progressFile, SplFileInfo $outputFile, array $teamcityFiles ): void { if ($this->options->needsTeamcity) { $teamcityProgress = $this->tailMultiple($teamcityFiles); if ($this->teamcityLogFileHandle !== null) { fwrite($this->teamcityLogFileHandle, $teamcityProgress); } } if ($this->options->configuration->outputIsTeamCity()) { assert(isset($teamcityProgress)); $this->output->write($teamcityProgress); return; } if ($this->options->configuration->noProgress()) { return; } $unexpectedOutput = $this->tail($outputFile); if ($unexpectedOutput !== '') { if (preg_match('/^T+$/', $unexpectedOutput) > 0) { return; } $this->output->write($unexpectedOutput); } $feedbackItems = $this->tail($progressFile); if ($feedbackItems === '') { return; } $feedbackItems = (string) preg_replace('/ +\\d+ \\/ \\d+ \\( ?\\d+%\\)\\s*/', '', $feedbackItems); $actualTestCount = strlen($feedbackItems); for ($index = 0; $index < $actualTestCount; $index++) { $this->printFeedbackItem($feedbackItems[$index]); } } /** * @param list $teamcityFiles * @param list $testdoxFiles */ public function printResults(TestResult $testResult, array $teamcityFiles, array $testdoxFiles, Duration $duration): void { if ($this->options->needsTeamcity) { $teamcityProgress = $this->tailMultiple($teamcityFiles); if ($this->teamcityLogFileHandle !== null) { fwrite($this->teamcityLogFileHandle, $teamcityProgress); $resource = $this->teamcityLogFileHandle; $this->teamcityLogFileHandle = null; fclose($resource); } } if ($this->options->configuration->outputIsTeamCity()) { assert(isset($teamcityProgress)); $this->output->write($teamcityProgress); return; } if ($this->options->configuration->outputIsTestDox()) { $this->output->write($this->tailMultiple($testdoxFiles)); return; } $state = (new StateGenerator)->fromPhpUnitTestResult($this->passedTests, $testResult); $this->compactPrinter->errors($state); $this->compactPrinter->recap($state, $testResult, $duration, $this->options); } private function printFeedbackItem(string $item): void { if ($this->lastWasTodo) { $this->lastWasTodo = false; return; } if ($item === 'T') { $this->lastWasTodo = true; } if ($item === '.') { $this->passedTests++; } $this->compactPrinter->descriptionItem($item); } /** @param list $files */ private function tailMultiple(array $files): string { $content = ''; foreach ($files as $file) { if (! $file->isFile()) { continue; } $content .= $this->tail($file); } return $content; } private function tail(SplFileInfo $file): string { $path = $file->getPathname(); assert($path !== ''); $handle = fopen($path, 'r'); assert($handle !== false); $fseek = fseek($handle, $this->tailPositions[$path] ?? 0); assert($fseek === 0); $contents = ''; while (! feof($handle)) { $fread = fread($handle, 8192); assert($fread !== false); $contents .= $fread; } $ftell = ftell($handle); assert($ftell !== false); $this->tailPositions[$path] = $ftell; fclose($handle); return $contents; } } ================================================ FILE: src/Plugins/Parallel/Paratest/WrapperRunner.php ================================================ */ private array $pending = []; /** * The exit code. */ private int $exitcode = -1; /** @var array */ private array $workers = []; /** @var array */ private array $batches = []; /** @var list */ private array $unexpectedOutputFiles = []; /** @var list */ private array $resultCacheFiles = []; /** @var list */ private array $testResultFiles = []; /** @var list */ private array $coverageFiles = []; /** @var list */ private array $junitFiles = []; /** @var list */ private array $teamcityFiles = []; /** @var list */ private array $testdoxFiles = []; /** @var non-empty-string[] */ private readonly array $parameters; /** * The code coverage filter registry. */ private CodeCoverageFilterRegistry $codeCoverageFilterRegistry; public function __construct( private readonly Options $options, private readonly OutputInterface $output ) { $this->printer = new ResultPrinter($output, $options); $this->timer = new Timer; $wrapper = realpath( dirname(__DIR__, 4).DIRECTORY_SEPARATOR.'bin'.DIRECTORY_SEPARATOR.'worker.php', ); assert($wrapper !== false); $phpFinder = new PhpExecutableFinder; $phpBin = $phpFinder->find(false); assert($phpBin !== false); $parameters = [$phpBin]; $parameters = array_merge($parameters, $phpFinder->findArguments()); if ($options->passthruPhp !== null) { $parameters = array_merge($parameters, $options->passthruPhp); } /** @var array $parameters */ $parameters = $this->handleLaravelHerd($parameters); $parameters[] = $wrapper; $parameters[] = '--test-directory='.TestSuite::getInstance()->testPath; $this->parameters = $parameters; $this->codeCoverageFilterRegistry = new CodeCoverageFilterRegistry; } public function run(): int { $directory = dirname(__DIR__); assert($directory !== ''); ExcludeList::addDirectory($directory); TestResultFacade::init(); EventFacade::instance()->seal(); $suiteLoader = new SuiteLoader( $this->options, $this->output, $this->codeCoverageFilterRegistry, ); $this->pending = $this->getTestFiles($suiteLoader); $result = TestResultFacade::result(); $this->timer->start(); $this->startWorkers(); $this->assignAllPendingTests(); $this->waitForAllToFinish(); return $this->complete($result); } /** * Handles Laravel Herd's debug and coverage modes. * * @param array $parameters * @return array */ private function handleLaravelHerd(array $parameters): array { if (isset($_ENV['HERD_DEBUG_INI'])) { return array_merge($parameters, ['-c', $_ENV['HERD_DEBUG_INI']]); } return $parameters; } private function startWorkers(): void { for ($token = 1; $token <= $this->options->processes; $token++) { $this->startWorker($token); } } private function assignAllPendingTests(): void { $batchSize = $this->options->maxBatchSize; while (count($this->pending) > 0 && count($this->workers) > 0) { foreach ($this->workers as $token => $worker) { if (! $worker->isRunning()) { throw $worker->getWorkerCrashedException(); } if (! $worker->isFree()) { continue; } $this->flushWorker($worker); if ($batchSize !== 0 && $this->batches[$token] === $batchSize) { $this->destroyWorker($token); $worker = $this->startWorker($token); } if ( $this->exitcode > 0 && $this->options->configuration->stopOnFailure() ) { $this->pending = []; } elseif (($pending = array_shift($this->pending)) !== null) { $worker->assign($pending); $this->batches[$token]++; } } usleep(self::CYCLE_SLEEP); } } private function flushWorker(WrapperWorker $worker): void { $this->exitcode = max($this->exitcode, $worker->getExitCode()); $this->printer->printFeedback( $worker->progressFile, $worker->unexpectedOutputFile, $this->teamcityFiles, ); $worker->reset(); } private function waitForAllToFinish(): void { $stopped = []; while (count($this->workers) > 0) { foreach ($this->workers as $index => $worker) { if ($worker->isRunning()) { if (! isset($stopped[$index]) && $worker->isFree()) { $worker->stop(); $stopped[$index] = true; } continue; } if (! $worker->isFree()) { throw $worker->getWorkerCrashedException(); } $this->flushWorker($worker); unset($this->workers[$index]); } usleep(self::CYCLE_SLEEP); } } /** @param positive-int $token */ private function startWorker(int $token): WrapperWorker { $worker = new WrapperWorker( $this->output, $this->options, $this->parameters, $token, ); $worker->start(); $this->batches[$token] = 0; $this->unexpectedOutputFiles[] = $worker->unexpectedOutputFile; $this->unexpectedOutputFiles[] = $worker->unexpectedOutputFile; $this->testResultFiles[] = $worker->testResultFile; if (isset($worker->junitFile)) { $this->junitFiles[] = $worker->junitFile; } if (isset($worker->coverageFile)) { $this->coverageFiles[] = $worker->coverageFile; } if (isset($worker->teamcityFile)) { $this->teamcityFiles[] = $worker->teamcityFile; } if (isset($worker->testdoxFile)) { $this->testdoxFiles[] = $worker->testdoxFile; } return $this->workers[$token] = $worker; } private function destroyWorker(int $token): void { $this->workers[$token]->stop(); // We need to wait for ApplicationForWrapperWorker::end to end while ($this->workers[$token]->isRunning()) { usleep(self::CYCLE_SLEEP); } unset($this->workers[$token]); } private function complete(TestResult $testResultSum): int { foreach ($this->testResultFiles as $testResultFile) { if (! $testResultFile->isFile()) { continue; } $contents = file_get_contents($testResultFile->getPathname()); assert($contents !== false); $testResult = unserialize($contents); assert($testResult instanceof TestResult); /** @var list $failedEvents */ $failedEvents = array_merge_recursive($testResultSum->testFailedEvents(), $testResult->testFailedEvents()); $testResultSum = new TestResult( (int) $testResultSum->hasTests() + (int) $testResult->hasTests(), $testResultSum->numberOfTestsRun() + $testResult->numberOfTestsRun(), $testResultSum->numberOfAssertions() + $testResult->numberOfAssertions(), array_merge_recursive($testResultSum->testErroredEvents(), $testResult->testErroredEvents()), $failedEvents, array_merge_recursive($testResultSum->testConsideredRiskyEvents(), $testResult->testConsideredRiskyEvents()), array_merge_recursive($testResultSum->testSuiteSkippedEvents(), $testResult->testSuiteSkippedEvents()), array_merge_recursive($testResultSum->testSkippedEvents(), $testResult->testSkippedEvents()), array_merge_recursive($testResultSum->testMarkedIncompleteEvents(), $testResult->testMarkedIncompleteEvents()), array_merge_recursive($testResultSum->testTriggeredPhpunitDeprecationEvents(), $testResult->testTriggeredPhpunitDeprecationEvents()), array_merge_recursive($testResultSum->testTriggeredPhpunitErrorEvents(), $testResult->testTriggeredPhpunitErrorEvents()), array_merge_recursive($testResultSum->testTriggeredPhpunitNoticeEvents(), $testResult->testTriggeredPhpunitNoticeEvents()), array_merge_recursive($testResultSum->testTriggeredPhpunitWarningEvents(), $testResult->testTriggeredPhpunitWarningEvents()), // @phpstan-ignore-next-line array_merge_recursive($testResultSum->testRunnerTriggeredDeprecationEvents(), $testResult->testRunnerTriggeredDeprecationEvents()), // @phpstan-ignore-next-line array_merge_recursive($testResultSum->testRunnerTriggeredNoticeEvents(), $testResult->testRunnerTriggeredNoticeEvents()), // @phpstan-ignore-next-line array_merge_recursive($testResultSum->testRunnerTriggeredWarningEvents(), $testResult->testRunnerTriggeredWarningEvents()), // @phpstan-ignore-next-line array_merge_recursive($testResultSum->errors(), $testResult->errors()), // @phpstan-ignore-next-line array_merge_recursive($testResultSum->deprecations(), $testResult->deprecations()), // @phpstan-ignore-next-line array_merge_recursive($testResultSum->notices(), $testResult->notices()), // @phpstan-ignore-next-line array_merge_recursive($testResultSum->warnings(), $testResult->warnings()), // @phpstan-ignore-next-line array_merge_recursive($testResultSum->phpDeprecations(), $testResult->phpDeprecations()), // @phpstan-ignore-next-line array_merge_recursive($testResultSum->phpNotices(), $testResult->phpNotices()), // @phpstan-ignore-next-line array_merge_recursive($testResultSum->phpWarnings(), $testResult->phpWarnings()), $testResultSum->numberOfIssuesIgnoredByBaseline() + $testResult->numberOfIssuesIgnoredByBaseline(), ); } $testResultSum = new TestResult( ResultReflection::numberOfTests($testResultSum), $testResultSum->numberOfTestsRun(), $testResultSum->numberOfAssertions(), $testResultSum->testErroredEvents(), $testResultSum->testFailedEvents(), $testResultSum->testConsideredRiskyEvents(), $testResultSum->testSuiteSkippedEvents(), $testResultSum->testSkippedEvents(), $testResultSum->testMarkedIncompleteEvents(), $testResultSum->testTriggeredPhpunitDeprecationEvents(), $testResultSum->testTriggeredPhpunitErrorEvents(), $testResultSum->testTriggeredPhpunitNoticeEvents(), $testResultSum->testTriggeredPhpunitWarningEvents(), $testResultSum->testRunnerTriggeredDeprecationEvents(), $testResultSum->testRunnerTriggeredNoticeEvents(), array_values(array_filter( $testResultSum->testRunnerTriggeredWarningEvents(), fn (WarningTriggered $event): bool => ! str_contains($event->message(), 'No tests found') )), $testResultSum->errors(), $testResultSum->deprecations(), $testResultSum->notices(), $testResultSum->warnings(), $testResultSum->phpDeprecations(), $testResultSum->phpNotices(), $testResultSum->phpWarnings(), $testResultSum->numberOfIssuesIgnoredByBaseline(), ); if ($this->options->configuration->cacheResult()) { $resultCacheSum = new DefaultResultCache($this->options->configuration->testResultCacheFile()); foreach ($this->resultCacheFiles as $resultCacheFile) { $resultCache = new DefaultResultCache($resultCacheFile->getPathname()); $resultCache->load(); $resultCacheSum->mergeWith($resultCache); } $resultCacheSum->persist(); } $this->printer->printResults( $testResultSum, $this->teamcityFiles, $this->testdoxFiles, $this->timer->stop(), ); $this->generateCodeCoverageReports(); $this->generateLogs(); $exitcode = Result::exitCode($this->options->configuration, $testResultSum); $this->clearFiles($this->unexpectedOutputFiles); $this->clearFiles($this->testResultFiles); $this->clearFiles($this->coverageFiles); $this->clearFiles($this->junitFiles); $this->clearFiles($this->teamcityFiles); $this->clearFiles($this->testdoxFiles); return $exitcode; } private function generateCodeCoverageReports(): void { if ($this->coverageFiles === []) { return; } $coverageManager = new CodeCoverage; $coverageManager->init( $this->options->configuration, $this->codeCoverageFilterRegistry, false, ); if (! $coverageManager->isActive()) { $this->output->writeln([ '', ' WARN No code coverage driver is available.', '', ]); return; } $coverageMerger = new CoverageMerger($coverageManager->codeCoverage()); foreach ($this->coverageFiles as $coverageFile) { $coverageMerger->addCoverageFromFile($coverageFile); } $coverageManager->generateReports( $this->printer->printer, $this->options->configuration, ); } private function generateLogs(): void { if ($this->junitFiles === []) { return; } $testSuite = (new LogMerger)->merge($this->junitFiles); assert($testSuite instanceof \ParaTest\JUnit\TestSuite); (new Writer)->write( $testSuite, $this->options->configuration->logfileJunit(), ); } /** @param list $files */ private function clearFiles(array $files): void { foreach ($files as $file) { if (! $file->isFile()) { continue; } unlink($file->getPathname()); } } /** * Returns the test files to be executed. * * @return array */ private function getTestFiles(SuiteLoader $suiteLoader): array { /** @var array $files */ $files = [ ...array_values(array_filter( $suiteLoader->tests, fn (string $filename): bool => ! str_ends_with($filename, "eval()'d code") )), ...TestSuite::getInstance()->tests->getFilenames(), ]; return $files; // @phpstan-ignore-line } } ================================================ FILE: src/Plugins/Parallel/Support/CompactPrinter.php ================================================ > */ private const array LOOKUP_TABLE = [ '.' => ['gray', '.'], 'S' => ['yellow', 's'], 'T' => ['cyan', 't'], 'I' => ['yellow', '!'], 'N' => ['yellow', '!'], 'D' => ['yellow', '!'], 'R' => ['yellow', '!'], 'W' => ['yellow', '!'], 'E' => ['red', '⨯'], 'F' => ['red', '⨯'], ]; /** * Creates a new instance of the Compact Printer. */ public function __construct( private readonly Terminal $terminal, private readonly OutputInterface $output, private readonly Style $style, private readonly int $compactSymbolsPerLine, ) { // .. } /** * Creates a new instance of the Compact Printer. */ public static function default(): self { return new self( terminal(), new ConsoleOutput(decorated: true), new Style(new ConsoleOutput(decorated: true)), terminal()->width() - 4, ); } /** * Output an empty line in the console. Useful for providing a little breathing room. */ public function newLine(): void { render('
'); } /** * Outputs the given description item from the ProgressPrinter as a gorgeous, colored symbol. */ public function descriptionItem(string $item): void { [$color, $icon] = self::LOOKUP_TABLE[$item] ?? self::LOOKUP_TABLE['.']; $symbolsOnCurrentLine = $this->processed % $this->compactSymbolsPerLine; if ($symbolsOnCurrentLine >= $this->terminal->width() - 4) { $symbolsOnCurrentLine = 0; } if ($symbolsOnCurrentLine === 0) { $this->output->writeln(''); $this->output->write(' '); } $this->output->write(sprintf('%s', $color, $icon)); $this->processed++; } /** * Outputs all errors from the given state using Collision's beautiful error output. */ public function errors(State $state): void { $this->output->writeln(''); $this->style->writeErrorsSummary($state); } /** * Outputs a clean recap of the test run, including the number of tests, assertions, and failures. */ public function recap(State $state, PHPUnitTestResult $testResult, Duration $duration, Options $options): void { assert($this->output instanceof ConsoleOutput); $nanoseconds = $duration->asNanoseconds() % 1_000_000_000; $snapshotDuration = HRTime::fromSecondsAndNanoseconds((int) $duration->asSeconds(), $nanoseconds); $telemetryDuration = \PHPUnit\Event\Telemetry\Duration::fromSecondsAndNanoseconds((int) $duration->asSeconds(), $nanoseconds); $status = gc_status(); $garbageCollectorStatus = new GarbageCollectorStatus( $status['runs'], $status['collected'], $status['threshold'], $status['roots'], 0.00, 0.00, 0.00, 0.00, false, false, false, 0, ); $telemetry = new Info( new Snapshot( $snapshotDuration, MemoryUsage::fromBytes(0), MemoryUsage::fromBytes(0), $garbageCollectorStatus, ), $telemetryDuration, MemoryUsage::fromBytes(0), \PHPUnit\Event\Telemetry\Duration::fromSecondsAndNanoseconds(0, 0), MemoryUsage::fromBytes(0), ); $this->style->writeRecap($state, $telemetry, $testResult); $this->output->write("\033[1A"); $this->output->write([ sprintf( ' Parallel: %s process%s', $options->processes, $options->processes > 1 ? 'es' : '', ), "\n", "\n", ]); } } ================================================ FILE: src/Plugins/Parallel.php ================================================ hasParameterOption('--parallel')) { return true; } return $argv->hasParameterOption('-p'); } /** * If this code is running in a worker process rather than the main process. */ public static function isWorker(): bool { $argvValue = Arr::get($_SERVER, 'PARATEST'); assert(is_string($argvValue) || is_int($argvValue) || is_null($argvValue)); return ((int) $argvValue) === 1; } /** * Sets a global value that can be accessed by the parent process and all workers. */ public static function setGlobal(string $key, string|int|bool|Stringable $value): void { $data = ['value' => $value instanceof Stringable ? $value->__toString() : $value]; $_ENV[self::GLOBAL_PREFIX.$key] = json_encode($data, JSON_THROW_ON_ERROR); } /** * Returns the given global value if one has been set. */ public static function getGlobal(string $key): string|int|bool|null { $placesToCheck = [$_SERVER, $_ENV]; foreach ($placesToCheck as $location) { if (array_key_exists(self::GLOBAL_PREFIX.$key, $location)) { // @phpstan-ignore-next-line return json_decode((string) $location[self::GLOBAL_PREFIX.$key], true, 512, JSON_THROW_ON_ERROR)['value'] ?? null; } } return null; } /** * {@inheritdoc} */ public function handleArguments(array $arguments): array { if ($this->hasArgumentsThatWouldBeFasterWithoutParallel()) { return $this->runTestSuiteInSeries($arguments); } if (self::isEnabled()) { exit($this->runTestSuiteInParallel($arguments)); } if (self::isWorker()) { return $this->runWorkerHandlers($arguments); } return $arguments; } /** * Runs the test suite in parallel. This method will exit the process upon completion. * * @param array $arguments */ private function runTestSuiteInParallel(array $arguments): int { $handlers = array_filter( array_map(fn (string $handler): object|string => Container::getInstance()->get($handler), self::HANDLERS), fn (object|string $handler): bool => $handler instanceof HandlesArguments, ); $filteredArguments = array_reduce( $handlers, fn (array $arguments, HandlesArguments $handler): array => $handler->handleArguments($arguments), $arguments ); $exitCode = $this->paratestCommand()->run(new ArgvInput($filteredArguments), new CleanConsoleOutput); return CallsAddsOutput::execute($exitCode); } /** * Runs any handlers that have been registered to handle worker arguments, and returns the modified arguments. * * @param array $arguments * @return array */ private function runWorkerHandlers(array $arguments): array { $handlers = array_filter( array_map(fn (string $handler): object|string => Container::getInstance()->get($handler), self::HANDLERS), fn (object|string $handler): bool => $handler instanceof HandlersWorkerArguments, ); return array_reduce( $handlers, fn (array $arguments, HandlersWorkerArguments $handler): array => $handler->handleWorkerArguments($arguments), $arguments ); } /** * Builds an instance of the Paratest command. */ private function paratestCommand(): Application { /** @var non-empty-string $rootPath */ $rootPath = TestSuite::getInstance()->rootPath; $command = ParaTestCommand::applicationFactory($rootPath); $command->setAutoExit(false); $command->setName('Pest'); $command->setVersion(version()); return $command; } /** * Whether the command line arguments contain any arguments that are * not supported or are suboptimal when running in parallel. */ private function hasArgumentsThatWouldBeFasterWithoutParallel(): bool { $arguments = new ArgvInput; foreach (self::UNSUPPORTED_ARGUMENTS as $unsupportedArgument) { if ($arguments->hasParameterOption($unsupportedArgument)) { return true; } } return false; } /** * Removes any parallel arguments. * * @param array $arguments * @return array */ private function runTestSuiteInSeries(array $arguments): array { $arguments = $this->popArgument('--parallel', $arguments); return $this->popArgument('-p', $arguments); } } ================================================ FILE: src/Plugins/Printer.php ================================================ pushArgument('--no-output', $arguments); } } ================================================ FILE: src/Plugins/ProcessIsolation.php ================================================ hasArgument('--process-isolation', $arguments)) { throw new InvalidOption('The [--process-isolation] option is not supported.'); } return $arguments; } } ================================================ FILE: src/Plugins/Profile.php ================================================ hasArgument('--profile', $arguments)) { return $arguments; } if ($this->hasArgument('--parallel', $arguments)) { throw new InvalidOption('The [--profile] option is not supported when running in parallel.'); } return $arguments; } } ================================================ FILE: src/Plugins/Retry.php ================================================ hasArgument('--retry', $arguments)) { return $arguments; } if ($this->hasArgument('--parallel', $arguments)) { throw new InvalidOption('The [--retry] option is not supported when running in parallel.'); } $arguments = $this->popArgument('--retry', $arguments); $arguments = $this->pushArgument('--order-by=defects', $arguments); return $this->pushArgument('--stop-on-failure', $arguments); } } ================================================ FILE: src/Plugins/Shard.php ================================================ hasArgument('--shard', $arguments)) { return $arguments; } // @phpstan-ignore-next-line $input = new ArgvInput($arguments); ['index' => $index, 'total' => $total] = self::getShard($input); $arguments = $this->popArgument("--shard=$index/$total", $this->popArgument('--shard', $this->popArgument( "$index/$total", $arguments, ))); /** @phpstan-ignore-next-line */ $tests = $this->allTests($arguments); $testsToRun = (array_chunk($tests, max(1, (int) ceil(count($tests) / $total))))[$index - 1] ?? []; self::$shard = [ 'index' => $index, 'total' => $total, 'testsRan' => count($testsToRun), 'testsCount' => count($tests), ]; return [...$arguments, '--filter', $this->buildFilterArgument($testsToRun)]; } /** * Returns all tests that the test suite would run. * * @param list $arguments * @return list */ private function allTests(array $arguments): array { $output = (new Process([ 'php', ...$this->removeParallelArguments($arguments), '--list-tests', ]))->mustRun()->getOutput(); preg_match_all('/ - (?:P\\\\)?(Tests\\\\[^:]+)::/', $output, $matches); return array_values(array_unique($matches[1])); } /** * @param array $arguments * @return array */ private function removeParallelArguments(array $arguments): array { return array_filter($arguments, fn (string $argument): bool => ! in_array($argument, ['--parallel', '-p'], strict: true)); } /** * Builds the filter argument for the given tests to run. */ private function buildFilterArgument(mixed $testsToRun): string { return addslashes(implode('|', $testsToRun)); } /** * Adds output after the Test Suite execution. */ public function addOutput(int $exitCode): int { if (self::$shard === null) { return $exitCode; } [ 'index' => $index, 'total' => $total, 'testsRan' => $testsRan, 'testsCount' => $testsCount, ] = self::$shard; $this->output->writeln(sprintf( ' Shard: %d of %d — %d file%s ran, out of %d.', $index, $total, $testsRan, $testsRan === 1 ? '' : 's', $testsCount, )); return $exitCode; } /** * Returns the shard information. * * @return array{index: int, total: int} */ public static function getShard(InputInterface $input): array { if ($input->hasParameterOption('--'.self::SHARD_OPTION)) { $shard = $input->getParameterOption('--'.self::SHARD_OPTION); } else { $shard = null; } if (! is_string($shard) || ! preg_match('/^\d+\/\d+$/', $shard)) { throw new InvalidOption('The [--shard] option must be in the format "index/total".'); } [$index, $total] = explode('/', $shard); if (! is_numeric($index) || ! is_numeric($total)) { throw new InvalidOption('The [--shard] option must be in the format "index/total".'); } if ($index <= 0 || $total <= 0 || $index > $total) { throw new InvalidOption('The [--shard] option index must be a non-negative integer less than the total number of shards.'); } $index = (int) $index; $total = (int) $total; return [ 'index' => $index, 'total' => $total, ]; } } ================================================ FILE: src/Plugins/Snapshot.php ================================================ hasArgument('--update-snapshots', $arguments)) { return $arguments; } if ($this->hasArgument('--parallel', $arguments)) { throw new InvalidOption('The [--update-snapshots] option is not supported when running in parallel.'); } TestSuite::getInstance()->snapshots->flush(); return $this->popArgument('--update-snapshots', $arguments); } } ================================================ FILE: src/Plugins/Verbose.php ================================================ hasArgument('-'.$level, $arguments)) { $arguments = $this->popArgument('-'.$level, $arguments); } } if ($this->hasArgument('--quiet', $arguments)) { return $this->popArgument('--quiet', $arguments); } return $arguments; } } ================================================ FILE: src/Plugins/Version.php ================================================ hasArgument('--version', $arguments)) { View::render('version', [ 'version' => version(), ]); exit(0); } return $arguments; } } ================================================ FILE: src/Preset.php ================================================ */ private static ?array $baseNamespaces = null; /** * The custom presets. * * @var array */ private static array $customPresets = []; /** * Creates a new preset instance. */ public function __construct() { // } /** * Uses the Pest php preset and returns the test call instance. */ public function php(): Php { return $this->executePreset(new Php($this->baseNamespaces())); } /** * Uses the Pest laravel preset and returns the test call instance. */ public function laravel(): Laravel { return $this->executePreset(new Laravel($this->baseNamespaces())); } /** * Uses the Pest strict preset and returns the test call instance. */ public function strict(): Strict { return $this->executePreset(new Strict($this->baseNamespaces())); } /** * Uses the Pest security preset and returns the test call instance. */ public function security(): AbstractPreset { return $this->executePreset(new Security($this->baseNamespaces())); } /** * Uses the Pest relaxed preset and returns the test call instance. */ public function relaxed(): AbstractPreset { return $this->executePreset(new Relaxed($this->baseNamespaces())); } /** * Uses the Pest custom preset and returns the test call instance. * * @internal */ public static function custom(string $name, Closure $execute): void { if (preg_match('/^[a-zA-Z]+$/', $name) === false) { throw new InvalidArgumentException('The preset name must only contain words from a-z or A-Z.'); } self::$customPresets[$name] = $execute; } /** * Dynamically handle calls to the class. * * @param array $arguments * * @throws InvalidArgumentException */ public function __call(string $name, array $arguments): AbstractPreset { if (! array_key_exists($name, self::$customPresets)) { $availablePresets = [ ...['php', 'laravel', 'strict', 'security', 'relaxed'], ...array_keys(self::$customPresets), ]; throw new InvalidArgumentException(sprintf('The preset [%s] does not exist. The available presets are [%s].', $name, implode(', ', $availablePresets))); } return $this->executePreset(new Custom($this->baseNamespaces(), $name, self::$customPresets[$name])); } /** * Executes the given preset. * * @template TPreset of AbstractPreset * * @param TPreset $preset * @return TPreset */ private function executePreset(AbstractPreset $preset): AbstractPreset { $this->baseNamespaces(); $preset->execute(); // $this->testCall->testCaseMethod->closure = (function () use ($preset): void { // $preset->flush(); // })->bindTo(new stdClass); return $preset; } /** * Get the base namespaces for the application / package. * * @return array */ private function baseNamespaces(): array { if (self::$baseNamespaces === null) { self::$baseNamespaces = Composer::userNamespaces(); } return self::$baseNamespaces; } } ================================================ FILE: src/Repositories/AfterAllRepository.php ================================================ */ private array $state = []; /** * Runs the given closure for each after all. */ public function each(callable $each): void { foreach ($this->state as $filename => $closure) { $each($filename, $closure); } } /** * Sets a after all closure. */ public function set(Closure $closure): void { $filename = Reflection::getFileNameFromClosure($closure); if (array_key_exists($filename, $this->state)) { throw new AfterAllAlreadyExist($filename); } $this->state[$filename] = $closure; } /** * Gets a after all closure by the given filename. */ public function get(string $filename): Closure { return $this->state[$filename] ?? NullClosure::create(); } } ================================================ FILE: src/Repositories/AfterEachRepository.php ================================================ */ private array $state = []; /** * Sets a after each closure. */ public function set(string $filename, AfterEachCall $afterEachCall, Closure $afterEachTestCase): void { if (array_key_exists($filename, $this->state)) { $fromAfterEachTestCase = $this->state[$filename]; $afterEachTestCase = ChainableClosure::bound($fromAfterEachTestCase, $afterEachTestCase) ->bindTo($afterEachCall, $afterEachCall::class); } assert($afterEachTestCase instanceof Closure); $this->state[$filename] = $afterEachTestCase; } /** * Gets an after each closure by the given filename. */ public function get(string $filename): Closure { $afterEach = $this->state[$filename] ?? NullClosure::create(); return ChainableClosure::bound(function (): void { if (class_exists(Mockery::class)) { if ($container = Mockery::getContainer()) { /* @phpstan-ignore-next-line */ $this->addToAssertionCount($container->mockery_getExpectationCount()); } Mockery::close(); } }, $afterEach); } } ================================================ FILE: src/Repositories/BeforeAllRepository.php ================================================ */ private array $state = []; /** * Runs one before all closure, and unsets it from the repository. */ public function pop(string $filename): Closure { $closure = $this->get($filename); unset($this->state[$filename]); return $closure; } /** * Sets a before all closure. */ public function set(Closure $closure): void { $filename = Reflection::getFileNameFromClosure($closure); if (array_key_exists($filename, $this->state)) { throw new BeforeAllAlreadyExist($filename); } $this->state[$filename] = $closure; } /** * Gets a before all closure by the given filename. */ public function get(string $filename): Closure { return $this->state[$filename] ?? NullClosure::create(); } } ================================================ FILE: src/Repositories/BeforeEachRepository.php ================================================ */ private array $state = []; /** * Sets a before each closure. */ public function set(string $filename, BeforeEachCall $beforeEachCall, Closure $beforeEachTestCall, Closure $beforeEachTestCase): void { if (array_key_exists($filename, $this->state)) { [$fromBeforeEachTestCall, $fromBeforeEachTestCase] = $this->state[$filename]; $beforeEachTestCall = ChainableClosure::unbound($fromBeforeEachTestCall, $beforeEachTestCall); $beforeEachTestCase = ChainableClosure::bound($fromBeforeEachTestCase, $beforeEachTestCase)->bindTo($beforeEachCall, $beforeEachCall::class); assert($beforeEachTestCase instanceof Closure); } $this->state[$filename] = [$beforeEachTestCall, $beforeEachTestCase]; } /** * Gets a before each closure by the given filename. * * @return array{0: Closure, 1: Closure} */ public function get(string $filename): array { $closures = $this->state[$filename] ?? []; return [ $closures[0] ?? NullClosure::create(), $closures[1] ?? NullClosure::create(), ]; } } ================================================ FILE: src/Repositories/DatasetsRepository.php ================================================ >'; /** * Holds the datasets. * * @var array> */ private static array $datasets = []; /** * Holds the withs. * * @var array|string>> */ private static array $withs = []; /** * Sets the given. * * @param Closure|iterable $data */ public static function set(string $name, Closure|iterable $data, string $scope): void { $datasetKey = "$scope".self::SEPARATOR."$name"; if (array_key_exists("$datasetKey", self::$datasets)) { throw new DatasetAlreadyExists($name, $scope); } self::$datasets[$datasetKey] = $data; } /** * Sets the given "with". * * @param array|string> $with */ public static function with(string $filename, string $description, array $with): void { self::$withs["$filename".self::SEPARATOR."$description"] = $with; } public static function has(string $filename, string $description): bool { return array_key_exists($filename.self::SEPARATOR.$description, self::$withs); } /** * @return array * * @throws ShouldNotHappen */ public static function get(string $filename, string $description): array // @phpstan-ignore-line { $dataset = self::$withs[$filename.self::SEPARATOR.$description]; $dataset = self::resolve($dataset, $filename); if ($dataset === null) { throw ShouldNotHappen::fromMessage('Dataset [%s] not resolvable.'); } return $dataset; } /** * Resolves the current dataset to an array value. * * @param array|string> $dataset * @return array|null */ public static function resolve(array $dataset, string $currentTestFile): ?array { if ($dataset === []) { return null; } $dataset = self::processDatasets($dataset, $currentTestFile); $datasetCombinations = self::getDatasetsCombinations($dataset); $datasetDescriptions = []; $datasetValues = []; foreach ($datasetCombinations as $datasetCombination) { $partialDescriptions = []; $values = []; foreach ($datasetCombination as $datasetCombinationElement) { $partialDescriptions[] = $datasetCombinationElement['label']; $values = array_merge($values, $datasetCombinationElement['values']); } $datasetDescriptions[] = implode(' / ', $partialDescriptions); $datasetValues[] = $values; } foreach (array_count_values($datasetDescriptions) as $descriptionToCheck => $count) { if ($count > 1) { $index = 1; foreach ($datasetDescriptions as $i => $datasetDescription) { if ($datasetDescription === $descriptionToCheck) { $datasetDescriptions[$i] .= sprintf(' #%d', $index++); } } } } $namedData = []; foreach ($datasetDescriptions as $i => $datasetDescription) { $namedData[$datasetDescription] = $datasetValues[$i]; } return $namedData; } /** * @param array|string> $datasets * @return array> */ private static function processDatasets(array $datasets, string $currentTestFile): array { $processedDatasets = []; foreach ($datasets as $index => $data) { $processedDataset = []; if (is_string($data)) { $datasets[$index] = self::getScopedDataset($data, $currentTestFile); } if (is_callable($datasets[$index])) { $datasets[$index] = call_user_func($datasets[$index]); } if ($datasets[$index] instanceof Traversable) { $preserveKeysForArrayIterator = $datasets[$index] instanceof Generator && is_string($datasets[$index]->key()); $datasets[$index] = iterator_to_array($datasets[$index], $preserveKeysForArrayIterator); } foreach ($datasets[$index] as $key => $values) { $values = is_array($values) ? $values : [$values]; $processedDataset[] = [ 'label' => self::getDatasetDescription($key, $values), 'values' => $values, ]; } $processedDatasets[] = $processedDataset; } return $processedDatasets; } /** * @return Closure|iterable */ private static function getScopedDataset(string $name, string $currentTestFile): Closure|iterable { $matchingDatasets = array_filter(self::$datasets, function (string $key) use ($name, $currentTestFile): bool { [$datasetScope, $datasetName] = explode(self::SEPARATOR, $key); if ($name !== $datasetName) { return false; } return str_starts_with($currentTestFile, $datasetScope); }, ARRAY_FILTER_USE_KEY); /** @var string|null $closestScopeDatasetKey */ $closestScopeDatasetKey = array_reduce( array_keys($matchingDatasets), fn (string|int|null $keyA, string|int|null $keyB): string|int|null => $keyA !== null && strlen((string) $keyA) > strlen((string) $keyB) ? $keyA : $keyB ); if ($closestScopeDatasetKey === null) { throw new DatasetDoesNotExist($name); } return $matchingDatasets[$closestScopeDatasetKey]; } /** * @param array> $combinations * @return array>> */ private static function getDatasetsCombinations(array $combinations): array { $result = [[]]; foreach ($combinations as $index => $values) { $tmp = []; foreach ($result as $resultItem) { foreach ($values as $value) { $tmp[] = array_merge($resultItem, [$index => $value]); } } $result = $tmp; } return $result; } /** * @param array $data */ private static function getDatasetDescription(int|string $key, array $data): string { $exporter = Exporter::default(); if (is_int($key)) { return sprintf('(%s)', $exporter->shortenedRecursiveExport($data)); } return sprintf('dataset "%s"', $key); } } ================================================ FILE: src/Repositories/SnapshotRepository.php ================================================ */ private static array $expectationsCounter = []; /** * Creates a snapshot repository instance. */ public function __construct( private readonly string $rootPath, private readonly string $testsPath, private readonly string $snapshotsPath, ) {} /** * Checks if the snapshot exists. */ public function has(): bool { return file_exists($this->getSnapshotFilename()); } /** * Gets the snapshot. * * @return array{0: string, 1: string} * * @throws ShouldNotHappen */ public function get(): array { $contents = file_get_contents($snapshotFilename = $this->getSnapshotFilename()); if ($contents === false) { throw ShouldNotHappen::fromMessage('Snapshot file could not be read.'); } $snapshot = str_replace(dirname($this->testsPath).'/', '', $snapshotFilename); return [$snapshot, $contents]; } /** * Saves the given snapshot for the given test case. */ public function save(string $snapshot): string { $snapshotFilename = $this->getSnapshotFilename(); if (! file_exists(dirname($snapshotFilename))) { mkdir(dirname($snapshotFilename), 0755, true); } file_put_contents($snapshotFilename, $snapshot); return str_replace(dirname($this->testsPath).'/', '', $snapshotFilename); } /** * Flushes the snapshots. */ public function flush(): void { $absoluteSnapshotsPath = $this->testsPath.'/'.$this->snapshotsPath; $deleteDirectory = function (string $path) use (&$deleteDirectory): void { if (file_exists($path)) { $scannedDir = scandir($path); assert(is_array($scannedDir)); $files = array_diff($scannedDir, ['.', '..']); foreach ($files as $file) { if (is_dir($path.'/'.$file)) { $deleteDirectory($path.'/'.$file); } else { unlink($path.'/'.$file); } } rmdir($path); } }; if (file_exists($absoluteSnapshotsPath)) { $deleteDirectory($absoluteSnapshotsPath); } } /** * Gets the snapshot's "filename". */ private function getSnapshotFilename(): string { $testFile = TestSuite::getInstance()->getFilename(); if (str_starts_with($testFile, $this->testsPath)) { // if the test file is in the tests directory $startPath = $this->testsPath; } else { // if the test file is in the app, src, etc. directory $startPath = $this->rootPath; } // relative path: we use substr() and not str_replace() to remove the start path // for instance, if the $startPath is /app/ and the $testFile is /app/app/tests/Unit/ExampleTest.php, we should only remove the first /app/ from the path $relativePath = substr($testFile, strlen($startPath)); // remove extension from filename $relativePath = substr($relativePath, 0, (int) strrpos($relativePath, '.')); $description = TestSuite::getInstance()->getDescription(); if ($this->getCurrentSnapshotCounter() > 1) { $description .= '__'.$this->getCurrentSnapshotCounter(); } return sprintf('%s/%s.snap', $this->testsPath.'/'.$this->snapshotsPath.$relativePath, $description); } private function getCurrentSnapshotKey(): string { return TestSuite::getInstance()->getFilename().'###'.TestSuite::getInstance()->getDescription(); } private function getCurrentSnapshotCounter(): int { return self::$expectationsCounter[$this->getCurrentSnapshotKey()] ?? 0; } public function startNewExpectation(): void { $key = $this->getCurrentSnapshotKey(); if (! isset(self::$expectationsCounter[$key])) { self::$expectationsCounter[$key] = 0; } self::$expectationsCounter[$key]++; } } ================================================ FILE: src/Repositories/TestRepository.php ================================================ */ private array $testCases = []; /** * @var array, 1: array, 2: array>}> */ private array $uses = []; /** * @var array */ private array $testCaseFilters = []; /** * @var array */ private array $testCaseMethodFilters = []; /** * Counts the number of test cases. */ public function count(): int { return count($this->testCases); } /** * Returns the filename of each test that should be executed in the suite. * * @return array */ public function getFilenames(): array { return array_values(array_map(static fn (TestCaseFactory $factory): string => $factory->filename, $this->testCases)); } /** * Uses the given `$testCaseClass` on the given `$paths`. * * @param array $classOrTraits * @param array $groups * @param array $paths * @param array $hooks */ public function use(array $classOrTraits, array $groups, array $paths, array $hooks): void { foreach ($classOrTraits as $classOrTrait) { if (class_exists($classOrTrait)) { continue; } if (trait_exists($classOrTrait)) { continue; } throw new TestCaseClassOrTraitNotFound($classOrTrait); } $hooks = array_map(fn (Closure $hook): array => [$hook], $hooks); foreach ($paths as $path) { if (array_key_exists($path, $this->uses)) { $this->uses[$path] = [ [...$this->uses[$path][0], ...$classOrTraits], [...$this->uses[$path][1], ...$groups], array_map( fn (int $index): array => [...$this->uses[$path][2][$index] ?? [], ...($hooks[$index] ?? [])], range(0, 3), ), ]; } else { $this->uses[$path] = [$classOrTraits, $groups, $hooks]; } } } /** * Filters the test cases using the given filter. */ public function addTestCaseFilter(TestCaseFilter $filter): void { $this->testCaseFilters[] = $filter; } /** * Filters the test cases using the given filter. */ public function addTestCaseMethodFilter(TestCaseMethodFilter $filter): void { $this->testCaseMethodFilters[] = $filter; } /** * Gets the test case factory from the given filename. */ public function get(string $filename): ?TestCaseFactory { return $this->testCases[$filename] ?? null; } /** * Sets a new test case method. */ public function set(TestCaseMethodFactory $method): void { foreach ($this->testCaseFilters as $filter) { if (! $filter->accept($method->filename)) { return; } } foreach ($this->testCaseMethodFilters as $filter) { if (! $filter->accept($method)) { return; } } if (! array_key_exists($method->filename, $this->testCases)) { $this->testCases[$method->filename] = new TestCaseFactory($method->filename); } $this->testCases[$method->filename]->addMethod($method); } /** * Makes a Test Case from the given filename, if exists. */ public function makeIfNeeded(string $filename): void { if (! array_key_exists($filename, $this->testCases)) { return; } foreach ($this->testCaseFilters as $filter) { if (! $filter->accept($filename)) { return; } } $this->make($this->testCases[$filename]); } /** * Makes a Test Case using the given factory. */ private function make(TestCaseFactory $testCase): void { $startsWith = static fn (string $target, string $directory): bool => Str::startsWith($target, $directory.DIRECTORY_SEPARATOR); foreach ($this->uses as $path => $uses) { [$classOrTraits, $groups, $hooks] = $uses; if ((! is_dir($path) && $testCase->filename === $path) || (is_dir($path) && $startsWith($testCase->filename, $path))) { foreach ($classOrTraits as $class) { /** @var string $class */ if (class_exists($class)) { if ($testCase->class !== TestCase::class) { throw new TestCaseAlreadyInUse($testCase->class, $class, $testCase->filename); } $testCase->class = $class; } elseif (trait_exists($class)) { $testCase->traits[] = $class; } } foreach ($testCase->methods as $method) { foreach ($groups as $group) { $method->attributes[] = new Attribute( Group::class, [$group], ); } } foreach ($testCase->methods as $method) { $method->groups = [...$groups, ...$method->groups]; } foreach (['__addBeforeAll', '__addBeforeEach', '__addAfterEach', '__addAfterAll'] as $index => $name) { foreach ($hooks[$index] ?? [null] as $hook) { $testCase->factoryProxies->add($testCase->filename, 0, $name, [$hook]); } } } } $testCase->make(); } } ================================================ FILE: src/Result.php ================================================ calculate($configuration, $result); } } ================================================ FILE: src/Runner/Filter/EnsureTestCaseIsInitiatedFilter.php ================================================ $iterator */ public function __construct(RecursiveIterator $iterator) { parent::__construct($iterator); } /** * {@inheritdoc} */ public function accept(): bool { $test = $this->getInnerIterator()->current(); if ($test instanceof HasPrintableTestCaseName) { /** @phpstan-ignore-next-line */ $test->__initializeTestCase(); } return true; } } ================================================ FILE: src/Subscribers/EnsureConfigurationIsAvailable.php ================================================ add(Configuration::class, $event->configuration()); } } ================================================ FILE: src/Subscribers/EnsureIgnorableTestCasesAreIgnored.php ================================================ getProperty('collector'); $collector = $property->getValue(); assert($collector instanceof Collector); $reflection = new ReflectionClass($collector); $property = $reflection->getProperty('testRunnerTriggeredWarningEvents'); /** @var array $testRunnerTriggeredWarningEvents */ $testRunnerTriggeredWarningEvents = $property->getValue($collector); $testRunnerTriggeredWarningEvents = array_values(array_filter($testRunnerTriggeredWarningEvents, fn (WarningTriggered $event): bool => str_contains($event->message(), 'No tests found in class') === false)); $property->setValue($collector, $testRunnerTriggeredWarningEvents); } } ================================================ FILE: src/Subscribers/EnsureKernelDumpIsFlushed.php ================================================ get(KernelDump::class); assert($kernelDump instanceof KernelDump); $kernelDump->disable(); } } ================================================ FILE: src/Subscribers/EnsureTeamCityEnabled.php ================================================ input->hasParameterOption('--teamcity')) { return; } $flowId = getenv('FLOW_ID'); $flowId = is_string($flowId) ? (int) $flowId : getmypid(); new TeamCityLogger( $this->output, new Converter($this->testSuite->rootPath), $flowId === false ? null : $flowId, getenv('COLLISION_IGNORE_DURATION') !== false ); } } ================================================ FILE: src/Support/Arr.php ================================================ $array */ public static function has(array $array, string|int $key): bool { $key = (string) $key; if (array_key_exists($key, $array)) { return true; } foreach (explode('.', $key) as $segment) { if (is_array($array) && array_key_exists($segment, $array)) { $array = $array[$segment]; } else { return false; } } return true; } /** * Gets the given key value. * * @param array $array */ public static function get(array $array, string|int $key, mixed $default = null): mixed { $key = (string) $key; if (array_key_exists($key, $array)) { return $array[$key]; } if (! str_contains($key, '.')) { return $array[$key] ?? $default; } foreach (explode('.', $key) as $segment) { if (is_array($array) && array_key_exists($segment, $array)) { $array = $array[$segment]; } else { return $default; } } return $array; } /** * Flatten a multi-dimensional associative array with dots. * * @param array $array * @return array */ public static function dot(array $array, string $prepend = ''): array { $results = []; foreach ($array as $key => $value) { if (is_array($value) && $value !== []) { $results = array_merge($results, self::dot($value, $prepend.$key.'.')); } else { $results[$prepend.$value] = $value; } } return $results; } /** * Returns the value of the last element or false for empty array * * @param array $array */ public static function last(array $array): mixed { return end($array); } } ================================================ FILE: src/Support/Backtrace.php ================================================ */ private array $instances = []; /** * Gets a new or already existing container. */ public static function getInstance(): self { if (! self::$instance instanceof Container) { self::$instance = new self; } return self::$instance; } /** * Gets a dependency from the container. */ public function get(string $id): object|string { if (! array_key_exists($id, $this->instances)) { /** @var class-string $id */ $this->instances[$id] = $this->build($id); } return $this->instances[$id]; } /** * Adds the given instance to the container. * * @return $this */ public function add(string $id, object|string $instance): self { $this->instances[$id] = $instance; return $this; } /** * Tries to build the given instance. * * @template TObject of object * * @param class-string $id * @return TObject */ private function build(string $id): object { $reflectionClass = new ReflectionClass($id); if ($reflectionClass->isInstantiable()) { $constructor = $reflectionClass->getConstructor(); if ($constructor instanceof \ReflectionMethod) { $params = array_map( function (ReflectionParameter $param) use ($id): object|string { $candidate = Reflection::getParameterClassName($param); if ($candidate === null) { $type = $param->getType(); /* @phpstan-ignore-next-line */ if ($type instanceof \ReflectionType && $type->isBuiltin()) { $candidate = $param->getName(); } else { throw ShouldNotHappen::fromMessage(sprintf('The type of `$%s` in `%s` cannot be determined.', $id, $param->getName())); } } return $this->get($candidate); }, $constructor->getParameters() ); return $reflectionClass->newInstanceArgs($params); } return $reflectionClass->newInstance(); } throw ShouldNotHappen::fromMessage(sprintf('A dependency with the name `%s` cannot be resolved.', $id)); } } ================================================ FILE: src/Support/Coverage.php ================================================ canCollectCodeCoverage()) { return false; } if ($runtime->hasPCOV()) { return true; } if ($runtime->hasPHPDBGCodeCoverage()) { return true; } if (! $runtime->hasXdebug()) { return true; } if (! version_compare((string) phpversion('xdebug'), '3.1', '>=')) { return true; } return in_array('coverage', xdebug_info('mode'), true); } /** * If the user is using Xdebug. */ public static function usingXdebug(): bool { return (new Runtime)->hasXdebug(); } /** * Reports the code coverage report to the * console and returns the result in float. */ public static function report(OutputInterface $output, bool $compact = false): float { if (! file_exists($reportPath = self::getPath())) { if (self::usingXdebug()) { $output->writeln( " WARN Unable to get coverage using Xdebug. Did you set Xdebug's coverage mode?", ); return 0.0; } throw ShouldNotHappen::fromMessage(sprintf('Coverage not found in path: %s.', $reportPath)); } /** @var CodeCoverage $codeCoverage */ $codeCoverage = require $reportPath; unlink($reportPath); $totalCoverage = $codeCoverage->getReport()->percentageOfExecutedLines(); /** @var Directory $report */ $report = $codeCoverage->getReport(); foreach ($report->getIterator() as $file) { if (! $file instanceof File) { continue; } $dirname = dirname($file->id()); $basename = basename($file->id(), '.php'); $name = $dirname === '.' ? $basename : implode(DIRECTORY_SEPARATOR, [ $dirname, $basename, ]); $percentage = $file->numberOfExecutableLines() === 0 ? '100.0' : number_format($file->percentageOfExecutedLines()->asFloat(), 1, '.', ''); if ($percentage === '100.0' && $compact) { continue; } $uncoveredLines = ''; $percentageOfExecutedLinesAsString = $file->percentageOfExecutedLines()->asString(); if (! in_array($percentageOfExecutedLinesAsString, ['0.00%', '100.00%', '100.0%', ''], true)) { $uncoveredLines = trim(implode(', ', self::getMissingCoverage($file))); $uncoveredLines = sprintf('%s', $uncoveredLines).' / '; } $color = $percentage === '100.0' ? 'green' : ($percentage === '0.0' ? 'red' : 'yellow'); $truncateAt = max(1, terminal()->width() - 12); renderUsing($output); render(<< {$name} $uncoveredLines {$percentage}% HTML); } $totalCoverageAsString = $totalCoverage->asFloat() === 0.0 ? '0.0' : number_format(floor($totalCoverage->asFloat() * 10) / 10, 1, '.', ''); renderUsing($output); render(<<
Total: {$totalCoverageAsString} %
HTML); return $totalCoverage->asFloat(); } /** * Generates an array of missing coverage on the following format:. * * ``` * ['11', '20..25', '50', '60..80']; * ``` * * * @param File $file * @return array */ public static function getMissingCoverage(mixed $file): array { $shouldBeNewLine = true; $eachLine = function (array $array, array $tests, int $line) use (&$shouldBeNewLine): array { if ($tests !== []) { $shouldBeNewLine = true; return $array; } if ($shouldBeNewLine) { $array[] = (string) $line; $shouldBeNewLine = false; return $array; } $lastKey = count($array) - 1; if (array_key_exists($lastKey, $array) && str_contains((string) $array[$lastKey], '..')) { [$from] = explode('..', (string) $array[$lastKey]); $array[$lastKey] = $line > $from ? sprintf('%s..%s', $from, $line) : sprintf('%s..%s', $line, $from); return $array; } $array[$lastKey] = sprintf('%s..%s', $array[$lastKey], $line); return $array; }; $array = []; foreach (array_filter($file->lineCoverageData(), is_array(...)) as $line => $tests) { $array = $eachLine($array, $tests, $line); } return $array; } } ================================================ FILE: src/Support/DatasetInfo.php ================================================ description; } } ================================================ FILE: src/Support/ExceptionTrace.php ================================================ getMessage(), self::UNDEFINED_METHOD)) { $class = preg_match('/^Call to undefined method ([^:]+)::/', $message, $matches) === false ? null : $matches[1]; $message = str_replace(self::UNDEFINED_METHOD, 'Call to undefined method ', $message); if (class_exists((string) $class) && (is_countable(class_parents($class)) ? count(class_parents($class)) : 0) > 0 && array_values(class_parents($class))[0] === TestCase::class) { // @phpstan-ignore-line $message .= '. Did you forget to use the [pest()->extend()] function? Read more at: https://pestphp.com/docs/configuring-tests'; } Reflection::setPropertyValue($throwable, 'message', $message); } throw $throwable; } } } ================================================ FILE: src/Support/ExpectationPipeline.php ================================================ */ private array $pipes = []; /** * The list of passables. * * @var array */ private array $passables; /** * Creates a new instance of Expectation Pipeline. */ public function __construct( private readonly Closure $closure ) {} /** * Creates a new instance of Expectation Pipeline with given closure. */ public static function for(Closure $closure): self { return new self($closure); } /** * Sets the list of passables. */ public function send(mixed ...$passables): self { $this->passables = $passables; return $this; } /** * Sets the list of pipes. * * @param array $pipes */ public function through(array $pipes): self { $this->pipes = $pipes; return $this; } /** * Runs the pipeline. */ public function run(): void { $pipeline = array_reduce( array_reverse($this->pipes), $this->carry(), function (): void { call_user_func_array($this->closure, $this->passables); } ); $pipeline(); } /** * Get a Closure that will carry of the expectation. */ public function carry(): Closure { return fn (mixed $stack, callable $pipe): Closure => fn () => $pipe($stack, ...$this->passables); } } ================================================ FILE: src/Support/Exporter.php ================================================ $data */ public function shortenedRecursiveExport(array &$data, ?Context $context = null): string { $result = []; $array = $data; $itemsCount = 0; $exporter = self::default(); $context ??= new Context; $context->add($data); foreach ($array as $key => $value) { if (++$itemsCount > self::MAX_ARRAY_ITEMS) { $result[] = '…'; break; } if (! is_array($value)) { $result[] = $exporter->shortenedExport($value); continue; } $result[] = $context->contains($data[$key]) !== false ? '*RECURSION*' // @phpstan-ignore-next-line : sprintf('[%s]', $this->shortenedRecursiveExport($data[$key], $context)); } return implode(', ', $result); } /** * Exports a value into a single-line string. */ public function shortenedExport(mixed $value): string { $map = [ '#\.{3}#' => '…', '#\\\n\s*#' => '', '# Object \(…\)#' => '', ]; return (string) preg_replace(array_keys($map), array_values($map), $this->exporter->shortenedExport($value)); } } ================================================ FILE: src/Support/HigherOrderCallables.php ================================================ */ public function expect(mixed $value): Expectation { /** @var TValue $value */ $value = $value instanceof Closure ? Reflection::bindCallableWithData($value) : $value; return new Expectation($value); } /** * @template TValue * * Create a new expectation. Callable values will be executed prior to returning the new expectation. * * @param callable|TValue $value * @return Expectation<(callable(): mixed)|TValue> */ public function and(mixed $value): Expectation { // @phpstan-ignore-next-line return $this->expect($value); } /** * Execute the given callable after the test has executed the setup method. */ public function defer(callable $callable): object { Reflection::bindCallableWithData($callable); return $this->target; } } ================================================ FILE: src/Support/HigherOrderMessage.php ================================================ |null $arguments */ public function __construct( public string $filename, public int $line, public string $name, public ?array $arguments ) { // .. } /** * Re-throws the given `$throwable` with the good line and filename. * * @template TValue of object * * @param TValue $target */ public function call(object $target): mixed { if (is_callable($this->condition) && call_user_func(Closure::bind($this->condition, $target)) === false) { return $target; } if ($this->hasHigherOrderCallable()) { return (new HigherOrderCallables($target))->{$this->name}(...$this->arguments); } try { return is_array($this->arguments) ? Reflection::call($target, $this->name, $this->arguments) : $target->{$this->name}; } catch (Throwable $throwable) { Reflection::setPropertyValue($throwable, 'file', $this->filename); Reflection::setPropertyValue($throwable, 'line', $this->line); if ($throwable->getMessage() === $this->getUndefinedMethodMessage($target, $this->name)) { /** @var ReflectionClass $reflection */ $reflection = new ReflectionClass($target); $reflection = $reflection->getParentClass() ?: $reflection; Reflection::setPropertyValue($throwable, 'message', sprintf('Call to undefined method %s::%s()', $reflection->getName(), $this->name)); } throw $throwable; } } /** * Indicates that this message should only be called when the given condition is true. * * @param callable(): bool $condition */ public function when(callable $condition): self { $this->condition = Closure::fromCallable($condition); return $this; } /** * Determines whether or not there exists a higher order callable with the message name. */ private function hasHigherOrderCallable(): bool { return in_array($this->name, get_class_methods(HigherOrderCallables::class), true); } private function getUndefinedMethodMessage(object $target, string $methodName): string { return sprintf(self::UNDEFINED_METHOD, sprintf('%s::%s()', $target::class, $methodName)); } } ================================================ FILE: src/Support/HigherOrderMessageCollection.php ================================================ */ private array $messages = []; /** * Adds a new higher order message to the collection. * * @param array|null $arguments */ public function add(string $filename, int $line, string $name, ?array $arguments): void { $this->messages[] = new HigherOrderMessage($filename, $line, $name, $arguments); } /** * Adds a new higher order message to the collection if the callable condition is does not return false. * * @param array|null $arguments */ public function addWhen(callable $condition, string $filename, int $line, string $name, ?array $arguments): void { $this->messages[] = (new HigherOrderMessage($filename, $line, $name, $arguments))->when($condition); } /** * Proxy all the messages starting from the target. */ public function chain(object $target): void { foreach ($this->messages as $message) { $target = $message->call($target) ?? $target; } } /** * Proxy all the messages to the target. */ public function proxy(object $target): void { foreach ($this->messages as $message) { $message->call($target); } } /** * Count the number of messages with the given name. * * @param string $name A higher order message name (usually a method name) */ public function count(string $name): int { return array_reduce( $this->messages, static fn (int $total, HigherOrderMessage $message): int => $total + (int) ($name === $message->name), 0, ); } } ================================================ FILE: src/Support/HigherOrderTapProxy.php ================================================ target->{$property} = $value; } /** * Dynamically pass properties gets to the target. */ public function __get(string $property): mixed { if (property_exists($this->target, $property)) { return $this->target->{$property}; } $className = (new ReflectionClass($this->target))->getName(); if (str_starts_with($className, 'P\\')) { $className = substr($className, 2); } trigger_error(sprintf('Undefined property %s::$%s', $className, $property), E_USER_WARNING); return null; } /** * Dynamically pass method calls to the target. * * @param array $arguments * @return mixed */ public function __call(string $methodName, array $arguments) { $filename = Backtrace::file(); $line = Backtrace::line(); return (new HigherOrderMessage($filename, $line, $methodName, $arguments)) ->call($this->target); } } ================================================ FILE: src/Support/NullClosure.php ================================================ $args */ public static function call(object $object, string $method, array $args = []): mixed { $reflectionClass = new ReflectionClass($object); try { $reflectionMethod = $reflectionClass->getMethod($method); return $reflectionMethod->invoke($object, ...$args); } catch (ReflectionException $exception) { if (method_exists($object, '__call')) { return $object->__call($method, $args); } if (is_callable($method)) { return self::bindCallable($method, $args); } throw $exception; } } /** * Bind a callable to the TestCase and return the result. * * @param array $args */ public static function bindCallable(callable $callable, array $args = []): mixed { return Closure::fromCallable($callable)->bindTo(TestSuite::getInstance()->test)(...$args); } /** * Bind a callable to the TestCase and return the result, * passing in the current dataset values as arguments. */ public static function bindCallableWithData(callable $callable): mixed { $test = TestSuite::getInstance()->test; if (! $test instanceof TestCase) { return self::bindCallable($callable); } foreach ($test->providedData() as $value) { if ($value instanceof Closure) { throw new InvalidArgumentException('Bound datasets are not supported while doing high order testing.'); } } return Closure::fromCallable($callable)->bindTo($test)(...$test->providedData()); } /** * Infers the file name from the given closure. */ public static function getFileNameFromClosure(Closure $closure): string { $reflectionClosure = new ReflectionFunction($closure); return (string) $reflectionClosure->getFileName(); } /** * Gets the property value from of the given object. */ public static function getPropertyValue(object $object, string $property): mixed { $reflectionClass = new ReflectionClass($object); $reflectionProperty = null; while (! $reflectionProperty instanceof ReflectionProperty) { try { /* @var ReflectionProperty $reflectionProperty */ $reflectionProperty = $reflectionClass->getProperty($property); } catch (ReflectionException $reflectionException) { $reflectionClass = $reflectionClass->getParentClass(); if (! $reflectionClass instanceof ReflectionClass) { throw new ShouldNotHappen($reflectionException); } } } return $reflectionProperty->getValue($object); } /** * Sets the property value of the given object. * * @template TValue of object * * @param TValue $object */ public static function setPropertyValue(object $object, string $property, mixed $value): void { /** @var ReflectionClass $reflectionClass */ $reflectionClass = new ReflectionClass($object); $reflectionProperty = null; while (! $reflectionProperty instanceof ReflectionProperty) { try { /* @var ReflectionProperty $reflectionProperty */ $reflectionProperty = $reflectionClass->getProperty($property); } catch (ReflectionException $reflectionException) { $reflectionClass = $reflectionClass->getParentClass(); if (! $reflectionClass instanceof ReflectionClass) { throw new ShouldNotHappen($reflectionException); } } } $reflectionProperty->setValue($object, $value); } /** * Get the class name of the given parameter's type, if possible. * * @see https://github.com/laravel/framework/blob/v6.18.25/src/Illuminate/Support/Reflector.php */ public static function getParameterClassName(ReflectionParameter $parameter): ?string { $type = $parameter->getType(); if (! $type instanceof ReflectionNamedType) { return null; } if ($type->isBuiltin()) { return null; } $name = $type->getName(); if (($class = $parameter->getDeclaringClass()) instanceof ReflectionClass) { if ($name === 'self') { return $class->getName(); } if ($name === 'parent' && ($parent = $class->getParentClass()) instanceof ReflectionClass) { return $parent->getName(); } } return $name; } /** * Receive a map of function argument names to their types. * * @return array */ public static function getFunctionArguments(Closure $function): array { $parameters = (new ReflectionFunction($function))->getParameters(); $arguments = []; foreach ($parameters as $parameter) { /** @var ReflectionNamedType|ReflectionUnionType|null $types */ $types = ($parameter->hasType()) ? $parameter->getType() : null; if (is_null($types)) { $arguments[$parameter->getName()] = 'mixed'; continue; } $arguments[$parameter->getName()] = implode('|', array_map( static fn (ReflectionNamedType $type): string => $type->getName(), // @phpstan-ignore-line ($types instanceof ReflectionNamedType) ? [$types] // NOTE: normalize as list of to handle unions : $types->getTypes(), )); } return $arguments; } public static function getFunctionVariable(Closure $function, string $key): mixed { return (new ReflectionFunction($function))->getStaticVariables()[$key] ?? null; } /** * Get the properties from the given reflection class. * * Used by `expect()->toHavePropertiesDocumented()`. * * @param ReflectionClass $reflectionClass * @return array */ public static function getPropertiesFromReflectionClass(ReflectionClass $reflectionClass): array { $getProperties = fn (ReflectionClass $reflectionClass): array => array_filter( array_map( fn (ReflectionProperty $property): ReflectionProperty => $property, $reflectionClass->getProperties(), ), fn (ReflectionProperty $property): bool => $property->getDeclaringClass()->getName() === $reflectionClass->getName(), ); $propertiesFromTraits = []; foreach ($reflectionClass->getTraits() as $trait) { $propertiesFromTraits = array_merge($propertiesFromTraits, $getProperties($trait)); } $propertiesFromTraits = array_map( fn (ReflectionProperty $property): string => $property->getName(), $propertiesFromTraits, ); return array_values( array_filter( $getProperties($reflectionClass), fn (ReflectionProperty $property): bool => ! in_array($property->getName(), $propertiesFromTraits, true), ), ); } /** * Get the methods from the given reflection class. * * Used by `expect()->toHaveMethodsDocumented()`. * * @param ReflectionClass $reflectionClass * @return array */ public static function getMethodsFromReflectionClass(ReflectionClass $reflectionClass, int $filter = ReflectionMethod::IS_PUBLIC | ReflectionMethod::IS_PROTECTED | ReflectionMethod::IS_PRIVATE): array { $getMethods = fn (ReflectionClass $reflectionClass): array => array_filter( array_map( fn (ReflectionMethod $method): ReflectionMethod => $method, $reflectionClass->getMethods($filter), ), fn (ReflectionMethod $method): bool => $method->getDeclaringClass()->getName() === $reflectionClass->getName(), ); $methodsFromTraits = []; foreach ($reflectionClass->getTraits() as $trait) { $methodsFromTraits = array_merge($methodsFromTraits, $getMethods($trait)); } $methodsFromTraits = array_map( fn (ReflectionMethod $method): string => $method->getName(), $methodsFromTraits, ); return array_values( array_filter( $getMethods($reflectionClass), fn (ReflectionMethod $method): bool => ! in_array($method->getName(), $methodsFromTraits, true), ), ); } } ================================================ FILE: src/Support/Shell.php ================================================ setUpdateCheck(Checker::NEVER); $config->getPresenter()->addCasters(self::casters()); $shell = new PsyShell($config); $loader = self::tinkered($shell); try { $shell->run(); } finally { $loader?->unregister(); // @phpstan-ignore-line } } /** * Returns the casters for the Psy Shell. * * @return array */ private static function casters(): array { $casters = [ 'Illuminate\Support\Collection' => 'Laravel\Tinker\TinkerCaster::castCollection', 'Illuminate\Support\HtmlString' => 'Laravel\Tinker\TinkerCaster::castHtmlString', 'Illuminate\Support\Stringable' => 'Laravel\Tinker\TinkerCaster::castStringable', ]; if (class_exists('Illuminate\Database\Eloquent\Model')) { $casters['Illuminate\Database\Eloquent\Model'] = 'Laravel\Tinker\TinkerCaster::castModel'; } if (class_exists('Illuminate\Process\ProcessResult')) { $casters['Illuminate\Process\ProcessResult'] = 'Laravel\Tinker\TinkerCaster::castProcessResult'; } if (class_exists('Illuminate\Foundation\Application')) { $casters['Illuminate\Foundation\Application'] = 'Laravel\Tinker\TinkerCaster::castApplication'; } if (function_exists('app') === false) { return $casters; // @phpstan-ignore-line } $config = app()->make('config'); return array_merge($casters, (array) $config->get('tinker.casters', [])); } /** * Tinkers the current shell, if the Tinker package is available. */ private static function tinkered(PsyShell $shell): ?object { if (function_exists('app') === false || ! class_exists(Env::class) || ! class_exists(ClassAliasAutoloader::class) ) { return null; } $path = Env::get('COMPOSER_VENDOR_DIR', app()->basePath().DIRECTORY_SEPARATOR.'vendor'); $path .= '/composer/autoload_classmap.php'; if (! file_exists($path)) { $path = TestSuite::getInstance()->rootPath.DIRECTORY_SEPARATOR.'vendor'.DIRECTORY_SEPARATOR.'composer'.DIRECTORY_SEPARATOR.'autoload_classmap.php'; } $config = app()->make('config'); return ClassAliasAutoloader::register( $shell, $path, $config->get('tinker.alias', []), $config->get('tinker.dont_alias', []) ); } } ================================================ FILE: src/Support/StateGenerator.php ================================================ testErroredEvents() as $testResultEvent) { if ($testResultEvent instanceof Errored) { $state->add(TestResult::fromPestParallelTestCase( $testResultEvent->test(), TestResult::FAIL, $testResultEvent->throwable() )); } else { // @phpstan-ignore-next-line $state->add(TestResult::fromBeforeFirstTestMethodErrored($testResultEvent)); } } foreach ($testResult->testFailedEvents() as $testResultEvent) { $state->add(TestResult::fromPestParallelTestCase( $testResultEvent->test(), TestResult::FAIL, $testResultEvent->throwable() )); } foreach ($testResult->testMarkedIncompleteEvents() as $testResultEvent) { $state->add(TestResult::fromPestParallelTestCase( $testResultEvent->test(), TestResult::INCOMPLETE, $testResultEvent->throwable() )); } foreach ($testResult->testConsideredRiskyEvents() as $riskyEvents) { foreach ($riskyEvents as $riskyEvent) { $state->add(TestResult::fromPestParallelTestCase( $riskyEvent->test(), TestResult::RISKY, ThrowableBuilder::from(new TestOutcome($riskyEvent->message())) )); } } foreach ($testResult->testSkippedEvents() as $testResultEvent) { if ($testResultEvent->message() === '__TODO__') { $state->add(TestResult::fromPestParallelTestCase($testResultEvent->test(), TestResult::TODO)); continue; } $state->add(TestResult::fromPestParallelTestCase( $testResultEvent->test(), TestResult::SKIPPED, ThrowableBuilder::from(new SkippedWithMessageException($testResultEvent->message())) )); } foreach ($testResult->deprecations() as $testResultEvent) { foreach ($testResultEvent->triggeringTests() as $triggeringTest) { ['test' => $test] = $triggeringTest; $state->add(TestResult::fromPestParallelTestCase( $test, TestResult::DEPRECATED, ThrowableBuilder::from(new TestOutcome($testResultEvent->description())) )); } } foreach ($testResult->phpDeprecations() as $testResultEvent) { foreach ($testResultEvent->triggeringTests() as $triggeringTest) { ['test' => $test] = $triggeringTest; $state->add(TestResult::fromPestParallelTestCase( $test, TestResult::DEPRECATED, ThrowableBuilder::from(new TestOutcome($testResultEvent->description())) )); } } foreach ($testResult->notices() as $testResultEvent) { foreach ($testResultEvent->triggeringTests() as $triggeringTest) { ['test' => $test] = $triggeringTest; $state->add(TestResult::fromPestParallelTestCase( $test, TestResult::NOTICE, ThrowableBuilder::from(new TestOutcome($testResultEvent->description())) )); } } foreach ($testResult->phpNotices() as $testResultEvent) { foreach ($testResultEvent->triggeringTests() as $triggeringTest) { ['test' => $test] = $triggeringTest; $state->add(TestResult::fromPestParallelTestCase( $test, TestResult::NOTICE, ThrowableBuilder::from(new TestOutcome($testResultEvent->description())) )); } } foreach ($testResult->warnings() as $testResultEvent) { foreach ($testResultEvent->triggeringTests() as $triggeringTest) { ['test' => $test] = $triggeringTest; $state->add(TestResult::fromPestParallelTestCase( $test, TestResult::WARN, ThrowableBuilder::from(new TestOutcome($testResultEvent->description())) )); } } foreach ($testResult->phpWarnings() as $testResultEvent) { foreach ($testResultEvent->triggeringTests() as $triggeringTest) { ['test' => $test] = $triggeringTest; $state->add(TestResult::fromPestParallelTestCase( $test, TestResult::WARN, ThrowableBuilder::from(new TestOutcome($testResultEvent->description())) )); } } // for each test that passed, we need to add it to the state for ($i = 0; $i < $passedTests; $i++) { $state->add(TestResult::fromPestParallelTestCase( new TestMethod( "$i", // @phpstan-ignore-line '', // @phpstan-ignore-line '', // @phpstan-ignore-line 1, TestDoxBuilder::fromClassNameAndMethodName('', ''), // @phpstan-ignore-line MetadataCollection::fromArray([]), TestDataCollection::fromArray([]) ), TestResult::PASS )); } return $state; } } ================================================ FILE: src/Support/Str.php ================================================ 0; } /** * Creates a describe block as `$describeDescription` → `$testDescription` format. * * @param array $describeDescriptions */ public static function describe(array $describeDescriptions, string $testDescription): string { $descriptionComponents = [...$describeDescriptions, $testDescription]; return sprintf(str_repeat('`%s` → ', count($describeDescriptions)).'%s', ...$descriptionComponents); } /** * Determine if a given value is a valid URL. */ public static function isUrl(string $value): bool { return (bool) filter_var($value, FILTER_VALIDATE_URL); } /** * Converts the given `$target` to a URL-friendly "slug". */ public static function slugify(string $target): string { $target = preg_replace('/[^a-zA-Z0-9]+/', '-', $target); return strtolower(trim((string) $target, '-')); } } ================================================ FILE: src/Support/View.php ================================================ $data */ public static function render(string $path, array $data = []): void { $contents = self::compile($path, $data); $existing = Termwind::getRenderer(); renderUsing(self::$output); try { render($contents); } finally { renderUsing($existing); } } /** * Compiles the given view. * * @param array $data */ private static function compile(string $path, array $data): string { extract($data); ob_start(); $path = str_replace('.', '/', $path); include sprintf('%s/../../resources/views/%s.php', __DIR__, $path); $contents = ob_get_contents(); ob_clean(); return (string) $contents; } } ================================================ FILE: src/TestCaseFilters/GitDirtyTestCaseFilter.php ================================================ |null */ private ?array $changedFiles = null; /** * Creates a new instance of the filter. */ public function __construct(private readonly string $projectRoot) { // ... } /** * {@inheritdoc} */ public function accept(string $testCaseFilename): bool { if ($this->changedFiles === null) { $this->loadChangedFiles(); } assert(is_array($this->changedFiles)); $relativePath = str_replace($this->projectRoot, '', $testCaseFilename); $relativePath = str_replace(DIRECTORY_SEPARATOR, '/', $relativePath); if (str_starts_with($relativePath, '/')) { $relativePath = substr($relativePath, 1); } return in_array($relativePath, $this->changedFiles, true); } /** * Loads the changed files. */ private function loadChangedFiles(): void { $process = new Process(['git', 'status', '--short', '--', '*.php']); $process->run(); if (! $process->isSuccessful()) { throw new MissingDependency('Filter by dirty files', 'git'); } $output = preg_split('/\R+/', $process->getOutput(), flags: PREG_SPLIT_NO_EMPTY); assert(is_array($output)); $dirtyFiles = []; foreach ($output as $dirtyFile) { $dirtyFiles[substr($dirtyFile, 3)] = trim(substr($dirtyFile, 0, 3)); } $dirtyFiles = array_filter($dirtyFiles, fn (string $status): bool => $status !== 'D'); $dirtyFiles = array_map( fn (string $file, string $status): string => in_array($status, ['R', 'RM'], true) ? explode(' -> ', $file)[1] : $file, array_keys($dirtyFiles), $dirtyFiles, ); $dirtyFiles = array_filter( $dirtyFiles, fn (string $file): bool => str_starts_with('.'.DIRECTORY_SEPARATOR.$file, TestSuite::getInstance()->testPath) || str_starts_with($file, TestSuite::getInstance()->testPath) ); $dirtyFiles = array_values($dirtyFiles); if ($dirtyFiles === []) { Panic::with(new NoDirtyTestsFound); } $this->changedFiles = $dirtyFiles; } } ================================================ FILE: src/TestCaseMethodFilters/AssigneeTestCaseFilter.php ================================================ assignees, fn (string $assignee): bool => str_starts_with($assignee, $this->assignee)) !== []; } } ================================================ FILE: src/TestCaseMethodFilters/IssueTestCaseFilter.php ================================================ number, $factory->issues, true); } } ================================================ FILE: src/TestCaseMethodFilters/NotesTestCaseFilter.php ================================================ notes !== []; } } ================================================ FILE: src/TestCaseMethodFilters/PrTestCaseFilter.php ================================================ number, $factory->prs, true); } } ================================================ FILE: src/TestCaseMethodFilters/TodoTestCaseFilter.php ================================================ todo; } } ================================================ FILE: src/TestCases/IgnorableTestCase.php ================================================ beforeAll = new BeforeAllRepository; $this->beforeEach = new BeforeEachRepository; $this->tests = new TestRepository; $this->afterEach = new AfterEachRepository; $this->afterAll = new AfterAllRepository; $this->rootPath = (string) realpath($rootPath); $this->snapshots = new SnapshotRepository( $this->rootPath, implode(DIRECTORY_SEPARATOR, [$this->rootPath, $this->testPath]), implode(DIRECTORY_SEPARATOR, ['.pest', 'snapshots']), ); } /** * Returns the current instance of the test suite. */ public static function getInstance( ?string $rootPath = null, ?string $testPath = null, ): TestSuite { if (is_string($rootPath) && is_string($testPath)) { self::$instance = new TestSuite($rootPath, $testPath); foreach (Plugin::$callables as $callable) { $callable(); } return self::$instance; } if (! self::$instance instanceof self) { throw new InvalidPestCommand; } return self::$instance; } public function getFilename(): string { assert($this->test instanceof TestCase); return (fn () => self::$__filename)->call($this->test, $this->test::class); // @phpstan-ignore-line } public function getDescription(): string { assert($this->test instanceof TestCase); $description = str_replace('__pest_evaluable_', '', $this->test->name()); $datasetAsString = str_replace('__pest_evaluable_', '', Str::evaluable($this->test->dataSetAsString())); return str_replace(' ', '_', $description.$datasetAsString); } public function registerSnapshotChange(string $message): void { assert($this->test instanceof TestCase); (fn (): string => $this->__snapshotChanges[] = $message)->call($this->test, $this->test::class); // @phpstan-ignore-line } } ================================================ FILE: stubs/Browser.php ================================================ browse(function (Browser $browser) { $browser->visit('/{name}') ->assertSee('{name}'); }); }); ================================================ FILE: stubs/Dataset.php ================================================ get('/{name}'); $response->assertStatus(200); }); ================================================ FILE: stubs/Unit.php ================================================ toBeTrue(); }); ================================================ FILE: stubs/init/Feature/ExampleTest.php.stub ================================================ toBeTrue(); }); ================================================ FILE: stubs/init/Pest.php.stub ================================================ extend(TestCase::class)->in('Feature'); /* |-------------------------------------------------------------------------- | Expectations |-------------------------------------------------------------------------- | | When you're writing tests, you often need to check that values meet certain conditions. The | "expect()" function gives you access to a set of "expectations" methods that you can use | to assert different things. Of course, you may extend the Expectation API at any time. | */ expect()->extend('toBeOne', function () { return $this->toBe(1); }); /* |-------------------------------------------------------------------------- | Functions |-------------------------------------------------------------------------- | | While Pest is very powerful out-of-the-box, you may have some testing code specific to your | project that you don't want to repeat in every file. Here you can also expose helpers as | global functions to help you to reduce the number of lines of code in your test files. | */ function something() { // .. } ================================================ FILE: stubs/init/TestCase.php.stub ================================================ toBeTrue(); }); ================================================ FILE: stubs/init/phpunit.xml.stub ================================================ ./tests app src ================================================ FILE: stubs/init-laravel/Feature/ExampleTest.php.stub ================================================ get('/'); $response->assertStatus(200); }); ================================================ FILE: stubs/init-laravel/Pest.php.stub ================================================ extend(TestCase::class) // ->use(Illuminate\Foundation\Testing\RefreshDatabase::class) ->in('Feature'); /* |-------------------------------------------------------------------------- | Expectations |-------------------------------------------------------------------------- | | When you're writing tests, you often need to check that values meet certain conditions. The | "expect()" function gives you access to a set of "expectations" methods that you can use | to assert different things. Of course, you may extend the Expectation API at any time. | */ expect()->extend('toBeOne', function () { return $this->toBe(1); }); /* |-------------------------------------------------------------------------- | Functions |-------------------------------------------------------------------------- | | While Pest is very powerful out-of-the-box, you may have some testing code specific to your | project that you don't want to repeat in every file. Here you can also expose helpers as | global functions to help you to reduce the number of lines of code in your test files. | */ function something() { // .. } ================================================ FILE: stubs/init-laravel/TestCase.php.stub ================================================ toBeTrue(); }); ================================================ FILE: stubs/init-laravel/phpunit.xml.stub ================================================ tests/Unit tests/Feature app ================================================ FILE: tests/.cache/test-results ================================================ {"version":"pest_2.32.2","defects":[],"times":{"P\\Tests\\Playground::__pest_evaluable_basic":0.005}} ================================================ FILE: tests/.pest/snapshots/Features/Expect/toMatchSnapshot/_describable__→_multiple_snapshot_expectations_with_describe.snap ================================================ foo bar 1 ================================================ FILE: tests/.pest/snapshots/Features/Expect/toMatchSnapshot/_describable__→_multiple_snapshot_expectations_with_describe__2.snap ================================================ foo bar 2 ================================================ FILE: tests/.pest/snapshots/Features/Expect/toMatchSnapshot/_within_describe__→_pass_with_dataset_with_data_set____my_datas_set_value___.snap ================================================

Snapshot

================================================ FILE: tests/.pest/snapshots/Features/Expect/toMatchSnapshot/failures.snap ================================================

Snapshot

================================================ FILE: tests/.pest/snapshots/Features/Expect/toMatchSnapshot/failures_with_custom_message.snap ================================================

Snapshot

================================================ FILE: tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations.snap ================================================ foo bar 1 ================================================ FILE: tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations__2.snap ================================================ foo bar 2 ================================================ FILE: tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set___1__.snap ================================================ foo bar 1 ================================================ FILE: tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set___1____2.snap ================================================ foo bar 2 ================================================ FILE: tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set____bar___.snap ================================================ foo bar 1 ================================================ FILE: tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set____bar_____2.snap ================================================ foo bar 2 ================================================ FILE: tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set____baz___.snap ================================================ foo bar 1 ================================================ FILE: tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set____baz_____2.snap ================================================ foo bar 2 ================================================ FILE: tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set____foo___.snap ================================================ foo bar 1 ================================================ FILE: tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set____foo_____2.snap ================================================ foo bar 2 ================================================ FILE: tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___10__.snap ================================================ foo bar 1 ================================================ FILE: tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___10____2.snap ================================================ foo bar 2 ================================================ FILE: tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___1__.snap ================================================ foo bar 1 ================================================ FILE: tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___1____2.snap ================================================ foo bar 2 ================================================ FILE: tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___2__.snap ================================================ foo bar 1 ================================================ FILE: tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___2____2.snap ================================================ foo bar 2 ================================================ FILE: tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___3__.snap ================================================ foo bar 1 ================================================ FILE: tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___3____2.snap ================================================ foo bar 2 ================================================ FILE: tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___4__.snap ================================================ foo bar 1 ================================================ FILE: tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___4____2.snap ================================================ foo bar 2 ================================================ FILE: tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___5__.snap ================================================ foo bar 1 ================================================ FILE: tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___5____2.snap ================================================ foo bar 2 ================================================ FILE: tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___6__.snap ================================================ foo bar 1 ================================================ FILE: tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___6____2.snap ================================================ foo bar 2 ================================================ FILE: tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___7__.snap ================================================ foo bar 1 ================================================ FILE: tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___7____2.snap ================================================ foo bar 2 ================================================ FILE: tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___8__.snap ================================================ foo bar 1 ================================================ FILE: tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___8____2.snap ================================================ foo bar 2 ================================================ FILE: tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___9__.snap ================================================ foo bar 1 ================================================ FILE: tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_repeat_with_data_set___9____2.snap ================================================ foo bar 2 ================================================ FILE: tests/.pest/snapshots/Features/Expect/toMatchSnapshot/not_failures.snap ================================================

Snapshot

================================================ FILE: tests/.pest/snapshots/Features/Expect/toMatchSnapshot/pass.snap ================================================

Snapshot

================================================ FILE: tests/.pest/snapshots/Features/Expect/toMatchSnapshot/pass_using_pipes.snap ================================================ ================================================ FILE: tests/.pest/snapshots/Features/Expect/toMatchSnapshot/pass_with______toString_.snap ================================================

Snapshot

================================================ FILE: tests/.pest/snapshots/Features/Expect/toMatchSnapshot/pass_with__toArray_.snap ================================================ { "key": "
\n
\n
\n

Snapshot<\/h1>\n <\/div>\n <\/div>\n <\/div>" } ================================================ FILE: tests/.pest/snapshots/Features/Expect/toMatchSnapshot/pass_with__toSnapshot_.snap ================================================ { "key": "
\n
\n
\n

Snapshot<\/h1>\n <\/div>\n <\/div>\n <\/div>" } ================================================ FILE: tests/.pest/snapshots/Features/Expect/toMatchSnapshot/pass_with__toString_.snap ================================================

Snapshot

================================================ FILE: tests/.pest/snapshots/Features/Expect/toMatchSnapshot/pass_with_array.snap ================================================ { "key": "
\n
\n
\n

Snapshot<\/h1>\n <\/div>\n <\/div>\n <\/div>" } ================================================ FILE: tests/.pest/snapshots/Features/Expect/toMatchSnapshot/pass_with_dataset_with_data_set____my_datas_set_value___.snap ================================================

Snapshot

================================================ FILE: tests/.pest/snapshots/Visual/Collision/collision_with_data_set_________.snap ================================================ FAIL Tests\Fixtures\CollisionTest ⨯ error ✓ success ──────────────────────────────────────────────────────────────────────────── FAILED Tests\Fixtures\CollisionTest > error Exception error at tests/Fixtures/CollisionTest.php:4 1▕ skip(! isset($_SERVER['COLLISION_TEST'])); 6▕ 7▕ test('success', function () { 8▕ expect(true)->toBeTrue(); 9▕ })->skip(! isset($_SERVER['COLLISION_TEST'])); 1 tests/Fixtures/CollisionTest.php:4 Tests: 1 failed, 1 passed (1 assertions) ================================================ FILE: tests/.pest/snapshots/Visual/Help/visual_snapshot_of_help_command_output.snap ================================================ Pest Testing Framework 4.4.2. USAGE: pest [options] CONFIGURATION OPTIONS: --init ............................ Initialise a standard Pest configuration --bootstrap [file] ...... A PHP script that is included before the tests run -c|--configuration [file] ................. Read configuration from XML file --no-configuration ......... Ignore default configuration file (phpunit.xml) --extension [class] .. Register test runner extension with bootstrap [class] --no-extensions ..................... Do not register test runner extensions --include-path [path(s)] ..... Prepend PHP's include_path with given path(s) -d [key[=value]] ...................................... Sets a php.ini value --cache-directory [dir] ............................ Specify cache directory --generate-configuration Generate configuration file with suggested settings --migrate-configuration ....... Migrate configuration file to current format --generate-baseline [file] .................... Generate baseline for issues --use-baseline [file] ........................ Use baseline to ignore issues --ignore-baseline ..................... Do not use baseline to ignore issues SELECTION OPTIONS: --bail ........................... Stop execution upon first not-passed test --todos ........................ Output to standard output the list of todos --notes ......................... Output to standard output tests with notes --issue ........ Output to standard output tests with the given issue number --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) --list-groups ................................... List available test groups --group [name] .................. Only run tests from the specified group(s) --exclude-group [name] ........... Exclude tests from the specified group(s) --covers [name] ................. Only run tests that intend to cover [name] --uses [name] ..................... Only run tests that intend to use [name] --requires-php-extension [name] Only run tests that require PHP extension [name] --list-test-files ................................ List available test files --list-tests .......................................... List available tests --list-tests-xml [file] ................. List available tests in XML format --filter [pattern] ............................... Filter which tests to run --exclude-filter [pattern] .. Exclude tests for the specified filter pattern --test-suffix [suffixes] Only search for test in files with specified suffix(es). Default: Test.php,.phpt EXECUTION OPTIONS: --parallel ........................................... Run tests in parallel --update-snapshots Update snapshots for tests using the "toMatchSnapshot" expectation --globals-backup ................. Backup and restore $GLOBALS for each test --static-backup ......... Backup and restore static properties for each test --strict-coverage ................... Be strict about code coverage metadata --strict-global-state .............. Be strict about changes to global state --disallow-test-output ................. Be strict about output during tests --enforce-time-limit ................. Enforce time limit based on test size --default-time-limit [sec] Timeout in seconds for tests that have no declared size --do-not-report-useless-tests Do not report tests that do not test anything --stop-on-defect ... Stop after first error, failure, warning, or risky test --stop-on-error ..................................... Stop after first error --stop-on-failure ................................. Stop after first failure --stop-on-warning ................................. Stop after first warning --stop-on-risky ................................ Stop after first risky test --stop-on-deprecation ... Stop after first test that triggered a deprecation --stop-on-notice ............. Stop after first test that triggered a notice --stop-on-skipped ............................ Stop after first skipped test --stop-on-incomplete ...................... Stop after first incomplete test --fail-on-empty-test-suite Signal failure using shell exit code when no tests were run --fail-on-warning Signal failure using shell exit code when a warning was triggered --fail-on-risky Signal failure using shell exit code when a test was considered risky --fail-on-deprecation Signal failure using shell exit code when a deprecation was triggered --fail-on-phpunit-deprecation Signal failure using shell exit code when a PHPUnit deprecation was triggered --fail-on-phpunit-notice Signal failure using shell exit code when a PHPUnit notice was triggered --fail-on-phpunit-warning Signal failure using shell exit code when a PHPUnit warning was triggered --fail-on-notice Signal failure using shell exit code when a notice was triggered --fail-on-skipped Signal failure using shell exit code when a test was skipped --fail-on-incomplete Signal failure using shell exit code when a test was marked incomplete --fail-on-all-issues Signal failure using shell exit code when an issue is triggered --do-not-fail-on-empty-test-suite Do not signal failure using shell exit code when no tests were run --do-not-fail-on-warning Do not signal failure using shell exit code when a warning was triggered --do-not-fail-on-risky Do not signal failure using shell exit code when a test was considered risky --do-not-fail-on-deprecation Do not signal failure using shell exit code when a deprecation was triggered --do-not-fail-on-phpunit-deprecation Do not signal failure using shell exit code when a PHPUnit deprecation was triggered --do-not-fail-on-phpunit-notice Do not signal failure using shell exit code when a PHPUnit notice was triggered --do-not-fail-on-phpunit-warning Do not signal failure using shell exit code when a PHPUnit warning was triggered --do-not-fail-on-notice Do not signal failure using shell exit code when a notice was triggered --do-not-fail-on-skipped Do not signal failure using shell exit code when a test was skipped --do-not-fail-on-incomplete Do not signal failure using shell exit code when a test was marked incomplete --cache-result ............................ Write test results to cache file --do-not-cache-result .............. Do not write test results to cache file --order-by [order] Run tests in order: default|defects|depends|duration|no-depends|random|reverse|size --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") --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 --no-progress .................... Disable output of test execution progress --no-results ................................ Disable output of test results --no-output ............................................. Disable all output --display-incomplete .................. Display details for incomplete tests --display-skipped ........................ Display details for skipped tests --display-deprecations . Display details for deprecations triggered by tests --display-phpunit-deprecations .... Display details for PHPUnit deprecations --display-phpunit-notices .............. Display details for PHPUnit notices --display-errors ............. Display details for errors triggered by tests --display-notices ........... Display details for notices triggered by tests --display-warnings ......... Display details for warnings triggered by tests --display-all-issues ..... Display details for all issues that are triggered --reverse-list .............................. Print defects in reverse order --teamcity . Replace default progress and result output with TeamCity format --testdox ................ Replace default result output with TestDox format --testdox-summary Repeat TestDox output for tests with errors, failures, or issues --debug Replace default progress and result output with debugging information --with-telemetry Include telemetry information in debugging information output --compact ................ Replace default result output with Compact format LOGGING OPTIONS: --log-junit [file] .......... Write test results in JUnit XML format to file --log-otr [file] Write test results in Open Test Reporting XML format to file --include-git-information Include Git information in Open Test Reporting XML logfile --log-teamcity [file] ........ Write test results in TeamCity format to file --testdox-html [file] .. Write test results in TestDox format (HTML) to file --testdox-text [file] Write test results in TestDox format (plain text) to file --log-events-text [file] ............... Stream events as plain text to file --log-events-verbose-text [file] Stream events as plain text with extended information to file --no-logging ....... Ignore logging configured in the XML configuration file CODE COVERAGE OPTIONS: --coverage ..... Generate code coverage report and output to standard output --coverage --min Set the minimum required coverage percentage, and fail if not met --coverage-clover [file] Write code coverage report in Clover XML format to file --coverage-openclover [file] Write code coverage report in OpenClover XML format to file --coverage-cobertura [file] Write code coverage report in Cobertura XML format to file --coverage-crap4j [file] Write code coverage report in Crap4J XML format to file --coverage-html [dir] Write code coverage report in HTML format to directory --coverage-php [file] .......... Write serialized code coverage data to file --coverage-text=[file] Write code coverage report in text format to file [default: standard output] --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 MUTATION TESTING OPTIONS: --mutate .... Runs mutation testing, to understand the quality of your tests --mutate --parallel ...................... Runs mutation testing in parallel --mutate --min Set the minimum required mutation score, and fail if not met --mutate --id Run only the mutation with the given ID. But E.g. --id=ecb35ab30ffd3491. Note, you need to provide the same options as the original run --mutate --covered-only Only generate mutations for classes that are covered by tests --mutate --bail Stop mutation testing execution upon first untested or uncovered mutation --mutate --class Generate mutations for the given class(es). E.g. --class=App\\Models --mutate --ignore Ignore the given class(es) when generating mutations. E.g. --ignore=App\\Http\\Requests --mutate --clear-cache ............................ Clear the mutation cache --mutate --no-cache ............................... Clear the mutation cache --mutate --ignore-min-score-on-zero-mutations Ignore the minimum score requirement when there are no mutations --mutate --covered-only Only generate mutations for classes that are covered by tests --mutate --everything Generate mutations for all classes, even if they are not covered by tests --mutate --profile . Output to standard output the top ten slowest mutations --mutate --retry Run untested or uncovered mutations first and stop execution upon first error or failure --mutate --stop-on-uncovered Stop mutation testing execution upon first untested mutation --mutate --stop-on-untested Stop mutation testing execution upon first untested mutation PROFILING OPTIONS: --profile .............. Output to standard output the top ten slowest tests ================================================ FILE: tests/.pest/snapshots/Visual/Todo/todo.snap ================================================ TODO Tests\Features\BeforeEachProxiesToTestCallWithTodo - 4 todos ↓ is marked as todo 1 ↓ is marked as todo 2 ↓ is marked as todo 3 ↓ shouldBeMarkedAsTodo TODO Tests\Features\DatasetsTests - 1 todo ↓ forbids to define tests in Datasets dirs and Datasets.php files TODO Tests\Features\Describe - 5 todos ↓ todo ↓ todo on hook → should not fail ↓ todo on hook → should run ↓ todo on describe → should not fail ↓ todo on describe → should run TODO Tests\Features\Todo - 29 todos ↓ something todo later ↓ something todo later chained ↓ something todo later chained and with function body ↓ it may have an associated assignee [@nunomaduro] ↓ it may have an associated issue #1 ↓ it may have an associated PR #1 ↓ it may have an associated note // a note ↓ todo on describe → todo block → nested inside todo block → it should not execute ↓ todo on describe → todo block → nested inside todo block → it should set the note // hi ↓ todo on describe → todo block → describe with note → it should apply the note to a test without a todo // describe note ↓ todo on describe → todo block → describe with note → it should apply the note to a test with a todo // describe note ↓ todo on describe → todo block → describe with note → it should apply the note as well as the note from the test // describe note // test note ↓ todo on describe → todo block → describe with note → nested describe with note → it should apply all parent notes to a test without a todo // describe note // nested describe note ↓ todo on describe → todo block → describe with note → nested describe with note → it should apply all parent notes to a test with a todo // describe note // nested describe note ↓ todo on describe → todo block → describe with note → nested describe with note → it should apply all parent notes as well as the note from the test // describe note // nested describe note // test note ↓ todo on describe → todo block → it should not execute ↓ todo on describe with matching name → describe block → it should not execute ↓ todo on test after describe block ↓ todo with note on test after describe block // test note ↓ todo on beforeEach → todo block → nested inside todo block → it should not execute ↓ todo on beforeEach → todo block → describe with note → it should apply the note to a test without a todo // describe note ↓ todo on beforeEach → todo block → describe with note → it should apply the note to a test with a todo // describe note ↓ todo on beforeEach → todo block → describe with note → it should apply the note as well as the note from the test // describe note // test note ↓ todo on beforeEach → todo block → describe with note → nested describe with note → it should apply all parent notes to a test without a todo // describe note // nested describe note ↓ todo on beforeEach → todo block → describe with note → nested describe with note → it should apply all parent notes to a test with a todo // describe note // nested describe note ↓ todo on beforeEach → todo block → describe with note → nested describe with note → it should apply all parent notes as well as the note from the test // describe note // nested describe note // test note ↓ todo on beforeEach → todo block → it should not execute ↓ todo on test after describe block with beforeEach ↓ todo with note on test after describe block with beforeEach // test note PASS Tests\CustomTestCase\ChildTest ✓ override method PASS Tests\CustomTestCase\ExecutedTest ✓ that gets executed PASS Tests\CustomTestCase\ParentTest ✓ override method Tests: 39 todos, 3 passed (21 assertions) Duration: x.xxs ================================================ FILE: tests/.pest/snapshots/Visual/Todo/todo_in_parallel.snap ================================================ TODO Tests\Features\BeforeEachProxiesToTestCallWithTodo - 4 todos ↓ is marked as todo 1 ↓ is marked as todo 2 ↓ is marked as todo 3 ↓ shouldBeMarkedAsTodo TODO Tests\Features\DatasetsTests - 1 todo ↓ forbids to define tests in Datasets dirs and Datasets.php files TODO Tests\Features\Describe - 5 todos ↓ todo ↓ todo on hook → should not fail ↓ todo on hook → should run ↓ todo on describe → should not fail ↓ todo on describe → should run TODO Tests\Features\Todo - 29 todos ↓ something todo later ↓ something todo later chained ↓ something todo later chained and with function body ↓ it may have an associated assignee [@nunomaduro] ↓ it may have an associated issue #1 ↓ it may have an associated PR #1 ↓ it may have an associated note // a note ↓ todo on describe → todo block → nested inside todo block → it should not execute ↓ todo on describe → todo block → nested inside todo block → it should set the note // hi ↓ todo on describe → todo block → describe with note → it should apply the note to a test without a todo // describe note ↓ todo on describe → todo block → describe with note → it should apply the note to a test with a todo // describe note ↓ todo on describe → todo block → describe with note → it should apply the note as well as the note from the test // describe note // test note ↓ todo on describe → todo block → describe with note → nested describe with note → it should apply all parent notes to a test without a todo // describe note // nested describe note ↓ todo on describe → todo block → describe with note → nested describe with note → it should apply all parent notes to a test with a todo // describe note // nested describe note ↓ todo on describe → todo block → describe with note → nested describe with note → it should apply all parent notes as well as the note from the test // describe note // nested describe note // test note ↓ todo on describe → todo block → it should not execute ↓ todo on describe with matching name → describe block → it should not execute ↓ todo on test after describe block ↓ todo with note on test after describe block // test note ↓ todo on beforeEach → todo block → nested inside todo block → it should not execute ↓ todo on beforeEach → todo block → describe with note → it should apply the note to a test without a todo // describe note ↓ todo on beforeEach → todo block → describe with note → it should apply the note to a test with a todo // describe note ↓ todo on beforeEach → todo block → describe with note → it should apply the note as well as the note from the test // describe note // test note ↓ todo on beforeEach → todo block → describe with note → nested describe with note → it should apply all parent notes to a test without a todo // describe note // nested describe note ↓ todo on beforeEach → todo block → describe with note → nested describe with note → it should apply all parent notes to a test with a todo // describe note // nested describe note ↓ todo on beforeEach → todo block → describe with note → nested describe with note → it should apply all parent notes as well as the note from the test // describe note // nested describe note // test note ↓ todo on beforeEach → todo block → it should not execute ↓ todo on test after describe block with beforeEach ↓ todo with note on test after describe block with beforeEach // test note PASS Tests\CustomTestCase\ChildTest ✓ override method PASS Tests\CustomTestCase\ExecutedTest ✓ that gets executed PASS Tests\CustomTestCase\ParentTest ✓ override method Tests: 39 todos, 3 passed (21 assertions) Duration: x.xxs ================================================ FILE: tests/.pest/snapshots/Visual/Todo/todos.snap ================================================ TODO Tests\Features\BeforeEachProxiesToTestCallWithTodo - 4 todos ↓ is marked as todo 1 ↓ is marked as todo 2 ↓ is marked as todo 3 ↓ shouldBeMarkedAsTodo TODO Tests\Features\DatasetsTests - 1 todo ↓ forbids to define tests in Datasets dirs and Datasets.php files TODO Tests\Features\Describe - 5 todos ↓ todo ↓ todo on hook → should not fail ↓ todo on hook → should run ↓ todo on describe → should not fail ↓ todo on describe → should run TODO Tests\Features\Todo - 29 todos ↓ something todo later ↓ something todo later chained ↓ something todo later chained and with function body ↓ it may have an associated assignee [@nunomaduro] ↓ it may have an associated issue #1 ↓ it may have an associated PR #1 ↓ it may have an associated note // a note ↓ todo on describe → todo block → nested inside todo block → it should not execute ↓ todo on describe → todo block → nested inside todo block → it should set the note // hi ↓ todo on describe → todo block → describe with note → it should apply the note to a test without a todo // describe note ↓ todo on describe → todo block → describe with note → it should apply the note to a test with a todo // describe note ↓ todo on describe → todo block → describe with note → it should apply the note as well as the note from the test // describe note // test note ↓ todo on describe → todo block → describe with note → nested describe with note → it should apply all parent notes to a test without a todo // describe note // nested describe note ↓ todo on describe → todo block → describe with note → nested describe with note → it should apply all parent notes to a test with a todo // describe note // nested describe note ↓ todo on describe → todo block → describe with note → nested describe with note → it should apply all parent notes as well as the note from the test // describe note // nested describe note // test note ↓ todo on describe → todo block → it should not execute ↓ todo on describe with matching name → describe block → it should not execute ↓ todo on test after describe block ↓ todo with note on test after describe block // test note ↓ todo on beforeEach → todo block → nested inside todo block → it should not execute ↓ todo on beforeEach → todo block → describe with note → it should apply the note to a test without a todo // describe note ↓ todo on beforeEach → todo block → describe with note → it should apply the note to a test with a todo // describe note ↓ todo on beforeEach → todo block → describe with note → it should apply the note as well as the note from the test // describe note // test note ↓ todo on beforeEach → todo block → describe with note → nested describe with note → it should apply all parent notes to a test without a todo // describe note // nested describe note ↓ todo on beforeEach → todo block → describe with note → nested describe with note → it should apply all parent notes to a test with a todo // describe note // nested describe note ↓ todo on beforeEach → todo block → describe with note → nested describe with note → it should apply all parent notes as well as the note from the test // describe note // nested describe note // test note ↓ todo on beforeEach → todo block → it should not execute ↓ todo on test after describe block with beforeEach ↓ todo with note on test after describe block with beforeEach // test note PASS Tests\CustomTestCase\ChildTest ✓ override method PASS Tests\CustomTestCase\ExecutedTest ✓ that gets executed PASS Tests\CustomTestCase\ParentTest ✓ override method Tests: 39 todos, 3 passed (21 assertions) Duration: x.xxs ================================================ FILE: tests/.pest/snapshots/Visual/Todo/todos_in_parallel.snap ================================================ TODO Tests\Features\BeforeEachProxiesToTestCallWithTodo - 4 todos ↓ is marked as todo 1 ↓ is marked as todo 2 ↓ is marked as todo 3 ↓ shouldBeMarkedAsTodo TODO Tests\Features\DatasetsTests - 1 todo ↓ forbids to define tests in Datasets dirs and Datasets.php files TODO Tests\Features\Describe - 5 todos ↓ todo ↓ todo on hook → should not fail ↓ todo on hook → should run ↓ todo on describe → should not fail ↓ todo on describe → should run TODO Tests\Features\Todo - 29 todos ↓ something todo later ↓ something todo later chained ↓ something todo later chained and with function body ↓ it may have an associated assignee [@nunomaduro] ↓ it may have an associated issue #1 ↓ it may have an associated PR #1 ↓ it may have an associated note // a note ↓ todo on describe → todo block → nested inside todo block → it should not execute ↓ todo on describe → todo block → nested inside todo block → it should set the note // hi ↓ todo on describe → todo block → describe with note → it should apply the note to a test without a todo // describe note ↓ todo on describe → todo block → describe with note → it should apply the note to a test with a todo // describe note ↓ todo on describe → todo block → describe with note → it should apply the note as well as the note from the test // describe note // test note ↓ todo on describe → todo block → describe with note → nested describe with note → it should apply all parent notes to a test without a todo // describe note // nested describe note ↓ todo on describe → todo block → describe with note → nested describe with note → it should apply all parent notes to a test with a todo // describe note // nested describe note ↓ todo on describe → todo block → describe with note → nested describe with note → it should apply all parent notes as well as the note from the test // describe note // nested describe note // test note ↓ todo on describe → todo block → it should not execute ↓ todo on describe with matching name → describe block → it should not execute ↓ todo on test after describe block ↓ todo with note on test after describe block // test note ↓ todo on beforeEach → todo block → nested inside todo block → it should not execute ↓ todo on beforeEach → todo block → describe with note → it should apply the note to a test without a todo // describe note ↓ todo on beforeEach → todo block → describe with note → it should apply the note to a test with a todo // describe note ↓ todo on beforeEach → todo block → describe with note → it should apply the note as well as the note from the test // describe note // test note ↓ todo on beforeEach → todo block → describe with note → nested describe with note → it should apply all parent notes to a test without a todo // describe note // nested describe note ↓ todo on beforeEach → todo block → describe with note → nested describe with note → it should apply all parent notes to a test with a todo // describe note // nested describe note ↓ todo on beforeEach → todo block → describe with note → nested describe with note → it should apply all parent notes as well as the note from the test // describe note // nested describe note // test note ↓ todo on beforeEach → todo block → it should not execute ↓ todo on test after describe block with beforeEach ↓ todo with note on test after describe block with beforeEach // test note PASS Tests\CustomTestCase\ChildTest ✓ override method PASS Tests\CustomTestCase\ExecutedTest ✓ that gets executed PASS Tests\CustomTestCase\ParentTest ✓ override method Tests: 39 todos, 3 passed (21 assertions) Duration: x.xxs ================================================ FILE: tests/.pest/snapshots/Visual/Version/visual_snapshot_of_help_command_output.snap ================================================ Pest Testing Framework 4.4.2. ================================================ FILE: tests/.pest/snapshots-external/Features/Expect/toMatchSnapshot/_within_describe__→_pass_with_dataset___my_datas_set_value___with_data___my_datas_set_value__.snap ================================================

Snapshot

================================================ FILE: tests/.pest/snapshots-external/Features/Expect/toMatchSnapshot/_within_describe__→_pass_with_dataset_with_data_set_____my_datas_set_value___.snap ================================================

Snapshot

================================================ FILE: tests/.pest/snapshots-external/Features/Expect/toMatchSnapshot/_within_describe__→_pass_with_dataset_with_data_set____my_datas_set_value___.snap ================================================

Snapshot

================================================ FILE: tests/.pest/snapshots-external/Features/Expect/toMatchSnapshot/pass_with_dataset___my_datas_set_value___with_data___my_datas_set_value__.snap ================================================

Snapshot

================================================ FILE: tests/.pest/snapshots-external/Features/Expect/toMatchSnapshot/pass_with_dataset_with_data_set_____my_datas_set_value___.snap ================================================

Snapshot

================================================ FILE: tests/.pest/snapshots-external/Features/Expect/toMatchSnapshot/pass_with_dataset_with_data_set____my_datas_set_value___.snap ================================================

Snapshot

================================================ FILE: tests/.snapshots/Failure.php.inc ================================================ ##teamcity[testSuiteStarted name='Tests/tests/Failure' locationHint='pest_qn://tests/.tests/Failure.php' flowId='1234'] ##teamcity[testCount count='8' flowId='1234'] ##teamcity[testStarted name='it can fail with comparison' locationHint='pest_qn://tests/.tests/Failure.php::it can fail with comparison' flowId='1234'] ##teamcity[testFailed name='it can fail with comparison' message='Failed asserting that true matches expected false.' details='at tests/.tests/Failure.php:6' type='comparisonFailure' actual='true' expected='false' flowId='1234'] ##teamcity[testFinished name='it can fail with comparison' duration='100000' flowId='1234'] ##teamcity[testStarted name='it can be ignored because of no assertions' locationHint='pest_qn://tests/.tests/Failure.php::it can be ignored because of no assertions' flowId='1234'] ##teamcity[testIgnored name='it can be ignored because of no assertions' message='This test did not perform any assertions' details='' flowId='1234'] ##teamcity[testFinished name='it can be ignored because of no assertions' duration='100000' flowId='1234'] ##teamcity[testStarted name='it can be ignored because it is skipped' locationHint='pest_qn://tests/.tests/Failure.php::it can be ignored because it is skipped' flowId='1234'] ##teamcity[testIgnored name='it can be ignored because it is skipped' message='This test was ignored.' details='' flowId='1234'] ##teamcity[testFinished name='it can be ignored because it is skipped' duration='100000' flowId='1234'] ##teamcity[testStarted name='it can fail' locationHint='pest_qn://tests/.tests/Failure.php::it can fail' flowId='1234'] ##teamcity[testFailed name='it can fail' message='oh noo' details='at tests/.tests/Failure.php:18' flowId='1234'] ##teamcity[testFinished name='it can fail' duration='100000' flowId='1234'] ##teamcity[testStarted name='it throws exception' locationHint='pest_qn://tests/.tests/Failure.php::it throws exception' flowId='1234'] ##teamcity[testFailed name='it throws exception' message='Exception: test error' details='at tests/.tests/Failure.php:22' flowId='1234'] ##teamcity[testFinished name='it throws exception' duration='100000' flowId='1234'] ##teamcity[testStarted name='it is not done yet' locationHint='pest_qn://tests/.tests/Failure.php::it is not done yet' flowId='1234'] ##teamcity[testFinished name='it is not done yet' duration='100000' flowId='1234'] ##teamcity[testStarted name='build this one.' locationHint='pest_qn://tests/.tests/Failure.php::build this one.' flowId='1234'] ##teamcity[testFinished name='build this one.' duration='100000' flowId='1234'] ##teamcity[testStarted name='it is passing' locationHint='pest_qn://tests/.tests/Failure.php::it is passing' flowId='1234'] ##teamcity[testFinished name='it is passing' duration='100000' flowId='1234'] ##teamcity[testSuiteFinished name='Tests/tests/Failure' flowId='1234'] Tests: 3 failed, 1 risky, 2 todos, 1 skipped, 1 passed (3 assertions) Duration: 1.00s ================================================ FILE: tests/.snapshots/SuccessOnly.php.inc ================================================ ##teamcity[testSuiteStarted name='Tests/tests/SuccessOnly' locationHint='pest_qn://tests/.tests/SuccessOnly.php' flowId='1234'] ##teamcity[testCount count='4' flowId='1234'] ##teamcity[testStarted name='it can pass with comparison' locationHint='pest_qn://tests/.tests/SuccessOnly.php::it can pass with comparison' flowId='1234'] ##teamcity[testFinished name='it can pass with comparison' duration='100000' flowId='1234'] ##teamcity[testStarted name='can also pass' locationHint='pest_qn://tests/.tests/SuccessOnly.php::can also pass' flowId='1234'] ##teamcity[testFinished name='can also pass' duration='100000' flowId='1234'] ##teamcity[testSuiteStarted name='can pass with dataset' locationHint='pest_qn://tests/.tests/SuccessOnly.php::can pass with dataset' flowId='1234'] ##teamcity[testStarted name='can pass with dataset with data set "(true)"' locationHint='pest_qn://tests/.tests/SuccessOnly.php::can pass with dataset with data set "(true)"' flowId='1234'] ##teamcity[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: 4 passed (4 assertions) Duration: 1.00s ================================================ FILE: tests/.snapshots/allows-to-run-a-directory.txt ================================================ WARN Tests\Fixtures\CollisionTest - error - success PASS Tests\Fixtures\DirectoryWithTests\ExampleTest ✓ it example 1 PASS Tests\Fixtures\ExampleTest ✓ it example 2 WARN Tests\Fixtures\Inheritance\Base\ExampleTest - example PASS Tests\Fixtures\Inheritance\ExampleTest ✓ example Tests: 3 skipped, 3 passed (3 assertions) ================================================ FILE: tests/.snapshots/allows-to-run-a-single-test.txt ================================================ PASS Tests\Fixtures\DirectoryWithTests\ExampleTest ✓ it example 1 Tests: 1 passed (1 assertions) ================================================ FILE: tests/.snapshots/disable-decorating-printer.txt ================================================ PASS Tests\Fixtures\DirectoryWithTests\ExampleTest ✓ it example 1 Tests: 1 passed (1 assertions) ================================================ FILE: tests/.snapshots/success.txt ================================================ PASS Tests\Arch ✓ preset → php → ignoring ['Pest\Expectation', 'debug_backtrace', 'var_export', …] ✓ preset → strict → ignoring ['usleep'] ✓ preset → security → ignoring ['eval', 'str_shuffle', 'exec', …] ✓ globals ✓ contracts PASS Tests\Environments\Windows ✓ global functions are loaded WARN Tests\Features\After ✓ it can run after test ✓ it can run after test twice - it does not run when skipped - something → it does not run when skipped ✓ something → it can run after test ✓ something 2 → it can run after test ✓ high order test - high order test with skip ✓ post 'foo' → defer Closure Object () → expect Closure Object () → toBe 1 PASS Tests\Features\AfterAll ✓ deletes file after all PASS Tests\Features\AfterEach ✓ it does not get executed before the test ✓ it gets executed after the test ✓ outer → inner → it does not get executed before the test ✓ outer → inner → it should call all parent afterEach functions ✓ matching describe block names → outer → middle → inner → it does not get executed before the test ✓ matching describe block names → outer → middle → inner → it should call all parent afterEach functions ✓ matching describe block names → outer → middle → it does not get executed before the test ✓ matching describe block names → outer → middle → it should not call afterEach functions for sibling describe blocks with the same name ✓ matching describe block names → outer → inner → it does not get executed before the test ✓ matching describe block names → outer → inner → it should not call afterEach functions for descendent of sibling describe blocks with the same name PASS Tests\Features\Assignee ✓ it may be associated with an assignee [@nunomaduro, @taylorotwell] ✓ nested → it may be associated with an assignee [@nunomaduro, @jamesbrooks, @joedixon, @taylorotwell] // an note between an the assignee PASS Tests\Features\BeforeAll ✓ it gets executed before tests ✓ it do not get executed before each test PASS Tests\Features\BeforeEach ✓ it gets executed before each test ✓ it gets executed before each test once again ✓ outer → inner → it should call all parent beforeEach functions ✓ with expectations → nested block → test ✓ with expectations → test ✓ matching describe block names → outer → middle → inner → it should call all parent beforeEach functions ✓ matching describe block names → outer → middle → it should not call beforeEach functions for sibling describe blocks with the same name ✓ matching describe block names → outer → inner → it should not call beforeEach functions for descendent of sibling describe blocks with the same name ✓ matching name → it should call the before each ✓ matching name → it should not call the before each on the describe block with the same name ✓ called on all tests → beforeEach should be called ✓ called on all tests → beforeEach should be called for all tests PASS Tests\Features\BeforeEachProxiesToTestCallWithExpectations ✓ runs 1 ✓ runs 2 ✓ runs 3 WARN Tests\Features\BeforeEachProxiesToTestCallWithSkip - does not run 1 - does not run 2 - does not run 3 TODO Tests\Features\BeforeEachProxiesToTestCallWithTodo - 4 todos ↓ is marked as todo 1 ↓ is marked as todo 2 ↓ is marked as todo 3 ↓ shouldBeMarkedAsTodo WARN Tests\Features\Coverage ✓ it has plugin - it adds coverage if --coverage exist → Coverage is not available ✓ it adds coverage if --min exist ✓ it generates coverage based on file input PASS Tests\Features\Covers\ClassCoverage ✓ it uses the correct PHPUnit attribute for class PASS Tests\Features\Covers\ExceptionHandling ✓ it throws exception if no class nor method has been found PASS Tests\Features\Covers\FunctionCoverage ✓ it uses the correct PHPUnit attribute for function PASS Tests\Features\Covers\GuessCoverage ✓ it guesses if the given argument is a class or function PASS Tests\Features\Covers\TraitCoverage ✓ it uses the correct PHPUnit attribute for trait PASS Tests\Features\DatasetsTests - 1 todo ✓ it throws exception if dataset does not exist ✓ it throws exception if dataset already exist ✓ it sets closures ✓ it sets arrays ✓ it gets bound to test case object with ('a') ✓ it gets bound to test case object with ('b') ✓ it truncates the description with ('FoooFoooFoooFoooFoooFoooFoooF…ooFooo') ✓ lazy datasets with (1) ✓ lazy datasets with (2) ✓ lazy datasets did the job right ✓ interpolated (1) lazy datasets ✓ interpolated (2) lazy datasets ✓ eager datasets with (1) ✓ eager datasets with (2) ✓ eager datasets did the job right ✓ lazy registered datasets with (1) ✓ lazy registered datasets with (2) ✓ lazy registered datasets did the job right ✓ eager registered datasets with (1) ✓ eager registered datasets with (2) ✓ eager registered datasets did the job right ✓ eager wrapped registered datasets with (1) ✓ eager wrapped registered datasets with (2) ✓ eager registered wrapped datasets did the job right ✓ named datasets with dataset "one" ✓ named datasets with dataset "two" ✓ interpolated "one" named datasets ✓ interpolated "two" named datasets ✓ named datasets did the job right ✓ lazy named datasets with (Bar) ✓ it creates unique test case names with ('Name 1', Pest\Plugin, true) #1 ✓ it creates unique test case names with ('Name 1', Pest\Plugin, true) #2 ✓ it creates unique test case names with ('Name 1', Pest\Plugin, false) ✓ it creates unique test case names with ('Name 2', Pest\Plugin, false) ✓ it creates unique test case names with ('Name 2', Pest\Plugin, true) ✓ it creates unique test case names with ('Name 1', Pest\Plugin, true) #3 ✓ it creates unique test case names - count ✓ lazy multiple datasets with (1) / (3) ✓ lazy multiple datasets with (1) / (4) ✓ lazy multiple datasets with (2) / (3) ✓ lazy multiple datasets with (2) / (4) ✓ lazy multiple datasets did the job right ✓ eager multiple datasets with (1) / (3) ✓ eager multiple datasets with (1) / (4) ✓ eager multiple datasets with (2) / (3) ✓ eager multiple datasets with (2) / (4) ✓ eager multiple datasets did the job right ✓ lazy registered multiple datasets with (1) / (1) ✓ lazy registered multiple datasets with (1) / (2) ✓ lazy registered multiple datasets with (2) / (1) ✓ lazy registered multiple datasets with (2) / (2) ✓ lazy registered multiple datasets did the job right ✓ eager registered multiple datasets with (1) / (1) ✓ eager registered multiple datasets with (1) / (2) ✓ eager registered multiple datasets with (2) / (1) ✓ eager registered multiple datasets with (2) / (2) ✓ eager registered multiple datasets did the job right ✓ eager wrapped registered multiple datasets with (1) / (1) ✓ eager wrapped registered multiple datasets with (1) / (2) ✓ eager wrapped registered multiple datasets with (2) / (1) ✓ eager wrapped registered multiple datasets with (2) / (2) ✓ eager wrapped registered multiple datasets did the job right ✓ named multiple datasets with dataset "one" / dataset "three" ✓ named multiple datasets with dataset "one" / dataset "four" ✓ named multiple datasets with dataset "two" / dataset "three" ✓ named multiple datasets with dataset "two" / dataset "four" ✓ named multiple datasets did the job right ✓ more than two datasets with (1) / (3) / (5) ✓ more than two datasets with (1) / (3) / (6) ✓ more than two datasets with (1) / (4) / (5) ✓ more than two datasets with (1) / (4) / (6) ✓ more than two datasets with (2) / (3) / (5) ✓ more than two datasets with (2) / (3) / (6) ✓ more than two datasets with (2) / (4) / (5) ✓ more than two datasets with (2) / (4) / (6) ✓ more than two datasets did the job right ✓ eager registered wrapped datasets with Generator functions with (1) ✓ eager registered wrapped datasets with Generator functions with (2) ✓ eager registered wrapped datasets with Generator functions with (3) ✓ eager registered wrapped datasets with Generator functions with (4) ✓ eager registered wrapped datasets with Generator functions did the job right ✓ eager registered wrapped datasets with Generator functions display description with dataset "taylor" ✓ eager registered wrapped datasets with Generator functions display description with dataset "james" ✓ it can resolve a dataset after the test case is available with (Closure Object ()) #1 ✓ it can resolve a dataset after the test case is available with (Closure Object ()) #2 ✓ it can resolve a dataset after the test case is available with multiple datasets with (Closure Object ()) / (Closure Object ()) #1 ✓ it can resolve a dataset after the test case is available with multiple datasets with (Closure Object ()) / (Closure Object ()) #2 ✓ it can resolve a dataset after the test case is available with multiple datasets with (Closure Object ()) / (Closure Object ()) #3 ✓ it can resolve a dataset after the test case is available with multiple datasets with (Closure Object ()) / (Closure Object ()) #4 ✓ it can resolve a dataset after the test case is available with shared yield sets with (Closure Object ()) #1 ✓ it can resolve a dataset after the test case is available with shared yield sets with (Closure Object ()) #2 ✓ it can resolve a dataset after the test case is available with shared array sets with (Closure Object ()) #1 ✓ it can resolve a dataset after the test case is available with shared array sets with (Closure Object ()) #2 ✓ it resolves a potential bound dataset logically with ('foo', Closure Object ()) ✓ it resolves a potential bound dataset logically even when the closure comes first with (Closure Object (), 'bar') ✓ it will not resolve a closure if it is type hinted as a closure with (Closure Object ()) #1 ✓ it will not resolve a closure if it is type hinted as a closure with (Closure Object ()) #2 ✓ it will not resolve a closure if it is type hinted as a callable with (Closure Object ()) #1 ✓ it will not resolve a closure if it is type hinted as a callable with (Closure Object ()) #2 ✓ it can correctly resolve a bound dataset that returns an array with (Closure Object ()) ✓ it can correctly resolve a bound dataset that returns an array but wants to be spread with (Closure Object ()) ↓ forbids to define tests in Datasets dirs and Datasets.php files ✓ it may be used with high order with dataset "formal" ✓ it may be used with high order with dataset "informal" ✓ it may be used with high order even when bound with dataset "formal" ✓ it may be used with high order even when bound with dataset "informal" ✓ with on nested describe → nested → before inner describe block with (1) ✓ with on nested describe → nested → describe → it should include the with value from all parent describe blocks with (1) / (2) ✓ with on nested describe → nested → describe → should include the with value from all parent describe blocks and the test with (1) / (2) / (3) ✓ with on nested describe → nested → after inner describe block with (1) ✓ matching describe block names → outer → before inner describe block with (1) ✓ matching describe block names → outer → inner → it should include the with value from all parent describe blocks with (1) / (2) ✓ matching describe block names → outer → inner → should include the with value from all parent describe blocks and the test with (1) / (2) / (3) ✓ matching describe block names → outer → inner → it should not include the value from the other describe block with the same name with (1) ✓ matching describe block names → outer → after inner describe block with (1) ✓ after describe block with (5) ✓ it may be used with high order after describe block with dataset "formal" ✓ it may be used with high order after describe block with dataset "informal" ✓ after describe block with named dataset with ('after') PASS Tests\Features\Depends ✓ first ✓ second ✓ depends ✓ depends with ...params ✓ depends with defined arguments ✓ depends run test only once ✓ it asserts true is true ✓ depends works with the correct test name ✓ describe block → first in describe ✓ describe block → second in describe ✓ describe block → third in describe ✓ describe block → nested describe → first in nested describe ✓ describe block → nested describe → second in nested describe ✓ describe block → nested describe → third in nested describe ✓ depends on test after describe block PASS Tests\Features\DependsInheritance ✓ it is a test ✓ it uses correct parent class DEPR Tests\Features\Deprecated ! deprecated → str_contains(): Passing null to parameter #1 ($haystack) of type string is deprecated // tests/Features/Deprecated.php:4 ! user deprecated → Since foo 1.0: This is a deprecation description // tests/Features/Deprecated.php:10 PASS Tests\Features\Describe - 5 todos ✓ before each ✓ hooks → value ✓ hooks in different orders → value ↓ todo ✓ previous describable before each does not get applied here ↓ todo on hook → should not fail ↓ todo on hook → should run ↓ todo on describe → should not fail ↓ todo on describe → should run ✓ should run ✓ with with (1) ✓ with on hook → value with (2) ✓ with on describe → value with (3) ✓ depends on describe → foo ✓ depends on describe → bar ✓ depends on describe using with → foo with (3) ✓ depends on describe using with → bar with (3) ✓ with test after describe → it should run the before each PASS Tests\Features\DescriptionLess ✓ get 'foo' ✓ get 'foo' → get 'bar' → expect true → toBeTrue ✓ get 'foo' → expect true → toBeTrue ✓ a "describe" group of tests → get 'foo' ✓ a "describe" group of tests → get 'foo' → get 'bar' → expect true → toBeTrue ✓ a "describe" group of tests → get 'foo' → expect true → toBeTrue PASS Tests\Features\Done ✓ it may have an associated assignee [@nunomaduro] ✓ it may have an associated issue #1 ✓ it may have an associated PR #1 ✓ it may have an associated note // a note PASS Tests\Features\Exceptions ✓ it gives access the the underlying expectException ✓ it catch exceptions ✓ it catch exceptions and messages ✓ it catch exceptions, messages and code ✓ it can just define the message ✓ it can just define the code ✓ it not catch exceptions if given condition is false ✓ it catch exceptions if given condition is true ✓ it catch exceptions and messages if given condition is true ✓ it catch exceptions, messages and code if given condition is true ✓ it can just define the message if given condition is true ✓ it can just define the code if given condition is true ✓ it can just define the message if given condition is 1 ✓ it can just define the code if given condition is 1 ✓ it not catch exceptions if given condition is true ✓ it catch exceptions if given condition is false ✓ it catch exceptions and messages if given condition is false ✓ it catch exceptions, messages and code if given condition is false ✓ it can just define the message if given condition is false ✓ it can just define the code if given condition is false ✓ it can just define the message if given condition is 0 ✓ it can just define the code if given condition is 0 PASS Tests\Features\Expect\HigherOrder\methods ✓ it can access methods ✓ it can access multiple methods ✓ it works with not ✓ it can accept arguments ✓ it works with each ✓ it works inside of each ✓ it works with sequence ✓ it can compose complex expectations ✓ it can handle nested method calls ✓ it works with higher order tests ✓ it can use the scoped method to lock into the given level for expectations ✓ it works consistently with the json expectation method PASS Tests\Features\Expect\HigherOrder\methodsAndProperties ✓ it can access methods and properties ✓ it can handle nested methods and properties ✓ it works with higher order tests ✓ it can start a new higher order expectation using the and syntax ✓ it can start a new higher order expectation using the and syntax in higher order tests ✓ it can start a new higher order expectation using the and syntax without nesting expectations PASS Tests\Features\Expect\HigherOrder\properties ✓ it allows properties to be accessed from the value ✓ it can access multiple properties from the value ✓ it works with not ✓ it works with each ✓ it works inside of each ✓ it works with sequence ✓ it can compose complex expectations ✓ it works with objects ✓ it works with nested properties ✓ it works with higher order tests PASS Tests\Features\Expect\each ✓ an exception is thrown if the the type is not iterable ✓ it expects on each item ✓ it chains expectations on each item ✓ opposite expectations on each item ✓ chained opposite and non-opposite expectations ✓ it can add expectations via "and" ✓ it accepts callables ✓ it passes the key of the current item to callables PASS Tests\Features\Expect\extend ✓ it macros true is true ✓ it macros false is not true ✓ it macros true is true with argument ✓ it macros false is not true with argument PASS Tests\Features\Expect\json ✓ it properly parses json string ✓ fails with broken json string PASS Tests\Features\Expect\matchExpectation ✓ it pass ✓ it failures ✓ it runs with truthy ✓ it runs with falsy ✓ it runs with truthy closure condition ✓ it runs with falsy closure condition ✓ it can be passed non-callable values ✓ it fails with unhandled match ✓ it can be used in higher order tests PASS Tests\Features\Expect\not ✓ not property calls PASS Tests\Features\Expect\pipes ✓ pipe is applied and can stop pipeline ✓ pipe is run and can let the pipeline keep going ✓ pipe works with negated expectation ✓ interceptor is applied ✓ interceptor stops the pipeline ✓ interceptor is called only when filter is met ✓ interceptor can be filtered with a closure ✓ interceptor can be filter the expected parameter as well ✓ interceptor works with negated expectation ✓ intercept can add new parameters to the expectation PASS Tests\Features\Expect\ray ✓ ray calls do not fail when ray is not installed PASS Tests\Features\Expect\sequence ✓ an exception is thrown if the the type is not iterable ✓ an exception is thrown if there are no expectations ✓ allows for sequences of checks to be run on iterable data ✓ loops back to the start if it runs out of sequence items ✓ fails if the number of iterable items is less than the number of expectations ✓ it works with associative arrays ✓ it can be passed non-callable values ✓ it can be passed a mixture of value types ✓ it works with traversables PASS Tests\Features\Expect\toBe ✓ strict comparisons ✓ failures ✓ failures with custom message ✓ not failures PASS Tests\Features\Expect\toBeAlpha ✓ pass ✓ failures ✓ failures with custom message ✓ not failures PASS Tests\Features\Expect\toBeAlphaNumeric ✓ pass ✓ failures ✓ failures with custom message ✓ not failures PASS Tests\Features\Expect\toBeArray ✓ pass ✓ failures ✓ failures with custom message ✓ not failures PASS Tests\Features\Expect\toBeBetween ✓ passes with int ✓ passes with float ✓ passes with float and int ✓ passes with DateTime ✓ failure with int ✓ failure with float ✓ failure with float and int ✓ failure with DateTime ✓ failures with custom message ✓ not failures PASS Tests\Features\Expect\toBeBool ✓ pass ✓ failures ✓ failures with custom message ✓ not failures PASS Tests\Features\Expect\toBeCallable ✓ pass ✓ failures ✓ failures with custom message ✓ not failures PASS Tests\Features\Expect\toBeCamelCase ✓ pass ✓ failures ✓ failures with custom message ✓ not failures PASS Tests\Features\Expect\toBeDigits ✓ pass ✓ failures ✓ failures with custom message ✓ not failures PASS Tests\Features\Expect\toBeDirectory ✓ pass ✓ failures ✓ failures with custom message ✓ not failures PASS Tests\Features\Expect\toBeEmpty ✓ pass ✓ failures ✓ failures with custom message ✓ not failures PASS Tests\Features\Expect\toBeFalse ✓ strict comparisons ✓ failures ✓ failures with custom message ✓ not failures PASS Tests\Features\Expect\toBeFalsy ✓ passes as falsy with (false) ✓ passes as falsy with ('') ✓ passes as falsy with (null) ✓ passes as falsy with (0) ✓ passes as falsy with ('0') ✓ passes as not falsy with (true) ✓ passes as not falsy with (1) #1 ✓ passes as not falsy with ('false') ✓ passes as not falsy with (1) #2 ✓ passes as not falsy with (-1) ✓ failures ✓ failures with custom message ✓ not failures PASS Tests\Features\Expect\toBeFile ✓ pass ✓ failures ✓ failures with custom message ✓ not failures PASS Tests\Features\Expect\toBeFloat ✓ pass ✓ failures ✓ failures with custom message ✓ not failures PASS Tests\Features\Expect\toBeGreaterThan ✓ passes ✓ passes with DateTime and DateTimeImmutable ✓ passes with strings ✓ failures ✓ failures with custom message ✓ not failures PASS Tests\Features\Expect\toBeGreaterThanOrEqual ✓ passes ✓ passes with DateTime and DateTimeImmutable ✓ passes with strings ✓ failures ✓ failures with custom message ✓ not failures PASS Tests\Features\Expect\toBeIn ✓ passes ✓ failures ✓ failures with custom message ✓ not failures PASS Tests\Features\Expect\toBeInfinite ✓ pass ✓ failures ✓ failures with custom message ✓ not failures PASS Tests\Features\Expect\toBeInstanceOf ✓ pass ✓ failures ✓ failures with custom message ✓ not failures PASS Tests\Features\Expect\toBeInt ✓ pass ✓ failures ✓ failures with custom message ✓ not failures PASS Tests\Features\Expect\toBeIntBackedEnum ✓ enum is backed by int ✓ enum is not backed by int PASS Tests\Features\Expect\toBeInvokable ✓ class is invokable ✓ opposite class is invokable ✓ class is invokable via a parent class ✓ class is invokable via a trait ✓ failure when the class is not invokable ✓ class is not invokable PASS Tests\Features\Expect\toBeIterable ✓ pass ✓ failures ✓ failures with custom message ✓ not failures PASS Tests\Features\Expect\toBeJson ✓ pass ✓ failures ✓ failures with custom message ✓ not failures PASS Tests\Features\Expect\toBeKebabCase ✓ pass ✓ failures ✓ failures with custom message ✓ not failures PASS Tests\Features\Expect\toBeLessThan ✓ passes ✓ passes with DateTime and DateTimeImmutable ✓ passes with strings ✓ failures ✓ failures with custom message ✓ not failures PASS Tests\Features\Expect\toBeLessThanOrEqual ✓ passes ✓ passes with DateTime and DateTimeImmutable ✓ passes with strings ✓ failures ✓ failures with custom message ✓ not failures PASS Tests\Features\Expect\toBeList ✓ pass ✓ failures ✓ failures with custom message ✓ not failures PASS Tests\Features\Expect\toBeLowercase ✓ pass ✓ failures ✓ failures with custom message ✓ not failures PASS Tests\Features\Expect\toBeNAN ✓ pass ✓ failures ✓ failures with custom message ✓ not failures PASS Tests\Features\Expect\toBeNull ✓ pass ✓ failures ✓ failures with custom message ✓ not failures PASS Tests\Features\Expect\toBeNumeric ✓ pass ✓ failures ✓ failures with custom message ✓ not failures PASS Tests\Features\Expect\toBeObject ✓ pass ✓ failures ✓ failures with custom message ✓ not failures PASS Tests\Features\Expect\toBeReadableDirectory ✓ pass ✓ failures ✓ failures with custom message ✓ not failures PASS Tests\Features\Expect\toBeReadableFile ✓ pass ✓ failures ✓ failures with custom message ✓ not failures PASS Tests\Features\Expect\toBeResource ✓ pass ✓ failures ✓ failures with custom message ✓ not failures PASS Tests\Features\Expect\toBeScalar ✓ pass ✓ failures ✓ failures with custom message ✓ not failures PASS Tests\Features\Expect\toBeSlug ✓ pass ✓ failures ✓ failures with custom message ✓ failures with default message ✓ not failures PASS Tests\Features\Expect\toBeSnakeCase ✓ pass ✓ failures ✓ failures with custom message ✓ not failures PASS Tests\Features\Expect\toBeString ✓ pass ✓ failures ✓ failures with custom message ✓ not failures PASS Tests\Features\Expect\toBeStringBackedEnum ✓ enum is backed by string ✓ enum is not backed by string PASS Tests\Features\Expect\toBeStudlyCase ✓ pass ✓ failures ✓ failures with custom message ✓ not failures PASS Tests\Features\Expect\toBeTrue ✓ strict comparisons ✓ failures ✓ failures with custom message ✓ not failures PASS Tests\Features\Expect\toBeTruthy ✓ passes as truthy with (true) ✓ passes as truthy with (1) #1 ✓ passes as truthy with ('false') ✓ passes as truthy with (1) #2 ✓ passes as truthy with (-1) ✓ passes as not truthy with (false) ✓ passes as not truthy with ('') ✓ passes as not truthy with (null) ✓ passes as not truthy with (0) ✓ passes as not truthy with ('0') ✓ failures ✓ failures with custom message ✓ not failures PASS Tests\Features\Expect\toBeUppercase ✓ pass ✓ failures ✓ failures with custom message ✓ not failures PASS Tests\Features\Expect\toBeUrl ✓ pass ✓ failures ✓ failures with custom message ✓ failures with default message ✓ not failures PASS Tests\Features\Expect\toBeUuid ✓ failures with wrong type ✓ pass ✓ failures ✓ failures with message ✓ not failures PASS Tests\Features\Expect\toBeWritableDirectory ✓ pass ✓ failures ✓ failures with custom message ✓ not failures PASS Tests\Features\Expect\toBeWritableFile ✓ pass ✓ failures ✓ failures with custom message ✓ not failures PASS Tests\Features\Expect\toContain ✓ passes strings ✓ passes strings with multiple needles ✓ passes arrays ✓ passes arrays with multiple needles ✓ passes with array needles ✓ failures ✓ failures with multiple needles (all failing) ✓ failures with multiple needles (some failing) ✓ not failures ✓ not failures with multiple needles (all failing) ✓ not failures with multiple needles (some failing) PASS Tests\Features\Expect\toContainEqual ✓ passes arrays ✓ passes arrays with multiple needles ✓ failures ✓ failures with multiple needles (all failing) ✓ failures with multiple needles (some failing) ✓ not failures ✓ not failures with multiple needles (all failing) ✓ not failures with multiple needles (some failing) PASS Tests\Features\Expect\toContainOnlyInstancesOf ✓ pass ✓ failures ✓ failures with custom message ✓ not failures PASS Tests\Features\Expect\toEndWith ✓ pass ✓ failures ✓ failures with custom message ✓ not failures PASS Tests\Features\Expect\toEqual ✓ pass ✓ failures ✓ failures with custom message ✓ not failures PASS Tests\Features\Expect\toEqualCanonicalizing ✓ pass ✓ failures ✓ failures with custom message ✓ not failures PASS Tests\Features\Expect\toEqualWithDelta ✓ pass ✓ failures with custom message ✓ not failures PASS Tests\Features\Expect\toHaveAttribute ✓ class has attribute ✓ opposite class has attribute ✓ class not has attribute PASS Tests\Features\Expect\toHaveCamelCaseKeys ✓ pass ✓ failures ✓ failures with message ✓ not failures PASS Tests\Features\Expect\toHaveConstructor ✓ class has constructor ✓ class has no constructor PASS Tests\Features\Expect\toHaveCount ✓ pass ✓ failures with invalid type ✓ failures ✓ failures with message ✓ not failures PASS Tests\Features\Expect\toHaveDestructor ✓ class has destructor ✓ class has no destructor PASS Tests\Features\Expect\toHaveKebabCaseKeys ✓ pass ✓ failures ✓ failures with message ✓ not failures PASS Tests\Features\Expect\toHaveKey ✓ pass ✓ pass with nested key ✓ pass with plain key with dots ✓ pass with value check ✓ pass with value check and nested key ✓ pass with value check and plain key with dots ✓ failures ✓ failures with custom message ✓ failures with custom message and Any matcher ✓ failures with nested key ✓ failures with nested key and custom message ✓ failures with nested key and custom message with Any matcher ✓ failures with plain key with dots ✓ fails with wrong value ✓ fails with wrong value and nested key ✓ fails with wrong value and plain key with dots ✓ not failures ✓ not failures with nested key ✓ not failures with plain key with dots ✓ not failures with correct value ✓ not failures with correct value and with nested key ✓ not failures with correct value and with plain key with dots PASS Tests\Features\Expect\toHaveKeys ✓ pass ✓ pass with multi-dimensional arrays ✓ failures ✓ failures with custom message ✓ failures with multi-dimensional arrays ✓ failures with multi-dimensional arrays and custom message ✓ not failures ✓ not failures with multi-dimensional arrays PASS Tests\Features\Expect\toHaveLength ✓ it passes with ('Fortaleza') ✓ it passes with ('Sollefteå') ✓ it passes with ('Ιεράπετρα') ✓ it passes with (stdClass) ✓ it passes with array ✓ it passes with *not* ✓ it properly fails with *not* ✓ it fails ✓ it fails with message PASS Tests\Features\Expect\toHaveLineCountLessThan ✓ it passes ✓ it fails PASS Tests\Features\Expect\toHaveMethod ✓ class has method ✓ opposite class has method ✓ class has method via a parent class ✓ class has method via a trait ✓ failure when the class has no method ✓ class has no method PASS Tests\Features\Expect\toHaveMethods ✓ class has method ✓ opposite class has method ✓ class has method via a parent class ✓ class has method via a trait ✓ failure when the class has no method ✓ class has no method PASS Tests\Features\Expect\toHaveMethodsDocumented ✓ it passes ✓ it fails 1 ✓ it fails 2 PASS Tests\Features\Expect\toHavePrefix ✓ missing prefix ✓ has prefix ✓ opposite missing prefix ✓ opposite has prefix PASS Tests\Features\Expect\toHavePrivateMethodsBesides ✓ pass ✓ failures PASS Tests\Features\Expect\toHaveProperties ✓ pass ✓ failures ✓ failures with custom message ✓ not failures PASS Tests\Features\Expect\toHavePropertiesDocumented ✓ it passes ✓ it fails 1 ✓ it fails 2 PASS Tests\Features\Expect\toHaveProperty ✓ pass ✓ failures ✓ failures with message ✓ failures with message and Any matcher ✓ not failures PASS Tests\Features\Expect\toHaveProtectedMethodsBesides ✓ pass ✓ failures PASS Tests\Features\Expect\toHavePublicMethodsBesides ✓ pass ✓ failures PASS Tests\Features\Expect\toHaveSameSize ✓ failures with wrong type ✓ pass ✓ failures ✓ failures with message ✓ not failures PASS Tests\Features\Expect\toHaveSnakeCaseKeys ✓ pass ✓ failures ✓ failures with message ✓ not failures PASS Tests\Features\Expect\toHaveStudlyCaseKeys ✓ pass ✓ failures ✓ failures with message ✓ not failures PASS Tests\Features\Expect\toHaveSuffix ✓ missing suffix ✓ has suffix ✓ opposite missing suffix ✓ opposite has suffix PASS Tests\Features\Expect\toMatch ✓ pass ✓ failures ✓ failures with custom message ✓ not failures PASS Tests\Features\Expect\toMatchArray ✓ pass ✓ failures ✓ failures with custom message ✓ not failures PASS Tests\Features\Expect\toMatchConstraint ✓ pass ✓ failures ✓ failures with custom message ✓ not failures PASS Tests\Features\Expect\toMatchObject ✓ pass ✓ pass with class ✓ failures ✓ failures with custom message ✓ not failures PASS Tests\Features\Expect\toMatchSnapshot ✓ pass ✓ pass using pipes ✓ pass with __toString ✓ pass with toString ✓ pass with dataset with ('my-datas-set-value') ✓ within describe → pass with dataset with ('my-datas-set-value') ✓ pass with toArray ✓ pass with array ✓ pass with toSnapshot ✓ failures ✓ failures with custom message ✓ not failures ✓ multiple snapshot expectations ✓ multiple snapshot expectations with datasets with (1) ✓ multiple snapshot expectations with datasets with ('foo') ✓ multiple snapshot expectations with datasets with ('bar') ✓ multiple snapshot expectations with datasets with ('baz') ✓ describable → multiple snapshot expectations with describe ✓ multiple snapshot expectations with repeat @ repetition 1 of 10 ✓ multiple snapshot expectations with repeat @ repetition 2 of 10 ✓ multiple snapshot expectations with repeat @ repetition 3 of 10 ✓ multiple snapshot expectations with repeat @ repetition 4 of 10 ✓ multiple snapshot expectations with repeat @ repetition 5 of 10 ✓ multiple snapshot expectations with repeat @ repetition 6 of 10 ✓ multiple snapshot expectations with repeat @ repetition 7 of 10 ✓ multiple snapshot expectations with repeat @ repetition 8 of 10 ✓ multiple snapshot expectations with repeat @ repetition 9 of 10 ✓ multiple snapshot expectations with repeat @ repetition 10 of 10 PASS Tests\Features\Expect\toStartWith ✓ pass ✓ failures ✓ failures with custom message ✓ not failures PASS Tests\Features\Expect\toThrow ✓ passes ✓ failures 1 ✓ failures 2 ✓ failures 3 ✓ failures 4 ✓ failures 5 ✓ failures 6 ✓ failures 7 ✓ failures 8 ✓ failures with custom message ✓ not failures ✓ closure missing parameter ✓ closure missing type-hint ✓ it can handle a non-defined exception ✓ it can handle a class not found Error PASS Tests\Features\Expect\toUseStrictEquality ✓ missing strict equality ✓ has strict equality ✓ opposite missing strict equality ✓ opposite has strict equality PASS Tests\Features\Expect\toUseStrictTypes ✓ pass ✓ failures PASS Tests\Features\Expect\toUseTrait ✓ pass ✓ failures ✓ not failures PASS Tests\Features\Expect\unless ✓ it pass ✓ it failures ✓ it runs with truthy ✓ it skips with falsy ✓ it runs with truthy closure condition ✓ it skips with falsy closure condition ✓ it can be used in higher order tests PASS Tests\Features\Expect\when ✓ it pass ✓ it failures ✓ it runs with truthy ✓ it skips with falsy ✓ it runs with truthy closure condition ✓ it skips with falsy closure condition ✓ it can be used in higher order tests PASS Tests\Features\Fail ✓ it may fail ✓ it may fail with the given message PASS Tests\Features\Fails ✓ it may fail ✓ it may fail with the given message PASS Tests\Features\Fixture ✓ it may return a file path ✓ it may throw an exception if the file does not exist WARN Tests\Features\Helpers ✓ it can set/get properties on $this ! it gets null if property do not exist → Undefined property Tests\Features\Helpers::$wqdwqdqw ✓ it allows to call underlying protected/private methods ✓ it throws error if method do not exist ✓ it can forward unexpected calls to any global function ✓ it can use helpers from helpers file ✓ it can use helpers from helpers directory PASS Tests\Features\HigherOrderTests ✓ it proxies calls to object ✓ it is capable doing multiple assertions ✓ it resolves expect callables correctly ✓ does not treat method names as callables ✓ it can defer a method until after test setup ✓ it can pass datasets into the expect callables with (1, 2, 3) ✓ it can pass datasets into the defer callable with (1, 2, 3) ✓ it can pass shared datasets into callables with (1) ✓ it can pass shared datasets into callables with (2) WARN Tests\Features\Incompleted … incompleted … it is incompleted … it is incompleted even with method calls like skip … it is incompleted even with method calls like group ✓ it is not incompleted because of expect ✓ it is not incompleted because of assert ✓ it is not incompleted because of test with assertions … a "describe" group of tests → it is incompleted PASS Tests\Features\Issue ✓ it may be associated with an issue #1, #2 ✓ nested → it may be associated with an issue #1, #4, #5, #6, #3 // an note between an the issue PASS Tests\Features\It ✓ it is a test ✓ it is a higher order message test ✓ a "describe" group of tests → it is a test ✓ a "describe" group of tests → it is a higher order message test PASS Tests\Features\Note ✓ it may have a static note // This is before each static note // This is a note // This is before each runtime note ✓ it may have a runtime note // This is before each static note // This is before each runtime note // This is a runtime note ✓ it may have static note and runtime note // This is before each static note // This is a static note // This is before each runtime note // This is a runtime note ✓ nested → it may have static note and runtime note // This is before each static note // This is describe static note // This is before each describe static note // This is a static note within describe // This is before each runtime note // This is before each describe runtime note // This is a runtime note within describe ✓ nested → describe nested within describe → it may have a static note and runtime note // This is before each static note // This is describe static note // This is before each describe static note // This is a nested describe static note // This is before each nested describe static note // This is a static note within a nested describe // This is before each runtime note // This is before each describe runtime note // This is before each nested describe runtime note // This is a runtime note within a nested describe ✓ matching describe names → describe block → it may have a static note and runtime note // This is before each static note // This is before each matching describe static note // This is a nested matching static note // This is a static note within a matching describe // This is before each runtime note // This is before each matching describe runtime note // This is before each matching describe runtime note // This is a runtime note within a matching describe ✓ matching describe names → describe block → it may have a static note and runtime note, that are different than the matching describe block // This is before each static note // This is before each matching describe static note // This is a nested matching static note, and should not contain the matching describe notes // This is before each matching describe static note, and should not contain the matching describe notes // This is a static note within a matching describe, and should not contain the matching describe notes // This is before each runtime note // This is before each matching describe runtime note // This is before each matching describe runtime note, and should not contain the matching describe notes // This is a runtime note within a matching describe, and should not contain the matching describe notes ✓ multiple notes // This is before each static note // This is before each runtime note // This is a runtime note // This is another runtime note NOTI Tests\Features\Notices ! notice → This is a notice description // tests/Features/Notices.php:4 ! a "describe" group of tests → notice → This is a notice description // tests/Features/Notices.php:11 PASS Tests\Features\Pr ✓ it may be associated with an pr #1, #2 ✓ nested → it may be associated with an pr #1, #4, #5, #6, #3 // an note between an the pr PASS Tests\Features\References ✓ it can reference a specific class ✓ it can reference a specific class method PASS Tests\Features\Repeat ✓ once ✓ multiple times @ repetition 1 of 5 ✓ multiple times @ repetition 2 of 5 ✓ multiple times @ repetition 3 of 5 ✓ multiple times @ repetition 4 of 5 ✓ multiple times @ repetition 5 of 5 ✓ multiple times with single dataset dataset "a" @ repetition 1 of 6 ✓ multiple times with single dataset dataset "b" @ repetition 1 of 6 ✓ multiple times with single dataset dataset "c" @ repetition 1 of 6 ✓ multiple times with single dataset dataset "a" @ repetition 2 of 6 ✓ multiple times with single dataset dataset "b" @ repetition 2 of 6 ✓ multiple times with single dataset dataset "c" @ repetition 2 of 6 ✓ multiple times with single dataset dataset "a" @ repetition 3 of 6 ✓ multiple times with single dataset dataset "b" @ repetition 3 of 6 ✓ multiple times with single dataset dataset "c" @ repetition 3 of 6 ✓ multiple times with single dataset dataset "a" @ repetition 4 of 6 ✓ multiple times with single dataset dataset "b" @ repetition 4 of 6 ✓ multiple times with single dataset dataset "c" @ repetition 4 of 6 ✓ multiple times with single dataset dataset "a" @ repetition 5 of 6 ✓ multiple times with single dataset dataset "b" @ repetition 5 of 6 ✓ multiple times with single dataset dataset "c" @ repetition 5 of 6 ✓ multiple times with single dataset dataset "a" @ repetition 6 of 6 ✓ multiple times with single dataset dataset "b" @ repetition 6 of 6 ✓ multiple times with single dataset dataset "c" @ repetition 6 of 6 ✓ multiple times with multiple dataset dataset "a" / (4) @ repetition 1 of 7 ✓ multiple times with multiple dataset dataset "a" / (5) @ repetition 1 of 7 ✓ multiple times with multiple dataset dataset "a" / (6) @ repetition 1 of 7 ✓ multiple times with multiple dataset dataset "b" / (4) @ repetition 1 of 7 ✓ multiple times with multiple dataset dataset "b" / (5) @ repetition 1 of 7 ✓ multiple times with multiple dataset dataset "b" / (6) @ repetition 1 of 7 ✓ multiple times with multiple dataset dataset "c" / (4) @ repetition 1 of 7 ✓ multiple times with multiple dataset dataset "c" / (5) @ repetition 1 of 7 ✓ multiple times with multiple dataset dataset "c" / (6) @ repetition 1 of 7 ✓ multiple times with multiple dataset dataset "a" / (4) @ repetition 2 of 7 ✓ multiple times with multiple dataset dataset "a" / (5) @ repetition 2 of 7 ✓ multiple times with multiple dataset dataset "a" / (6) @ repetition 2 of 7 ✓ multiple times with multiple dataset dataset "b" / (4) @ repetition 2 of 7 ✓ multiple times with multiple dataset dataset "b" / (5) @ repetition 2 of 7 ✓ multiple times with multiple dataset dataset "b" / (6) @ repetition 2 of 7 ✓ multiple times with multiple dataset dataset "c" / (4) @ repetition 2 of 7 ✓ multiple times with multiple dataset dataset "c" / (5) @ repetition 2 of 7 ✓ multiple times with multiple dataset dataset "c" / (6) @ repetition 2 of 7 ✓ multiple times with multiple dataset dataset "a" / (4) @ repetition 3 of 7 ✓ multiple times with multiple dataset dataset "a" / (5) @ repetition 3 of 7 ✓ multiple times with multiple dataset dataset "a" / (6) @ repetition 3 of 7 ✓ multiple times with multiple dataset dataset "b" / (4) @ repetition 3 of 7 ✓ multiple times with multiple dataset dataset "b" / (5) @ repetition 3 of 7 ✓ multiple times with multiple dataset dataset "b" / (6) @ repetition 3 of 7 ✓ multiple times with multiple dataset dataset "c" / (4) @ repetition 3 of 7 ✓ multiple times with multiple dataset dataset "c" / (5) @ repetition 3 of 7 ✓ multiple times with multiple dataset dataset "c" / (6) @ repetition 3 of 7 ✓ multiple times with multiple dataset dataset "a" / (4) @ repetition 4 of 7 ✓ multiple times with multiple dataset dataset "a" / (5) @ repetition 4 of 7 ✓ multiple times with multiple dataset dataset "a" / (6) @ repetition 4 of 7 ✓ multiple times with multiple dataset dataset "b" / (4) @ repetition 4 of 7 ✓ multiple times with multiple dataset dataset "b" / (5) @ repetition 4 of 7 ✓ multiple times with multiple dataset dataset "b" / (6) @ repetition 4 of 7 ✓ multiple times with multiple dataset dataset "c" / (4) @ repetition 4 of 7 ✓ multiple times with multiple dataset dataset "c" / (5) @ repetition 4 of 7 ✓ multiple times with multiple dataset dataset "c" / (6) @ repetition 4 of 7 ✓ multiple times with multiple dataset dataset "a" / (4) @ repetition 5 of 7 ✓ multiple times with multiple dataset dataset "a" / (5) @ repetition 5 of 7 ✓ multiple times with multiple dataset dataset "a" / (6) @ repetition 5 of 7 ✓ multiple times with multiple dataset dataset "b" / (4) @ repetition 5 of 7 ✓ multiple times with multiple dataset dataset "b" / (5) @ repetition 5 of 7 ✓ multiple times with multiple dataset dataset "b" / (6) @ repetition 5 of 7 ✓ multiple times with multiple dataset dataset "c" / (4) @ repetition 5 of 7 ✓ multiple times with multiple dataset dataset "c" / (5) @ repetition 5 of 7 ✓ multiple times with multiple dataset dataset "c" / (6) @ repetition 5 of 7 ✓ multiple times with multiple dataset dataset "a" / (4) @ repetition 6 of 7 ✓ multiple times with multiple dataset dataset "a" / (5) @ repetition 6 of 7 ✓ multiple times with multiple dataset dataset "a" / (6) @ repetition 6 of 7 ✓ multiple times with multiple dataset dataset "b" / (4) @ repetition 6 of 7 ✓ multiple times with multiple dataset dataset "b" / (5) @ repetition 6 of 7 ✓ multiple times with multiple dataset dataset "b" / (6) @ repetition 6 of 7 ✓ multiple times with multiple dataset dataset "c" / (4) @ repetition 6 of 7 ✓ multiple times with multiple dataset dataset "c" / (5) @ repetition 6 of 7 ✓ multiple times with multiple dataset dataset "c" / (6) @ repetition 6 of 7 ✓ multiple times with multiple dataset dataset "a" / (4) @ repetition 7 of 7 ✓ multiple times with multiple dataset dataset "a" / (5) @ repetition 7 of 7 ✓ multiple times with multiple dataset dataset "a" / (6) @ repetition 7 of 7 ✓ multiple times with multiple dataset dataset "b" / (4) @ repetition 7 of 7 ✓ multiple times with multiple dataset dataset "b" / (5) @ repetition 7 of 7 ✓ multiple times with multiple dataset dataset "b" / (6) @ repetition 7 of 7 ✓ multiple times with multiple dataset dataset "c" / (4) @ repetition 7 of 7 ✓ multiple times with multiple dataset dataset "c" / (5) @ repetition 7 of 7 ✓ multiple times with multiple dataset dataset "c" / (6) @ repetition 7 of 7 ✓ multiple times with iterator @ repetition 1 of 2 ✓ multiple times with iterator @ repetition 2 of 2 ✓ multiple times with repeat iterator with single dataset ('a') @ repetition 1 of 2 ✓ multiple times with repeat iterator with single dataset ('b') @ repetition 1 of 2 ✓ multiple times with repeat iterator with single dataset ('c') @ repetition 1 of 2 ✓ multiple times with repeat iterator with single dataset ('a') @ repetition 2 of 2 ✓ multiple times with repeat iterator with single dataset ('b') @ repetition 2 of 2 ✓ multiple times with repeat iterator with single dataset ('c') @ repetition 2 of 2 ✓ multiple times with repeat iterator with multiple dataset ('a') / ('d') @ repetition 1 of 2 ✓ multiple times with repeat iterator with multiple dataset ('a') / ('e') @ repetition 1 of 2 ✓ multiple times with repeat iterator with multiple dataset ('a') / ('f') @ repetition 1 of 2 ✓ multiple times with repeat iterator with multiple dataset ('b') / ('d') @ repetition 1 of 2 ✓ multiple times with repeat iterator with multiple dataset ('b') / ('e') @ repetition 1 of 2 ✓ multiple times with repeat iterator with multiple dataset ('b') / ('f') @ repetition 1 of 2 ✓ multiple times with repeat iterator with multiple dataset ('c') / ('d') @ repetition 1 of 2 ✓ multiple times with repeat iterator with multiple dataset ('c') / ('e') @ repetition 1 of 2 ✓ multiple times with repeat iterator with multiple dataset ('c') / ('f') @ repetition 1 of 2 ✓ multiple times with repeat iterator with multiple dataset ('a') / ('d') @ repetition 2 of 2 ✓ multiple times with repeat iterator with multiple dataset ('a') / ('e') @ repetition 2 of 2 ✓ multiple times with repeat iterator with multiple dataset ('a') / ('f') @ repetition 2 of 2 ✓ multiple times with repeat iterator with multiple dataset ('b') / ('d') @ repetition 2 of 2 ✓ multiple times with repeat iterator with multiple dataset ('b') / ('e') @ repetition 2 of 2 ✓ multiple times with repeat iterator with multiple dataset ('b') / ('f') @ repetition 2 of 2 ✓ multiple times with repeat iterator with multiple dataset ('c') / ('d') @ repetition 2 of 2 ✓ multiple times with repeat iterator with multiple dataset ('c') / ('e') @ repetition 2 of 2 ✓ multiple times with repeat iterator with multiple dataset ('c') / ('f') @ repetition 2 of 2 ✓ describe blocks → multiple times @ repetition 1 of 3 ✓ describe blocks → multiple times @ repetition 2 of 3 ✓ describe blocks → multiple times @ repetition 3 of 3 ✓ describe blocks → describe with repeat → test with no repeat should repeat the number of times specified in the parent describe block @ repetition 1 of 3 ✓ describe blocks → describe with repeat → test with no repeat should repeat the number of times specified in the parent describe block @ repetition 2 of 3 ✓ describe blocks → describe with repeat → test with no repeat should repeat the number of times specified in the parent describe block @ repetition 3 of 3 ✓ describe blocks → describe with repeat → test with repeat should repeat the number of times specified in the test @ repetition 1 of 2 ✓ describe blocks → describe with repeat → test with repeat should repeat the number of times specified in the test @ repetition 2 of 2 ✓ describe blocks → describe with repeat → nested describe without repeat → test with no repeat should repeat the number of times specified in the parent's parent describe block @ repetition 1 of 3 ✓ describe blocks → describe with repeat → nested describe without repeat → test with no repeat should repeat the number of times specified in the parent's parent describe block @ repetition 2 of 3 ✓ describe blocks → describe with repeat → nested describe without repeat → test with no repeat should repeat the number of times specified in the parent's parent describe block @ repetition 3 of 3 ✓ describe blocks → describe with repeat → nested describe without repeat → test with repeat should repeat the number of times specified in the test @ repetition 1 of 2 ✓ describe blocks → describe with repeat → nested describe without repeat → test with repeat should repeat the number of times specified in the test @ repetition 2 of 2 ✓ describe blocks → describe with repeat → nested describe with repeat → test with no repeat should repeat the number of times specified in the parent describe block @ repetition 1 of 2 ✓ describe blocks → describe with repeat → nested describe with repeat → test with no repeat should repeat the number of times specified in the parent describe block @ repetition 2 of 2 ✓ describe blocks → describe with repeat → nested describe with repeat → test with repeat should repeat the number of times specified in the test @ repetition 1 of 2 ✓ describe blocks → describe with repeat → nested describe with repeat → test with repeat should repeat the number of times specified in the test @ repetition 2 of 2 ✓ matching describe blocks → describe block → it should repeat the number of times specified in the parent describe block @ repetition 1 of 3 ✓ matching describe blocks → describe block → it should repeat the number of times specified in the parent describe block @ repetition 2 of 3 ✓ matching describe blocks → describe block → it should repeat the number of times specified in the parent describe block @ repetition 3 of 3 ✓ matching describe blocks → describe block → should not repeat the number of times of the describe block with the same name PASS Tests\Features\ScopedDatasets\Directory\NestedDirectory1\TestFileInNestedDirectoryWithDatasetsFile ✓ uses dataset with (1) ✓ uses dataset with (2) ✓ uses dataset with (3) ✓ uses dataset with (4) ✓ uses dataset with (5) ✓ uses dataset with ('ScopedDatasets/NestedDirector…ts.php') ✓ the right dataset is taken PASS Tests\Features\ScopedDatasets\Directory\NestedDirectory2\TestFileInNestedDirectory ✓ uses dataset with (1) ✓ uses dataset with (2) ✓ uses dataset with (3) ✓ uses dataset with (4) ✓ uses dataset with (5) ✓ uses dataset with ('ScopedDatasets/Datasets/Scoped.php') ✓ the right dataset is taken PASS Tests\Features\ScopedDatasets\Directory\TestFileWithLocallyDefinedDataset ✓ uses dataset with (1) ✓ uses dataset with (2) ✓ uses dataset with (3) ✓ uses dataset with (4) ✓ uses dataset with (5) ✓ uses dataset with ('ScopedDatasets/ScopedDatasets.php') ✓ the right dataset is taken PASS Tests\Features\ScopedDatasets\Directory\TestFileWithScopedDataset ✓ uses dataset with (1) ✓ uses dataset with (2) ✓ uses dataset with (3) ✓ uses dataset with (4) ✓ uses dataset with (5) ✓ uses dataset with ('ScopedDatasets/Datasets/Scoped.php') ✓ the right dataset is taken PASS Tests\Features\ScopedDatasets\TestFileOutOfScope ✓ uses dataset with (1) ✓ uses dataset with (2) ✓ the right dataset is taken ✓ it can see datasets defined in Pest.php file with ('A') ✓ it can see datasets defined in Pest.php file with ('B') ✓ Pest.php dataset is taken PASS Tests\Features\See ✓ it can reference a specific class ✓ it can reference a specific class method WARN Tests\Features\Skip ✓ it do not skips - it skips with truthy → 1 - it skips with truthy condition by default - it skips with message → skipped because bar - it skips with truthy closure condition ✓ it do not skips with falsy closure condition - it skips with condition and message → skipped because foo - it skips when skip after assertion - it can use something in the test case as a condition → This test was skipped - it can user higher order callables and skip - skip on describe → skipped tests → nested inside skipped block → it should not execute - skip on describe → skipped tests → it should not execute ✓ skip on describe → it should execute - skip on beforeEach → skipped tests → nested inside skipped block → it should not execute - skip on beforeEach → skipped tests → it should not execute ✓ skip on beforeEach → it should execute - matching describe with skip → describe block → it should not execute ✓ matching describe with skip → describe block → it should execute a test in a describe block with the same name as a skipped describe block ✓ matching describe with skip → it should execute - matching describe with skip on beforeEach → describe block → it should not execute ✓ matching describe with skip on beforeEach → describe block → it should execute a test in a describe block with the same name as a skipped describe block ✓ matching describe with skip on beforeEach → it should execute ✓ it does not skip after the describe block - it can skip after the describe block WARN Tests\Features\SkipOnPhp ✓ it can run on php version ✓ it can run on specific php version - it can skip on php versions depending on constraint → This test is skipped on PHP [>=7.4.0]. PASS Tests\Features\Test ✓ a test ✓ higher order message test PASS Tests\Features\ThrowsNoExceptions ✓ it allows access to the underlying expectNotToPerformAssertions method ✓ it allows performing no expectations without being risky ✓ a "describe" group of tests → it allows performing no expectations without being risky PASS Tests\Features\Ticket ✓ it may be associated with an ticket #1, #2 ✓ nested → it may be associated with an ticket #1, #4, #5, #6, #3 // an note between an the ticket PASS Tests\Features\Todo - 29 todos ↓ something todo later ↓ something todo later chained ↓ something todo later chained and with function body ✓ it does something within a file with a todo ↓ it may have an associated assignee [@nunomaduro] ↓ it may have an associated issue #1 ↓ it may have an associated PR #1 ↓ it may have an associated note // a note ↓ todo on describe → todo block → nested inside todo block → it should not execute ↓ todo on describe → todo block → nested inside todo block → it should set the note // hi ↓ todo on describe → todo block → describe with note → it should apply the note to a test without a todo // describe note ↓ todo on describe → todo block → describe with note → it should apply the note to a test with a todo // describe note ↓ todo on describe → todo block → describe with note → it should apply the note as well as the note from the test // describe note // test note ↓ todo on describe → todo block → describe with note → nested describe with note → it should apply all parent notes to a test without a todo // describe note // nested describe note ↓ todo on describe → todo block → describe with note → nested describe with note → it should apply all parent notes to a test with a todo // describe note // nested describe note ↓ todo on describe → todo block → describe with note → nested describe with note → it should apply all parent notes as well as the note from the test // describe note // nested describe note // test note ↓ todo on describe → todo block → it should not execute ✓ todo on describe → it should execute ↓ todo on describe with matching name → describe block → it should not execute ✓ todo on describe with matching name → describe block → it should execute a test in a describe block with the same name as a todo describe block ✓ todo on describe with matching name → it should execute ↓ todo on test after describe block ↓ todo with note on test after describe block // test note ↓ todo on beforeEach → todo block → nested inside todo block → it should not execute ↓ todo on beforeEach → todo block → describe with note → it should apply the note to a test without a todo // describe note ↓ todo on beforeEach → todo block → describe with note → it should apply the note to a test with a todo // describe note ↓ todo on beforeEach → todo block → describe with note → it should apply the note as well as the note from the test // describe note // test note ↓ todo on beforeEach → todo block → describe with note → nested describe with note → it should apply all parent notes to a test without a todo // describe note // nested describe note ↓ todo on beforeEach → todo block → describe with note → nested describe with note → it should apply all parent notes to a test with a todo // describe note // nested describe note ↓ todo on beforeEach → todo block → describe with note → nested describe with note → it should apply all parent notes as well as the note from the test // describe note // nested describe note // test note ↓ todo on beforeEach → todo block → it should not execute ✓ todo on beforeEach → it should execute ↓ todo on test after describe block with beforeEach ↓ todo with note on test after describe block with beforeEach // test note WARN Tests\Features\Warnings ! warning → Undefined property: P\Tests\Features\Warnings::$fooqwdfwqdfqw ! user warning → This is a warning description ! a "describe" group of tests → user warning → This is a warning description PASS Tests\Features\Wip ✓ it may have an associated assignee [@nunomaduro] ✓ it may have an associated issue #1 ✓ it may have an associated PR #1 ✓ it may have an associated note // a note WARN Tests\Fixtures\CollisionTest - error - success PASS Tests\Fixtures\DirectoryWithTests\ExampleTest ✓ it example 1 PASS Tests\Fixtures\ExampleTest ✓ it example 2 WARN Tests\Fixtures\UnexpectedOutput - output PASS Tests\Helpers\TestInHelpers ✓ it executes tests in the Helpers directory PASS Tests\Hooks\AfterEachTest ✓ nested → nested afterEach execution order ✓ global afterEach execution order PASS Tests\Hooks\BeforeAllTest ✓ it gets called before all tests 1 @ repetition 1 of 2 ✓ it gets called before all tests 1 @ repetition 2 of 2 ✓ it gets called before all tests 2 PASS Tests\Hooks\BeforeEachTest ✓ global beforeEach execution order PASS Tests\PHPUnit\CustomAffixes\InvalidTestName ✓ it runs file names like @#$%^&()-_=+.php PASS Tests\PHPUnit\CustomAffixes\ATestWithSpaces ✓ it runs file names like A Test With Spaces.php PASS Tests\PHPUnit\CustomAffixes\AdditionalFileExtension ✓ it runs file names like AdditionalFileExtension.spec.php PASS Tests\PHPUnit\CustomAffixes\FolderWithAn\ExampleTest ✓ custom traits can be used ✓ trait applied in this file PASS Tests\PHPUnit\CustomAffixes\ManyExtensions ✓ it runs file names like ManyExtensions.class.test.php PASS Tests\PHPUnit\CustomAffixes\TestCaseWithQuotes ✓ it runs file names like Test 'Case' With Quotes.php PASS Tests\PHPUnit\CustomAffixes\kebabcasespec ✓ it runs file names like kebab-case-spec.php PASS Tests\PHPUnit\CustomAffixes\snakecasespec ✓ it runs file names like snake_case_spec.php PASS Tests\CustomTestCase\ChildTest ✓ override method PASS Tests\CustomTestCase\ExecutedTest ✓ that gets executed PASS Tests\CustomTestCase\ParentTest ✓ override method PASS Tests\PHPUnit\CustomTestCase\UsesPerDirectory ✓ closure was bound to CustomTestCase PASS Tests\PHPUnit\CustomTestCaseInSubFolders\SubFolder\SubFolder\UsesPerSubDirectory ✓ closure was bound to CustomTestCase PASS Tests\PHPUnit\CustomTestCaseInSubFolders\SubFolder2\UsesPerFile ✓ custom traits can be used ✓ trait applied in this file PASS Tests\PHPUnit\GlobPatternTests\SubFolder\InnerFolder\UsesPerDirectoryAsPattern ✓ closure was bound to CustomTestCase PASS Tests\PHPUnit\GlobPatternTests\SubFolder2\UsesPerFileAsPattern ✓ closure was bound to CustomTestCase PASS Tests\Playground ✓ basic PASS Tests\Plugins\Coverage ✓ compute comparable coverage with (0, 0) ✓ compute comparable coverage with (0.5, 0.5) ✓ compute comparable coverage with (1.0, 1.0) ✓ compute comparable coverage with (32.51, 32.5) ✓ compute comparable coverage with (32.12312321312312, 32.1) ✓ compute comparable coverage with (32.53333333333333, 32.5) ✓ compute comparable coverage with (32.57777771232132, 32.5) ✓ compute comparable coverage with (100.0, 100.0) PASS Tests\Plugins\Traits ✓ it allows global uses ✓ it allows multiple global uses registered in the same path PASS Tests\Unit\Configuration\In ✓ it proxies to uses call PASS Tests\Unit\Configuration\Theme ✓ it creates a printer instance PASS Tests\Unit\Console\Help ✓ it outputs the help information when --help is used PASS Tests\Unit\DatasetsTests ✓ it show only the names of named datasets in their description ✓ it show the actual dataset of non-named datasets in their description ✓ it show only the names of multiple named datasets in their description ✓ it show the actual dataset of multiple non-named datasets in their description ✓ it show the correct description for mixed named and not-named datasets ✓ it shows the correct description for long texts with newlines ✓ it shows the correct description for arrays with many elements ✓ it shows the correct description of datasets with html PASS Tests\Unit\Expectations\OppositeExpectation ✓ it throw expectation failed exception with string argument ✓ it throw expectation failed exception with array argument PASS Tests\Unit\Overrides\ThrowableBuilder ✓ collision editor can be added to the stack trace PASS Tests\Unit\Plugins\Concerns\HandleArguments ✓ method hasArgument with ('--long-argument', true) ✓ method hasArgument with ('-a', true) ✓ method hasArgument with ('--with-equal-sign', true) ✓ method hasArgument with ('someValue', true) ✓ method hasArgument with ('--a', false) ✓ method hasArgument with ('--undefined-argument', false) PASS Tests\Unit\Plugins\Environment ✓ environment is set to CI when --ci option is used ✓ environment is set to Local when --ci option is not used PASS Tests\Unit\Plugins\Retry ✓ it orders by defects and stop on defects if when --retry is used PASS Tests\Unit\Preset ✓ preset invalid name ✓ preset → myFramework PASS Tests\Unit\Support\Arr ✓ last → it should return false for an empty arary ✓ last → it should return the last element for an array with a single element ✓ last → it should return the last element for an array without changing the internal pointer ✓ last → it should return the last element for an associative array without changing the internal pointer ✓ last → it should return the last element for an mixed key array without changing the internal pointer PASS Tests\Unit\Support\Backtrace ✓ it gets file name from called file PASS Tests\Unit\Support\Container ✓ it exists ✓ it gets an instance ✓ autowire ✓ it creates an instance and resolves parameters ✓ it creates an instance and resolves also sub parameters ✓ it can resolve builtin value types ✓ it cannot resolve a parameter without type PASS Tests\Unit\Support\DatasetInfo ✓ it can check if dataset is defined inside a Datasets directory with ('/var/www/project/tests/Datase…rs.php', true) ✓ it can check if dataset is defined inside a Datasets directory with ('/var/www/project/tests/Datasets.php', false) ✓ it can check if dataset is defined inside a Datasets directory with ('/var/www/project/tests/Featur…rs.php', true) ✓ it can check if dataset is defined inside a Datasets directory with ('/var/www/project/tests/Featur…rs.php', false) ✓ it can check if dataset is defined inside a Datasets directory with ('/var/www/project/tests/Featur…ts.php', false) ✓ it can check if dataset is defined inside a Datasets.php file with ('/var/www/project/tests/Datase…rs.php', false) ✓ it can check if dataset is defined inside a Datasets.php file with ('/var/www/project/tests/Datasets.php', true) ✓ it can check if dataset is defined inside a Datasets.php file with ('/var/www/project/tests/Featur…rs.php', false) #1 ✓ it can check if dataset is defined inside a Datasets.php file with ('/var/www/project/tests/Featur…rs.php', false) #2 ✓ it can check if dataset is defined inside a Datasets.php file with ('/var/www/project/tests/Featur…ts.php', true) ✓ it computes the dataset scope with ('/var/www/project/tests/Datase…rs.php', '/var/www/project/tests') ✓ it computes the dataset scope with ('/var/www/project/tests/Datasets.php', '/var/www/project/tests') ✓ it computes the dataset scope with ('/var/www/project/tests/Featur…rs.php', '/var/www/project/tests/Features') ✓ it computes the dataset scope with ('/var/www/project/tests/Featur…rs.php', '/var/www/project/tests/Featur…rs.php') #1 ✓ it computes the dataset scope with ('/var/www/project/tests/Featur…ts.php', '/var/www/project/tests/Features') ✓ it computes the dataset scope with ('/var/www/project/tests/Featur…rs.php', '/var/www/project/tests/Featur…ollers') ✓ it computes the dataset scope with ('/var/www/project/tests/Featur…rs.php', '/var/www/project/tests/Featur…rs.php') #2 ✓ it computes the dataset scope with ('/var/www/project/tests/Featur…ts.php', '/var/www/project/tests/Featur…ollers') PASS Tests\Unit\Support\ExceptionTrace ✓ it ensures the given closures reports the correct class name ✓ it ensures the given closures reports the correct class name and suggests the [pest()] function PASS Tests\Unit\Support\HigherOrderMessage ✓ undefined method exceptions PASS Tests\Unit\Support\Reflection ✓ it gets file name from closure ✓ it gets property values ✓ it gets properties from classes ✓ it gets methods from classes PASS Tests\Unit\Support\Str ✓ it evaluates the code with ('version()', '__pest_evaluable_version__') ✓ it evaluates the code with ('version__ ', '__pest_evaluable_version_____') ✓ it evaluates the code with ('version\', '__pest_evaluable_version_') PASS Tests\Unit\TestName ✓ it may start with P with ('P\Tests\BarTest', 'Tests\BarTest') ✓ it may start with P with ('P\Packages\Foo', 'Packages\Foo') ✓ it may start with P with ('P\PPPackages\Foo', 'PPPackages\Foo') ✓ it may start with P with ('PPPackages\Foo', 'PPPackages\Foo') #1 ✓ it may start with P with ('PPPackages\Foo', 'PPPackages\Foo') #2 ✓ test description ✓ test_description ✓ ふ+が+ ✓ ほげ ✓ 卜竹弓一十山 ✓ アゴデヸ ✓ !p8VrB ✓ &xe6VeKWF#n4 ✓ %%HurHUnw7zM! ✓ rundeliekend ✓ g%%c!Jt9$fy#Kf ✓ NRs*Gz2@hmB$W$BPD%%b2U%3P%z%apnwSX ✓ ÀĤ{¼÷ ✓ ìèéàòç ✓ زهراء المعادي ✓ الجبيهه ✓ الظهران ✓ Каролин ✓ অ্যান্টার্কটিকা ✓ Frýdek-Místek" ✓ Allingåbro& ✓ Κεντροαφρικανική Δημοκρατία ✓ آذربایجان غربی ✓ זימבבואה ✓ Belišće ✓ Գվատեմալա ✓ パプアニューギニア ✓ 富山県 ✓ Қарағанды ✓ Қостанай ✓ 안양시 동안구 ✓ Itālija ✓ Honningsvåg ✓ Águeda ✓ Râșcani ✓ Năsăud ✓ Орехово-Зуево ✓ Čereňany ✓ Moravče ✓ Šentjernej ✓ Врање ✓ Крушевац ✓ Åkersberga ✓ บอสเนียและเฮอร์เซโกวีนา ✓ Birleşik Arap Emirlikleri ✓ Німеччина ✓ Nam Định ✓ 呼和浩特 ✓ test /** with comment */ should do PASS Tests\Unit\TestSuite ✓ it does not allow to add the same test description twice ✓ it does not allow static closures ✓ it alerts users about tests with arguments but no input ✓ it can return an array of all test suite filenames PASS Tests\Visual\BeforeEachTestName ✓ description ✓ latest description PASS Tests\Visual\Collision ✓ collision with (['']) PASS Tests\Visual\Help ✓ visual snapshot of help command output WARN Tests\Visual\JUnit ✓ junit output - junit with parallel → Not working yet PASS Tests\Visual\Parallel ✓ parallel ✓ a parallel test can extend another test with same name PASS Tests\Visual\SingleTestOrDirectory ✓ allows to run a single test ✓ allows to run a directory ✓ it disable decorating printer when colors is set to never WARN Tests\Visual\Success - visual snapshot of test suite on success WARN Tests\Visual\TeamCity - visual snapshot of team city with ('Failure.php') - visual snapshot of team city with ('SuccessOnly.php') WARN Tests\Visual\Todo - todos - todos in parallel - todo - todo in parallel WARN Tests\Visual\Version - visual snapshot of help command output PASS Testsexternal\Features\Expect\toMatchSnapshot ✓ pass with dataset with ('my-datas-set-value') ✓ within describe → pass with dataset with ('my-datas-set-value') Tests: 2 deprecated, 4 warnings, 5 incomplete, 2 notices, 39 todos, 35 skipped, 1188 passed (2813 assertions) ================================================ FILE: tests/.tests/Failure.php ================================================ toEqual(false); }); it('can be ignored because of no assertions', function () { }); it('can be ignored because it is skipped', function () { expect(true)->toBeTrue(); })->skip("this is why"); it('can fail', function () { $this->fail("oh noo"); }); it('throws exception', function () { throw new Exception('test error'); }); it('is not done yet', function () { })->todo(); todo("build this one."); it('is passing', function () { expect(true)->toEqual(true); }); ================================================ FILE: tests/.tests/SuccessOnly.php ================================================ toEqual(true); }); test('can also pass', function () { expect("string")->toBeString(); }); 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]); }); ================================================ FILE: tests/Arch.php ================================================ preset()->php()->ignoring([ Expectation::class, 'debug_backtrace', 'var_export', 'xdebug_info', ]); arch()->preset()->strict()->ignoring([ 'usleep', ]); arch()->preset()->security()->ignoring([ 'eval', 'str_shuffle', 'exec', 'unserialize', 'extract', 'assert', ]); arch('globals') ->expect(['dd', 'dump', 'ray', 'die', 'var_dump', 'sleep']) ->not->toBeUsed() ->ignoring(Expectation::class); arch('contracts') ->expect('Pest\Contracts') ->toOnlyUse([ 'NunoMaduro\Collision\Contracts', 'Pest\Factories\TestCaseMethodFactory', 'Symfony\Component\Console', 'Pest\Arch\Contracts', 'Pest\PendingCalls', ])->toBeInterfaces(); ================================================ FILE: tests/Autoload.php ================================================ assertTrue(true); } } trait SecondPluginTrait { public function assertSecondPluginTraitGotRegistered(): void { $this->assertTrue(true); } } Plugin::uses(PluginTrait::class); Plugin::uses(SecondPluginTrait::class); function _assertThat() { expect(true)->toBeTrue(); } ================================================ FILE: tests/Datasets/Bound.php ================================================ toBeString(); }); ================================================ FILE: tests/Features/After.php ================================================ count = 0; }); afterEach(function () { match ($this->name()) { '__pest_evaluable_it_can_run_after_test' => expect($this->count)->toBe(1), '__pest_evaluable_it_can_run_after_test_twice' => expect($this->count)->toBe(1), '__pest_evaluable_it_does_not_run_when_skipped' => expect($this->count)->toBe(0), '__pest_evaluable__something__→_it_does_not_run_when_skipped' => expect($this->count)->toBe(0), '__pest_evaluable__something__→_it_can_run_after_test' => expect($this->count)->toBe(1), '__pest_evaluable__something_2__→_it_can_run_after_test' => expect($this->count)->toBe(1), '__pest_evaluable_high_order_test' => expect($this->count)->toBe(1), '__pest_evaluable_high_order_test_with_skip' => expect($this->count)->toBe(0), '__pest_evaluable_post__foo__→_defer_Closure_Object____→_expect_Closure_Object____→_toBe_1' => expect($this->count)->toBe(1), default => $this->fail('Unexpected test name: '.$this->name()), }; $this->count++; }); it('can run after test', function () { expect($this->count)->toBe(0); $this->count++; })->after(function () { expect($this->count)->toBe(2); $this->count++; }); it('can run after test twice', function () { expect($this->count)->toBe(0); $this->count++; })->after(function () { expect($this->count)->toBe(2); $this->count++; })->after(function () { expect($this->count)->toBe(3); $this->count++; }); it('does not run when skipped', function () { dd('This should not run 1'); })->skip()->after(function () { dd('This should not run 2'); }); afterEach(function () { match ($this->name()) { '__pest_evaluable_it_can_run_after_test' => expect($this->count)->toBe(3), '__pest_evaluable_it_can_run_after_test_twice' => expect($this->count)->toBe(4), '__pest_evaluable_it_does_not_run_when_skipped' => expect($this->count)->toBe(1), '__pest_evaluable__something__→_it_does_not_run_when_skipped' => expect($this->count)->toBe(1), '__pest_evaluable__something__→_it_can_run_after_test' => expect($this->count)->toBe(2), '__pest_evaluable__something_2__→_it_can_run_after_test' => expect($this->count)->toBe(2), '__pest_evaluable_high_order_test' => expect($this->count)->toBe(2), '__pest_evaluable_high_order_test_with_skip' => expect($this->count)->toBe(1), '__pest_evaluable_post__foo__→_defer_Closure_Object____→_expect_Closure_Object____→_toBe_1' => expect($this->count)->toBe(2), default => $this->fail('Unexpected test name: '.$this->name()), }; $this->count++; }); afterEach(function () { match ($this->name()) { '__pest_evaluable_it_can_run_after_test' => expect($this->count)->toBe(4), '__pest_evaluable_it_can_run_after_test_twice' => expect($this->count)->toBe(5), '__pest_evaluable_it_does_not_run_when_skipped' => expect($this->count)->toBe(2), '__pest_evaluable__something__→_it_does_not_run_when_skipped' => expect($this->count)->toBe(2), '__pest_evaluable__something__→_it_can_run_after_test' => expect($this->count)->toBe(3), '__pest_evaluable__something_2__→_it_can_run_after_test' => expect($this->count)->toBe(3), '__pest_evaluable_high_order_test' => expect($this->count)->toBe(3), '__pest_evaluable_high_order_test_with_skip' => expect($this->count)->toBe(2), '__pest_evaluable_post__foo__→_defer_Closure_Object____→_expect_Closure_Object____→_toBe_1' => expect($this->count)->toBe(3), default => $this->fail('Unexpected test name: '.$this->name()), }; $this->count++; }); describe('something', function () { it('does not run when skipped', function () { dd('This should not run 3'); })->skip()->after(function () { dd('This should not run 4'); }); it('can run after test', function () { expect($this->count)->toBe(0); $this->count++; })->after(function () { expect($this->count)->toBe(5); $this->count++; })->after(function () { expect($this->count)->toBe(6); $this->count++; }); })->after(function () { expect($this->count)->toBe(4); $this->count++; }); describe('something 2', function () { it('can run after test', function () { expect($this->count)->toBe(0); $this->count++; })->after(function () { expect($this->count)->toBe(6); $this->count++; }); })->after(function () { expect($this->count)->toBe(4); $this->count++; })->after(function () { expect($this->count)->toBe(5); $this->count++; }); test('high order test') ->defer(fn () => $this->count++) ->expect(fn () => $this->count)->toBe(1) ->after(function () { expect($this->count)->toBe(4); $this->count++; }); test('high order test with skip') ->skip() ->defer(fn () => $this->count++) ->expect(fn () => $this->count)->toBe(1) ->after(function () { dd('This should not run 5'); }); pest()->use(Postable::class); /** * @return TestCase|TestCall|Gettable */ function post(string $route) { return test()->post($route); } trait Postable { /** * @return TestCase|TestCall|Gettable */ public function post(string $route) { expect($route)->not->toBeEmpty(); return $this; } } post('foo')->defer(fn () => $this->count++)->expect(fn () => $this->count)->toBe(1); ================================================ FILE: tests/Features/AfterAll.php ================================================ assertFileExists($file); register_shutdown_function(function () { // $this->assertFileDoesNotExist($file); }); }); ================================================ FILE: tests/Features/AfterEach.php ================================================ state = $state; }); afterEach(function () { $this->state->bar = 1; }); afterEach(function () { unset($this->state->bar); }); it('does not get executed before the test', function () { expect($this->state)->not->toHaveProperty('bar'); }); it('gets executed after the test', function () { expect($this->state)->toHaveProperty('bar'); expect($this->state->bar)->toBe(2); }); afterEach(function () { $this->state->bar = 2; }); describe('outer', function () { afterEach(function () { $this->state->bar++; }); describe('inner', function () { afterEach(function () { $this->state->bar++; }); it('does not get executed before the test', function () { expect($this->state)->toHaveProperty('bar'); expect($this->state->bar)->toBe(2); }); it('should call all parent afterEach functions', function () { expect($this->state)->toHaveProperty('bar'); expect($this->state->bar)->toBe(4); }); }); }); describe('matching describe block names', function () { afterEach(function () { $this->state->foo = 1; }); describe('outer', function () { afterEach(function () { $this->state->foo++; }); describe('middle', function () { afterEach(function () { $this->state->foo++; }); describe('inner', function () { afterEach(function () { $this->state->foo++; }); it('does not get executed before the test', function () { expect($this)->not->toHaveProperty('foo'); }); it('should call all parent afterEach functions', function () { expect($this->state->foo)->toBe(4); }); }); }); describe('middle', function () { it('does not get executed before the test', function () { expect($this)->not->toHaveProperty('foo'); }); it('should not call afterEach functions for sibling describe blocks with the same name', function () { expect($this)->not->toHaveProperty('foo'); }); }); describe('inner', function () { it('does not get executed before the test', function () { expect($this)->not->toHaveProperty('foo'); }); it('should not call afterEach functions for descendent of sibling describe blocks with the same name', function () { expect($this)->not->toHaveProperty('foo'); }); }); }); }); ================================================ FILE: tests/Features/Assignee.php ================================================ toBeTrue(); })->assignee('nunomaduro'); it('may be associated with an assignee', function () { expect(true)->toBeTrue(); })->assignee('taylorotwell'); describe('nested', function () { it('may be associated with an assignee', function () { expect(true)->toBeTrue(); })->assignee('taylorotwell'); })->assignee('nunomaduro')->note('an note between an the assignee')->assignee(['jamesbrooks', 'joedixon']); ================================================ FILE: tests/Features/BeforeAll.php ================================================ bar = 0; beforeAll(function () use ($foo) { $foo->bar++; }); it('gets executed before tests', function () use ($foo) { expect($foo)->bar->toBe(1); $foo->bar = 'changed'; }); it('do not get executed before each test', function () use ($foo) { expect($foo)->bar->toBe('changed'); }); ================================================ FILE: tests/Features/BeforeEach.php ================================================ bar = 2; }); beforeEach(function () { $this->bar++; }); beforeEach(function () { $this->bar = 0; }); it('gets executed before each test', function () { expect($this->bar)->toBe(1); $this->bar = 'changed'; }); it('gets executed before each test once again', function () { expect($this->bar)->toBe(1); }); beforeEach(function () { $this->bar++; }); describe('outer', function () { beforeEach(function () { $this->bar++; }); describe('inner', function () { beforeEach(function () { $this->bar++; }); it('should call all parent beforeEach functions', function () { expect($this->bar)->toBe(3); }); }); }); describe('with expectations', function () { beforeEach()->expect(true)->toBeTrue(); describe('nested block', function () { test('test', function () {}); }); test('test', function () {}); }); describe('matching describe block names', function () { beforeEach(function () { $this->foo = 1; }); describe('outer', function () { beforeEach(function () { $this->foo++; }); describe('middle', function () { beforeEach(function () { $this->foo++; }); describe('inner', function () { beforeEach(function () { $this->foo++; }); it('should call all parent beforeEach functions', function () { expect($this->foo)->toBe(4); }); }); }); describe('middle', function () { it('should not call beforeEach functions for sibling describe blocks with the same name', function () { expect($this->foo)->toBe(2); }); }); describe('inner', function () { it('should not call beforeEach functions for descendent of sibling describe blocks with the same name', function () { expect($this->foo)->toBe(2); }); }); }); }); $matchingNameCalls = 0; describe('matching name', function () use (&$matchingNameCalls) { beforeEach(function () use (&$matchingNameCalls) { $matchingNameCalls++; }); it('should call the before each', function () use (&$matchingNameCalls) { expect($matchingNameCalls)->toBe(1); }); }); describe('matching name', function () use (&$matchingNameCalls) { it('should not call the before each on the describe block with the same name', function () use (&$matchingNameCalls) { expect($matchingNameCalls)->toBe(1); }); }); beforeEach(function () { $this->baz = 1; }); describe('called on all tests', function () { beforeEach(function () { $this->baz++; }); test('beforeEach should be called', function () { expect($this->baz)->toBe(2); }); test('beforeEach should be called for all tests', function () { expect($this->baz)->toBe(2); }); }); ================================================ FILE: tests/Features/BeforeEachProxiesToTestCallWithExpectations.php ================================================ expect(true)->toBeTrue(); test('runs 1', function () { // This test did performs assertions... }); test('runs 2', function () { // This test did performs assertions... }); test('runs 3', function () { // This test did performs assertions... }); ================================================ FILE: tests/Features/BeforeEachProxiesToTestCallWithSkip.php ================================================ skip(); test('does not run 1', function () { $this->fail('This test should not run'); }); test('does not run 2', function () { $this->fail('This test should not run'); }); test('does not run 3', function () { $this->fail('This test should not run'); }); ================================================ FILE: tests/Features/BeforeEachProxiesToTestCallWithTodo.php ================================================ todo(); test('is marked as todo 1', function () { $this->fail('This test should not run'); }); test('is marked as todo 2', function () { $this->fail('This test should not run'); }); test('is marked as todo 3'); test()->shouldBeMarkedAsTodo(); ================================================ FILE: tests/Features/Coverage.php ================================================ assertTrue(class_exists(CoveragePlugin::class)); it('adds coverage if --coverage exist', function () { $plugin = new CoveragePlugin(new ConsoleOutput); expect($plugin->coverage)->toBeFalse(); $arguments = $plugin->handleArguments([]); expect($arguments)->toEqual([]) ->and($plugin->coverage)->toBeFalse(); $arguments = $plugin->handleArguments(['--coverage']); expect($arguments)->toEqual(['--coverage-php', Coverage::getPath()]) ->and($plugin->coverage)->toBeTrue(); })->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); expect($plugin->coverageMin)->toEqual(0.0) ->and($plugin->coverage)->toBeFalse(); $plugin->handleArguments([]); expect($plugin->coverageMin)->toEqual(0.0); $plugin->handleArguments(['--min=2']); expect($plugin->coverageMin)->toEqual(2.0); $plugin->handleArguments(['--min=2.4']); expect($plugin->coverageMin)->toEqual(2.4); }); it('generates coverage based on file input', function () { expect(Coverage::getMissingCoverage(new class { public function lineCoverageData(): array { return [ 1 => ['foo'], 2 => ['bar'], 4 => [], 5 => [], 6 => [], 7 => null, 100 => null, 101 => ['foo'], 102 => [], ]; } }))->toEqual([ '4..6', '102', ]); }); ================================================ FILE: tests/Features/Covers/ClassCoverage.php ================================================ getAttributes(); expect($attributes[1]->getName())->toBe(CoversClass::class); expect($attributes[1]->getArguments()[0])->toBe('Tests\Fixtures\Covers\CoversClass1'); }); ================================================ FILE: tests/Features/Covers/ExceptionHandling.php ================================================ 'closure'); $testCall->covers('fakeName'); })->throws(InvalidArgumentException::class, 'No class, trait or method named "fakeName" has been found.'); ================================================ FILE: tests/Features/Covers/FunctionCoverage.php ================================================ getAttributes(); expect($attributes[1]->getName())->toBe(CoversFunction::class); expect($attributes[1]->getArguments()[0])->toBe('testCoversFunction'); })->coversFunction('testCoversFunction'); ================================================ FILE: tests/Features/Covers/GuessCoverage.php ================================================ getAttributes(); expect($attributes[1]->getName())->toBe(CoversClass::class); expect($attributes[1]->getArguments()[0])->toBe(CoversClass3::class); expect($attributes[2]->getName())->toBe(CoversFunction::class); expect($attributes[2]->getArguments()[0])->toBe('testCoversFunction2'); })->covers(CoversClass3::class, 'testCoversFunction2'); ================================================ FILE: tests/Features/Covers/TraitCoverage.php ================================================ getAttributes(); expect($attributes[1]->getName())->toBe(PHPUnitCoversTrait::class); expect($attributes[1]->getArguments()[0])->toBe('Tests\Fixtures\Covers\CoversTrait'); })->coversTrait(CoversTrait::class); ================================================ FILE: tests/Features/DatasetsTests.php ================================================ foo = 'bar'; }); it('throws exception if dataset does not exist', function () { $this->expectException(DatasetDoesNotExist::class); $this->expectExceptionMessage("A dataset with the name `first` does not exist. You can create it using `dataset('first', ['a', 'b']);`."); DatasetsRepository::resolve(['first'], __FILE__); }); it('throws exception if dataset already exist', function () { DatasetsRepository::set('second', [[]], __DIR__); $this->expectException(DatasetAlreadyExists::class); $this->expectExceptionMessage('A dataset with the name `second` already exists in scope ['.__DIR__.'].'); DatasetsRepository::set('second', [[]], __DIR__); }); it('sets closures', function () { DatasetsRepository::set('foo', function () { yield [1]; }, __DIR__); expect(DatasetsRepository::resolve(['foo'], __FILE__))->toBe(['(1)' => [1]]); }); it('sets arrays', function () { DatasetsRepository::set('bar', [[2]], __DIR__); expect(DatasetsRepository::resolve(['bar'], __FILE__))->toBe(['(2)' => [2]]); }); it('gets bound to test case object', function ($value) { $this->assertTrue(true); })->with([['a'], ['b']]); test('it truncates the description', function () { expect(true)->toBe(true); // it gets tested by the integration test })->with([str_repeat('Fooo', 10)]); $state = new stdClass; $state->text = ''; $datasets = [[1], [2]]; test('lazy datasets', function ($text) use ($state, $datasets) { $state->text .= $text; expect(in_array([$text], $datasets))->toBe(true); })->with($datasets); test('lazy datasets did the job right', function () use ($state) { expect($state->text)->toBe('12'); }); test('interpolated :dataset lazy datasets', function ($text) { expect(true)->toBeTrue(); })->with($datasets); $state->text = ''; test('eager datasets', function ($text) use ($state, $datasets) { $state->text .= $text; expect($datasets)->toContain([$text]); })->with(function () use ($datasets) { return $datasets; }); test('eager datasets did the job right', function () use ($state) { expect($state->text)->toBe('1212'); }); test('lazy registered datasets', function ($text) use ($state, $datasets) { $state->text .= $text; expect($datasets)->toContain([$text]); })->with('numbers.array'); test('lazy registered datasets did the job right', function () use ($state) { expect($state->text)->toBe('121212'); }); test('eager registered datasets', function ($text) use ($state, $datasets) { $state->text .= $text; expect($datasets)->toContain([$text]); })->with('numbers.closure'); test('eager registered datasets did the job right', function () use ($state) { expect($state->text)->toBe('12121212'); }); test('eager wrapped registered datasets', function ($text) use ($state, $datasets) { $state->text .= $text; expect($datasets)->toContain([$text]); })->with('numbers.closure.wrapped'); test('eager registered wrapped datasets did the job right', function () use ($state) { expect($state->text)->toBe('1212121212'); }); test('named datasets', function ($text) use ($state, $datasets) { $state->text .= $text; expect($datasets)->toContain([$text]); })->with([ 'one' => [1], 'two' => [2], ]); test('interpolated :dataset named datasets', function ($text) { expect(true)->toBeTrue(); })->with([ 'one' => [1], 'two' => [2], ]); test('named datasets did the job right', function () use ($state) { expect($state->text)->toBe('121212121212'); }); class Bar { public $name = 1; } $namedDatasets = [ new Bar, ]; test('lazy named datasets', function ($text) { expect(true)->toBeTrue(); })->with($namedDatasets); $counter = 0; it('creates unique test case names', function (string $name, Plugin $plugin, bool $bool) use (&$counter) { expect(true)->toBeTrue(); $counter++; })->with([ ['Name 1', new Plugin, true], ['Name 1', new Plugin, true], ['Name 1', new Plugin, false], ['Name 2', new Plugin, false], ['Name 2', new Plugin, true], ['Name 1', new Plugin, true], ]); it('creates unique test case names - count', function () use (&$counter) { expect($counter)->toBe(6); }); $datasets_a = [[1], [2]]; $datasets_b = [[3], [4]]; test('lazy multiple datasets', function ($text_a, $text_b) use ($state, $datasets_a, $datasets_b) { $state->text .= $text_a.$text_b; expect($datasets_a)->toContain([$text_a]); expect($datasets_b)->toContain([$text_b]); })->with($datasets_a, $datasets_b); test('lazy multiple datasets did the job right', function () use ($state) { expect($state->text)->toBe('12121212121213142324'); }); $state->text = ''; test('eager multiple datasets', function ($text_a, $text_b) use ($state, $datasets_a, $datasets_b) { $state->text .= $text_a.$text_b; expect($datasets_a)->toContain([$text_a]); expect($datasets_b)->toContain([$text_b]); })->with(function () use ($datasets_a) { return $datasets_a; })->with(function () use ($datasets_b) { return $datasets_b; }); test('eager multiple datasets did the job right', function () use ($state) { expect($state->text)->toBe('1212121212121314232413142324'); }); test('lazy registered multiple datasets', function ($text_a, $text_b) use ($state, $datasets) { $state->text .= $text_a.$text_b; expect($datasets)->toContain([$text_a]); expect($datasets)->toContain([$text_b]); })->with('numbers.array')->with('numbers.array'); test('lazy registered multiple datasets did the job right', function () use ($state) { expect($state->text)->toBe('121212121212131423241314232411122122'); }); test('eager registered multiple datasets', function ($text_a, $text_b) use ($state, $datasets) { $state->text .= $text_a.$text_b; expect($datasets)->toContain([$text_a]); expect($datasets)->toContain([$text_b]); })->with('numbers.array')->with('numbers.closure'); test('eager registered multiple datasets did the job right', function () use ($state) { expect($state->text)->toBe('12121212121213142324131423241112212211122122'); }); test('eager wrapped registered multiple datasets', function ($text_a, $text_b) use ($state, $datasets) { $state->text .= $text_a.$text_b; expect($datasets)->toContain([$text_a]); expect($datasets)->toContain([$text_b]); })->with('numbers.closure.wrapped')->with('numbers.closure'); test('eager wrapped registered multiple datasets did the job right', function () use ($state) { expect($state->text)->toBe('1212121212121314232413142324111221221112212211122122'); }); test('named multiple datasets', function ($text_a, $text_b) use ($state, $datasets_a, $datasets_b) { $state->text .= $text_a.$text_b; expect($datasets_a)->toContain([$text_a]); expect($datasets_b)->toContain([$text_b]); })->with([ 'one' => [1], 'two' => [2], ])->with([ 'three' => [3], 'four' => [4], ]); test('named multiple datasets did the job right', function () use ($state) { expect($state->text)->toBe('121212121212131423241314232411122122111221221112212213142324'); }); test('more than two datasets', function ($text_a, $text_b, $text_c) use ($state, $datasets_a, $datasets_b) { $state->text .= $text_a.$text_b.$text_c; expect($datasets_a)->toContain([$text_a]); expect($datasets_b)->toContain([$text_b]); expect([5, 6])->toContain($text_c); })->with($datasets_a, $datasets_b)->with([5, 6]); test('more than two datasets did the job right', function () use ($state) { expect($state->text)->toBe('121212121212131423241314232411122122111221221112212213142324135136145146235236245246'); }); $wrapped_generator_state = new stdClass; $wrapped_generator_state->text = ''; $wrapped_generator_function_datasets = [1, 2, 3, 4]; test( 'eager registered wrapped datasets with Generator functions', function (int $text) use ( $wrapped_generator_state, $wrapped_generator_function_datasets ) { $wrapped_generator_state->text .= $text; expect(in_array($text, $wrapped_generator_function_datasets))->toBe(true); } )->with('numbers.generators.wrapped'); test('eager registered wrapped datasets with Generator functions did the job right', function () use ($wrapped_generator_state) { expect($wrapped_generator_state->text)->toBe('1234'); }); test('eager registered wrapped datasets with Generator functions display description', function ($wrapped_generator_state_with_description) { expect($wrapped_generator_state_with_description)->not->toBeEmpty(); })->with(function () { yield 'taylor' => 'taylor@laravel.com'; yield 'james' => 'james@laravel.com'; }); it('can resolve a dataset after the test case is available', function ($result) { expect($result)->toBe('bar'); })->with([ function () { return $this->foo; }, [ function () { return $this->foo; }, ], ]); it('can resolve a dataset after the test case is available with multiple datasets', function (string $result, string $result2) { expect($result)->toBe('bar'); })->with([ function () { return $this->foo; }, [ function () { return $this->foo; }, ], ], [ function () { return $this->foo; }, [ function () { return $this->foo; }, ], ]); it('can resolve a dataset after the test case is available with shared yield sets', function ($result) { expect($result)->toBeInt()->toBeLessThan(3); })->with('bound.closure'); it('can resolve a dataset after the test case is available with shared array sets', function ($result) { expect($result)->toBeInt()->toBeLessThan(3); })->with('bound.array'); it('resolves a potential bound dataset logically', function ($foo, $bar) { expect($foo)->toBe('foo'); expect($bar())->toBe('bar'); })->with([ [ 'foo', function () { return 'bar'; }, ], // This should be passed as a closure because we've passed multiple arguments ]); it('resolves a potential bound dataset logically even when the closure comes first', function ($foo, $bar) { expect($foo())->toBe('foo'); expect($bar)->toBe('bar'); })->with([ [ function () { return 'foo'; }, 'bar', ], // This should be passed as a closure because we've passed multiple arguments ]); it('will not resolve a closure if it is type hinted as a closure', function (Closure $data) { expect($data())->toBeString(); })->with([ function () { return 'foo'; }, function () { return 'bar'; }, ]); it('will not resolve a closure if it is type hinted as a callable', function (callable $data) { expect($data())->toBeString(); })->with([ function () { return 'foo'; }, function () { return 'bar'; }, ]); it('can correctly resolve a bound dataset that returns an array', function (array $data) { expect($data)->toBe(['foo', 'bar', 'baz']); })->with([ function () { return ['foo', 'bar', 'baz']; }, ]); it('can correctly resolve a bound dataset that returns an array but wants to be spread', function (string $foo, string $bar, string $baz) { expect([$foo, $bar, $baz])->toBe(['foo', 'bar', 'baz']); })->with([ function () { return ['foo', 'bar', 'baz']; }, ]); todo('forbids to define tests in Datasets dirs and Datasets.php files'); dataset('greeting-string', [ 'formal' => 'Evening', 'informal' => 'yo', ]); it('may be used with high order') ->with('greeting-string') ->expect(fn (string $greeting) => $greeting) ->throwsNoExceptions(); dataset('greeting-bound', [ 'formal' => fn () => 'Evening', 'informal' => fn () => 'yo', ]); it('may be used with high order even when bound') ->with('greeting-bound') ->expect(fn (string $greeting) => $greeting) ->throws(InvalidArgumentException::class); describe('with on nested describe', function () { describe('nested', function () { test('before inner describe block', function (...$args) { expect($args)->toBe([1]); }); describe('describe', function () { it('should include the with value from all parent describe blocks', function (...$args) { expect($args)->toBe([1, 2]); }); test('should include the with value from all parent describe blocks and the test', function (...$args) { expect($args)->toBe([1, 2, 3]); })->with([3]); })->with([2]); test('after inner describe block', function (...$args) { expect($args)->toBe([1]); }); })->with([1]); }); describe('matching describe block names', function () { describe('outer', function () { test('before inner describe block', function (...$args) { expect($args)->toBe([1]); }); describe('inner', function () { it('should include the with value from all parent describe blocks', function (...$args) { expect($args)->toBe([1, 2]); }); test('should include the with value from all parent describe blocks and the test', function (...$args) { expect($args)->toBe([1, 2, 3]); })->with([3]); })->with([2]); describe('inner', function () { it('should not include the value from the other describe block with the same name', function (...$args) { expect($args)->toBe([1]); }); }); test('after inner describe block', function (...$args) { expect($args)->toBe([1]); }); })->with([1]); }); test('after describe block', function (...$args) { expect($args)->toBe([5]); })->with([5]); it('may be used with high order after describe block') ->with('greeting-string') ->expect(fn (string $greeting) => $greeting) ->throwsNoExceptions(); dataset('after-describe', ['after']); test('after describe block with named dataset', function (...$args) { expect($args)->toBe(['after']); })->with('after-describe'); ================================================ FILE: tests/Features/Depends.php ================================================ toBeTrue(); $runCounter++; return 'first'; }); test('second', function () use (&$runCounter) { expect(true)->toBeTrue(); $runCounter++; return 'second'; }); test('depends', function () { expect(func_get_args())->toBe(['first', 'second']); })->depends('first', 'second'); test('depends with ...params', function (string ...$params) { expect(func_get_args())->toBe($params); })->depends('first', 'second'); test('depends with defined arguments', function (string $first, string $second) { expect($first)->toBe('first'); expect($second)->toBe('second'); })->depends('first', 'second'); test('depends run test only once', function () use (&$runCounter) { expect($runCounter)->toBe(2); })->depends('first', 'second'); // Regression tests. See https://github.com/pestphp/pest/pull/216 it('asserts true is true')->assertTrue(true); test('depends works with the correct test name')->assertTrue(true)->depends('it asserts true is true'); describe('describe block', function () { $runCounter = 0; test('first in describe', function () use (&$runCounter) { $runCounter++; expect(true)->toBeTrue(); }); test('second in describe', function () use (&$runCounter) { expect($runCounter)->toBe(1); $runCounter++; })->depends('first in describe'); test('third in describe', function () use (&$runCounter) { expect($runCounter)->toBe(2); })->depends('second in describe'); describe('nested describe', function () { $runCounter = 0; test('first in nested describe', function () use (&$runCounter) { $runCounter++; expect(true)->toBeTrue(); }); test('second in nested describe', function () use (&$runCounter) { expect($runCounter)->toBe(1); $runCounter++; })->depends('first in nested describe'); test('third in nested describe', function () use (&$runCounter) { expect($runCounter)->toBe(2); })->depends('second in nested describe'); }); }); test('depends on test after describe block', function () use (&$runCounter) { expect($runCounter)->toBe(2); })->depends('first', 'second'); ================================================ FILE: tests/Features/DependsInheritance.php ================================================ extend(InheritanceTest::class); it('is a test', function () { expect(true)->toBeTrue(); }); it('uses correct parent class', function () { expect(get_parent_class($this))->toEqual(InheritanceTest::class); expect($this->foo())->toEqual('bar'); })->depends('it is a test'); ================================================ FILE: tests/Features/Deprecated.php ================================================ toBeTrue(); }); test('user deprecated', function () { trigger_error('Since foo 1.0: This is a deprecation description', \E_USER_DEPRECATED); expect(true)->toBeTrue(); }); ================================================ FILE: tests/Features/Describe.php ================================================ $this->count = 1); test('before each', function () { expect($this->count)->toBe(1); }); describe('hooks', function () { beforeEach(function () { $this->count++; }); test('value', function () { expect($this->count)->toBe(2); $this->count++; }); afterEach(function () { expect($this->count)->toBe(3); }); }); describe('hooks in different orders', function () { beforeEach(function () { $this->count++; }); test('value', function () { expect($this->count)->toBe(3); $this->count++; }); afterEach(function () { expect($this->count)->toBe(4); }); beforeEach(function () { $this->count++; }); }); test('todo')->todo()->shouldNotRun(); test('previous describable before each does not get applied here', function () { expect($this->count)->toBe(1); }); describe('todo on hook', function () { beforeEach()->todo(); test('should not fail')->shouldNotRun(); test('should run')->expect(true)->toBeTrue(); }); describe('todo on describe', function () { test('should not fail')->shouldNotRun(); test('should run')->expect(true)->toBeTrue(); })->todo(); test('should run')->expect(true)->toBeTrue(); test('with', fn ($foo) => expect($foo)->toBe(1))->with([1]); describe('with on hook', function () { beforeEach()->with([2]); test('value', function ($foo) { expect($foo)->toBe(2); }); }); describe('with on describe', function () { test('value', function ($foo) { expect($foo)->toBe(3); }); })->with([3]); describe('depends on describe', function () { test('foo', function () { expect('foo')->toBe('foo'); }); test('bar', function () { expect('bar')->toBe('bar'); })->depends('foo'); }); describe('depends on describe using with', function () { test('foo', function ($foo) { expect($foo)->toBe(3); }); test('bar', function ($foo) { expect($foo + $foo)->toBe(6); })->depends('foo'); })->with([3]); describe('with test after describe', function () { beforeEach(function () { $this->count++; }); describe('foo', function () {}); it('should run the before each', function () { expect($this->count)->toBe(2); }); }); ================================================ FILE: tests/Features/DescriptionLess.php ================================================ use(Gettable::class); /** * @return TestCase|TestCall|Gettable */ function get(string $route) { return test()->get($route); } trait Gettable { /** * @return TestCase|TestCall|Gettable */ public function get(string $route) { expect($route)->not->toBeEmpty(); return $this; } } get('foo'); // not incomplete because closure is created... get('foo')->get('bar')->expect(true)->toBeTrue(); get('foo')->expect(true)->toBeTrue(); describe('a "describe" group of tests', function () { get('foo'); // not incomplete because closure is created... get('foo')->get('bar')->expect(true)->toBeTrue(); get('foo')->expect(true)->toBeTrue(); }); ================================================ FILE: tests/Features/Done.php ================================================ toBeTrue(); })->done(assignee: 'nunomaduro'); it('may have an associated issue', function () { expect(true)->toBeTrue(); })->done(issue: 1); it('may have an associated PR', function () { expect(true)->toBeTrue(); })->done(pr: 1); it('may have an associated note', function () { expect(true)->toBeTrue(); })->done(note: 'a note'); ================================================ FILE: tests/Features/Exceptions.php ================================================ expectException(InvalidArgumentException::class); throw new InvalidArgumentException; }); it('catch exceptions', function () { throw new Exception('Something bad happened'); })->throws(Exception::class); it('catch exceptions and messages', function () { throw new Exception('Something bad happened'); })->throws(Exception::class, 'Something bad happened'); it('catch exceptions, messages and code', function () { throw new Exception('Something bad happened', 1); })->throws(Exception::class, 'Something bad happened', 1); it('can just define the message', function () { throw new Exception('Something bad happened'); })->throws('Something bad happened'); it('can just define the code', function () { throw new Exception('Something bad happened', 1); })->throws(1); it('not catch exceptions if given condition is false', function () { $this->assertTrue(true); })->throwsIf(false, Exception::class); it('catch exceptions if given condition is true', function () { throw new Exception('Something bad happened'); })->throwsIf(function () { return true; }, Exception::class); it('catch exceptions and messages if given condition is true', function () { throw new Exception('Something bad happened'); })->throwsIf(true, Exception::class, 'Something bad happened'); it('catch exceptions, messages and code if given condition is true', function () { throw new Exception('Something bad happened', 1); })->throwsIf(true, Exception::class, 'Something bad happened', 1); it('can just define the message if given condition is true', function () { throw new Exception('Something bad happened'); })->throwsIf(true, 'Something bad happened'); it('can just define the code if given condition is true', function () { throw new Exception('Something bad happened', 1); })->throwsIf(true, 1); it('can just define the message if given condition is 1', function () { throw new Exception('Something bad happened'); })->throwsIf(1, 'Something bad happened'); it('can just define the code if given condition is 1', function () { throw new Exception('Something bad happened', 1); })->throwsIf(1, 1); it('not catch exceptions if given condition is true', function () { $this->assertTrue(true); })->throwsUnless(true, Exception::class); it('catch exceptions if given condition is false', function () { throw new Exception('Something bad happened'); })->throwsUnless(function () { return false; }, Exception::class); it('catch exceptions and messages if given condition is false', function () { throw new Exception('Something bad happened'); })->throwsUnless(false, Exception::class, 'Something bad happened'); it('catch exceptions, messages and code if given condition is false', function () { throw new Exception('Something bad happened', 1); })->throwsUnless(false, Exception::class, 'Something bad happened', 1); it('can just define the message if given condition is false', function () { throw new Exception('Something bad happened'); })->throwsUnless(false, 'Something bad happened'); it('can just define the code if given condition is false', function () { throw new Exception('Something bad happened', 1); })->throwsUnless(false, 1); it('can just define the message if given condition is 0', function () { throw new Exception('Something bad happened'); })->throwsUnless(0, 'Something bad happened'); it('can just define the code if given condition is 0', function () { throw new Exception('Something bad happened', 1); })->throwsUnless(0, 1); ================================================ FILE: tests/Features/Expect/HigherOrder/methods.php ================================================ name()->toBeString()->toEqual('Has Methods'); }); it('can access multiple methods', function () { expect(new HasMethods) ->name()->toBeString()->toEqual('Has Methods') ->quantity()->toBeInt()->toEqual(20); }); it('works with not', function () { expect(new HasMethods) ->name()->not->toEqual('world')->toEqual('Has Methods') ->quantity()->toEqual(20)->not()->toEqual('bar')->not->toBeNull; }); it('can accept arguments', function () { expect(new HasMethods) ->multiply(5, 4)->toBeInt->toEqual(20); }); it('works with each', function () { expect(new HasMethods) ->attributes()->toBeArray->each->not()->toBeNull ->attributes()->each(function ($attribute) { $attribute->not->toBeNull(); }); }); it('works inside of each', function () { expect(new HasMethods) ->books()->each(function ($book) { $book->title->not->toBeNull->cost->toBeGreaterThan(19); }); }); it('works with sequence', function () { expect(new HasMethods) ->books()->sequence( function ($book) { $book->title->toEqual('Foo')->cost->toEqual(20); }, function ($book) { $book->title->toEqual('Bar')->cost->toEqual(30); }, ); }); it('can compose complex expectations', function () { expect(new HasMethods) ->toBeObject() ->name()->toEqual('Has Methods')->not()->toEqual('bar') ->quantity()->not->toEqual('world')->toEqual(20)->toBeInt ->multiply(3, 4)->not->toBeString->toEqual(12) ->attributes()->toBeArray() ->books()->toBeArray->each->not->toBeEmpty ->books()->sequence( function ($book) { $book->title->toEqual('Foo')->cost->toEqual(20); }, function ($book) { $book->title->toEqual('Bar')->cost->toEqual(30); }, ); }); it('can handle nested method calls', function () { expect(new HasMethods) ->newInstance()->newInstance()->name()->toEqual('Has Methods')->toBeString() ->newInstance()->name()->toEqual('Has Methods')->not->toBeInt ->name()->toEqual('Has Methods') ->books()->each->toBeArray(); }); it('works with higher order tests') ->expect(new HasMethods) ->newInstance()->newInstance()->name()->toEqual('Has Methods')->toBeString() ->newInstance()->name()->toEqual('Has Methods')->not->toBeArray ->name()->toEqual('Has Methods') ->books()->each->toBeArray; it('can use the scoped method to lock into the given level for expectations', function () { expect(new HasMethods) ->attributes()->scoped(fn ($attributes) => $attributes ->name->toBe('Has Methods') ->quantity->toBe(20) ) ->name()->toBeString()->toBe('Has Methods') ->newInstance()->newInstance()->scoped(fn ($instance) => $instance ->name()->toBe('Has Methods') ->quantity()->toBe(20) ->attributes()->scoped(fn ($attributes) => $attributes ->name->toBe('Has Methods') ->quantity->toBe(20) ) ); }); it('works consistently with the json expectation method', function () { expect(new HasMethods) ->jsonString()->json()->id->toBe(1) ->jsonString()->json()->name->toBe('Has Methods')->toBeString() ->jsonString()->json()->quantity->toBe(20)->toBeInt(); }); class HasMethods { public function jsonString(): string { return '{ "id": 1, "name": "Has Methods", "quantity": 20 }'; } public function name() { return 'Has Methods'; } public function quantity() { return 20; } public function multiply($x, $y) { return $x * $y; } public function attributes() { return [ 'name' => $this->name(), 'quantity' => $this->quantity(), ]; } public function books() { return [ [ 'title' => 'Foo', 'cost' => 20, ], [ 'title' => 'Bar', 'cost' => 30, ], ]; } public function newInstance() { return new static; } } ================================================ FILE: tests/Features/Expect/HigherOrder/methodsAndProperties.php ================================================ name->toEqual('Has Methods and Properties')->not()->toEqual('bar') ->multiply(3, 4)->not->toBeString->toEqual(12) ->posts->each(function ($post) { $post->is_published->toBeTrue; })->books()->toBeArray() ->posts->toBeArray->each->not->toBeEmpty ->books()->sequence( function ($book) { $book->title->toEqual('Foo')->cost->toEqual(20); }, function ($book) { $book->title->toEqual('Bar')->cost->toEqual(30); }, ); }); it('can handle nested methods and properties', function () { expect(new HasMethodsAndProperties) ->meta->foo->bar->toBeString()->toEqual('baz')->not->toBeInt ->newInstance()->meta->foo->toBeArray() ->newInstance()->multiply(2, 2)->toEqual(4)->not->toEqual(5) ->newInstance()->books()->toBeArray(); }); it('works with higher order tests') ->expect(new HasMethodsAndProperties) ->meta->foo->bar->toBeString()->toEqual('baz')->not->toBeInt ->newInstance()->meta->foo->toBeArray ->newInstance()->multiply(2, 2)->toEqual(4)->not->toEqual(5) ->newInstance()->books()->toBeArray(); it('can start a new higher order expectation using the and syntax', function () { expect(new HasMethodsAndProperties) ->toBeInstanceOf(HasMethodsAndProperties::class) ->meta->toBeArray ->and(['foo' => 'bar']) ->toBeArray() ->foo->toEqual('bar'); expect(static::getCount())->toEqual(4); }); it('can start a new higher order expectation using the and syntax in higher order tests') ->expect(new HasMethodsAndProperties) ->toBeInstanceOf(HasMethodsAndProperties::class) ->meta->toBeArray ->and(['foo' => 'bar']) ->toBeArray() ->foo->toEqual('bar'); it('can start a new higher order expectation using the and syntax without nesting expectations', function () { expect(new HasMethodsAndProperties) ->toBeInstanceOf(HasMethodsAndProperties::class) ->meta ->sequence( function ($value, $key) { $value->toBeArray()->and($key)->toBe('foo'); }, ); }); class HasMethodsAndProperties { public $name = 'Has Methods and Properties'; public $meta = ['foo' => ['bar' => 'baz']]; public $posts = [ [ 'is_published' => true, 'title' => 'Foo', ], [ 'is_published' => true, 'title' => 'Bar', ], ]; public function books() { return [ [ 'title' => 'Foo', 'cost' => 20, ], [ 'title' => 'Bar', 'cost' => 30, ], ]; } public function multiply($x, $y) { return $x * $y; } public function newInstance() { return new static; } } ================================================ FILE: tests/Features/Expect/HigherOrder/properties.php ================================================ 1])->foo->toBeInt()->toEqual(1); }); it('can access multiple properties from the value', function () { expect(['foo' => 'bar', 'hello' => 'world']) ->foo->toBeString()->toEqual('bar') ->hello->toBeString()->toEqual('world'); }); it('works with not', function () { expect(['foo' => 'bar', 'hello' => 'world']) ->foo->not->not->toEqual('bar') ->foo->not->toEqual('world')->toEqual('bar') ->hello->toEqual('world')->not()->toEqual('bar')->not->toBeNull; }); it('works with each', function () { expect(['numbers' => [1, 2, 3, 4], 'words' => ['hey', 'there']]) ->numbers->toEqual([1, 2, 3, 4])->each->toBeInt->toBeLessThan(5) ->words->each(function ($word) { $word->toBeString()->not->toBeInt(); }); }); it('works inside of each', function () { expect(['books' => [['title' => 'Foo', 'cost' => 20], ['title' => 'Bar', 'cost' => 30]]]) ->books->each(function ($book) { $book->title->not->toBeNull->cost->toBeGreaterThan(19); }); }); it('works with sequence', function () { expect(['books' => [['title' => 'Foo', 'cost' => 20], ['title' => 'Bar', 'cost' => 30]]]) ->books->sequence( function ($book) { $book->title->toEqual('Foo')->cost->toEqual(20); }, function ($book) { $book->title->toEqual('Bar')->cost->toEqual(30); }, ); }); it('can compose complex expectations', function () { expect(['foo' => 'bar', 'numbers' => [1, 2, 3, 4]]) ->toContain('bar')->toBeArray() ->numbers->toEqual([1, 2, 3, 4])->not()->toEqual('bar')->each->toBeInt ->foo->not->toEqual('world')->toEqual('bar') ->numbers->toBeArray(); }); it('works with objects', function () { expect(new HasProperties) ->name->toEqual('foo')->not->toEqual('world') ->posts->toHaveCount(2)->each(function ($post) { $post->is_published->toBeTrue(); }) ->posts->sequence( function ($post) { $post->title->toEqual('Foo'); }, function ($post) { $post->title->toEqual('Bar'); }, ); }); it('works with nested properties', function () { expect(new HasProperties) ->nested->foo->bar->toBeString()->toEqual('baz') ->posts->toBeArray()->toHaveCount(2); }); it('works with higher order tests') ->expect(new HasProperties) ->nested->foo->bar->toBeString()->toEqual('baz') ->posts->toBeArray()->toHaveCount(2); class HasProperties { public $name = 'foo'; public $posts = [ [ 'is_published' => true, 'title' => 'Foo', ], [ 'is_published' => true, 'title' => 'Bar', ], ]; public $nested = [ 'foo' => ['bar' => 'baz'], ]; } ================================================ FILE: tests/Features/Expect/each.php ================================================ each()->toEqual('Foobar'); })->throws(BadMethodCallException::class, 'Expectation value is not iterable.'); it('expects on each item', function () { expect([1, 1, 1]) ->each() ->toEqual(1); expect(static::getCount())->toBe(3); // + 1 assertion expect([1, 1, 1]) ->each ->toEqual(1); expect(static::getCount())->toBe(7); }); it('chains expectations on each item', function () { expect([1, 1, 1]) ->each() ->toBeInt() ->toEqual(1); expect(static::getCount())->toBe(6); // + 1 assertion expect([2, 2, 2]) ->each ->toBeInt ->toEqual(2); expect(static::getCount())->toBe(13); }); test('opposite expectations on each item', function () { expect([1, 2, 3]) ->each() ->not() ->toEqual(4); expect(static::getCount())->toBe(3); expect([1, 2, 3]) ->each() ->not->toBeString; expect(static::getCount())->toBe(7); }); test('chained opposite and non-opposite expectations', function () { expect([1, 2, 3]) ->each() ->not() ->toEqual(4) ->toBeInt(); expect(static::getCount())->toBe(6); }); it('can add expectations via "and"', function () { expect([1, 2, 3]) ->each() ->toBeInt // + 3 ->and([4, 5, 6]) ->each ->toBeLessThan(7) // + 3 ->not ->toBeLessThan(3) ->toBeGreaterThan(3) // + 3 ->and('Hello World') ->toBeString // + 1 ->toEqual('Hello World'); // + 1 expect(static::getCount())->toBe(14); }); it('accepts callables', function () { expect([1, 2, 3])->each(function ($number) { expect($number)->toBeInstanceOf(Expectation::class); expect($number->value)->toBeInt(); $number->toBeInt->not->toBeString; }); expect(static::getCount())->toBe(12); }); it('passes the key of the current item to callables', function () { expect([1, 2, 3])->each(function ($number, $key) { expect($key)->toBeInt(); }); expect(static::getCount())->toBe(3); }); ================================================ FILE: tests/Features/Expect/extend.php ================================================ extend('toBeAMacroExpectation', function () { $this->toBeTrue(); return $this; }); expect()->extend('toBeAMacroExpectationWithArguments', function (bool $value) { $this->toBe($value); return $this; }); it('macros true is true', function () { expect(true)->toBeAMacroExpectation(); }); it('macros false is not true', function () { expect(false)->not->toBeAMacroExpectation(); }); it('macros true is true with argument', function () { expect(true)->toBeAMacroExpectationWithArguments(true); }); it('macros false is not true with argument', function () { expect(false)->not->toBeAMacroExpectationWithArguments(true); }); ================================================ FILE: tests/Features/Expect/json.php ================================================ json() ->name ->toBe('Nuno'); }); test('fails with broken json string', function () { expect('{":"Nuno"}')->json(); })->throws(ExpectationFailedException::class); ================================================ FILE: tests/Features/Expect/matchExpectation.php ================================================ matched = null; }); it('pass', function () { expect('baz') ->match('foo', [ 'bar' => function ($value) { $this->matched = 'bar'; return $value->toEqual('bar'); }, 'foo' => function ($value) { $this->matched = 'baz'; return $value->toEqual('baz'); }, ] ) ->toEqual($this->matched); expect(static::getCount())->toBe(2); }); it('failures', function () { expect(true) ->match('foo', [ 'bar' => function ($value) { return $value->toBeTrue(); }, 'foo' => function ($value) { return $value->toBeFalse(); }, ] ); })->throws(ExpectationFailedException::class, 'true is false'); it('runs with truthy', function () { expect('foo') ->match(1, [ 'bar' => function ($value) { $this->matched = 'bar'; return $value->toEqual('bar'); }, true => function ($value) { $this->matched = 'foo'; return $value->toEqual('foo'); }, ] ) ->toEqual($this->matched); expect(static::getCount())->toBe(2); }); it('runs with falsy', function () { expect('foo') ->match(false, [ 'bar' => function ($value) { $this->matched = 'bar'; return $value->toEqual('bar'); }, false => function ($value) { $this->matched = 'foo'; return $value->toEqual('foo'); }, ] ) ->toEqual($this->matched); expect(static::getCount())->toBe(2); }); it('runs with truthy closure condition', function () { expect('foo') ->match( function () { return '1'; }, [ 'bar' => function ($value) { $this->matched = 'bar'; return $value->toEqual('bar'); }, true => function ($value) { $this->matched = 'foo'; return $value->toEqual('foo'); }, ] ) ->toEqual($this->matched); expect(static::getCount())->toBe(2); }); it('runs with falsy closure condition', function () { expect('foo') ->match( function () { return '0'; }, [ 'bar' => function ($value) { $this->matched = 'bar'; return $value->toEqual('bar'); }, false => function ($value) { $this->matched = 'foo'; return $value->toEqual('foo'); }, ] ) ->toEqual($this->matched); expect(static::getCount())->toBe(2); }); it('can be passed non-callable values', function () { expect('foo') ->match('pest', [ 'bar' => 'foo', 'pest' => 'baz', ] ); })->throws(ExpectationFailedException::class, 'two strings are equal'); it('fails with unhandled match', function () { expect('foo')->match('bar', []); })->throws(ExpectationFailedException::class, 'Unhandled match value.'); it('can be used in higher order tests') ->expect(true) ->match( function () { return true; }, [ false => function ($value) { return $value->toBeFalse(); }, true => function ($value) { return $value->toBeTrue(); }, ] ); ================================================ FILE: tests/Features/Expect/not.php ================================================ toBeTrue() ->not()->toBeFalse() ->not->toBeFalse ->and(false) ->toBeFalse(); }); ================================================ FILE: tests/Features/Expect/pipes.php ================================================ reset(); } public function reset(): void { $this->appliedCount = $this->runCount = [ 'char' => 0, 'number' => 0, 'wildcard' => 0, 'symbol' => 0, ]; } } $state = new State; /* * Overrides toBe to assert two Characters are the same */ expect()->pipe('toBe', function ($next, $expected) use ($state) { $state->runCount['char']++; if ($this->value instanceof Char) { $state->appliedCount['char']++; assertInstanceOf(Char::class, $expected); assertEquals($this->value->value, $expected->value); // returning nothing stops pipeline execution return; } // calling $next(); let the pipeline to keep running $next(); }); /* * Overrides toBe to assert two Number objects are the same */ expect()->intercept('toBe', Number::class, function ($expected) use ($state) { $state->runCount['number']++; $state->appliedCount['number']++; assertInstanceOf(Number::class, $expected); assertEquals($this->value->value, $expected->value); }); /* * Overrides toBe to assert all integers are allowed if value is a wildcard (*) */ expect()->intercept('toBe', fn ($value, $expected) => $value === '*' && is_numeric($expected), function ($expected) use ($state) { $state->runCount['wildcard']++; $state->appliedCount['wildcard']++; }); /* * Overrides toBe to assert to Symbols are the same */ expect()->pipe('toBe', function ($next, $expected) use ($state) { $state->runCount['symbol']++; if ($this->value instanceof Symbol) { $state->appliedCount['symbol']++; assertInstanceOf(Symbol::class, $expected); assertEquals($this->value->value, $expected->value); return; } $next(); }); /* * Overrides toBe to allow ignoring case when checking strings */ expect()->intercept('toBe', fn ($value) => is_string($value), function ($expected, $ignoreCase = false) { if ($ignoreCase) { assertEqualsIgnoringCase($expected, $this->value); } else { assertSame($expected, $this->value); } }); test('pipe is applied and can stop pipeline', function () use ($state) { $char = new Char('A'); $state->reset(); expect($char)->toBe(new Char('A')) ->and($state) ->runCount->toMatchArray([ 'char' => 1, 'number' => 0, 'wildcard' => 0, 'symbol' => 0, ]) ->appliedCount->toMatchArray([ 'char' => 1, 'number' => 0, 'wildcard' => 0, 'symbol' => 0, ]); }); test('pipe is run and can let the pipeline keep going', function () use ($state) { $state->reset(); expect(3)->toBe(3) ->and($state) ->runCount->toMatchArray([ 'char' => 1, 'number' => 0, 'wildcard' => 0, 'symbol' => 1, ]) ->appliedCount->toMatchArray([ 'char' => 0, 'number' => 0, 'wildcard' => 0, 'symbol' => 0, ]); }); test('pipe works with negated expectation', function () use ($state) { $char = new Char('A'); $state->reset(); expect($char)->not->toBe(new Char('B')) ->and($state) ->runCount->toMatchArray([ 'char' => 1, 'number' => 0, 'wildcard' => 0, 'symbol' => 0, ]) ->appliedCount->toMatchArray([ 'char' => 1, 'number' => 0, 'wildcard' => 0, 'symbol' => 0, ]); }); test('interceptor is applied', function () use ($state) { $number = new Number(1); $state->reset(); expect($number)->toBe(new Number(1)) ->and($state) ->runCount->toHaveKey('number', 1) ->appliedCount->toHaveKey('number', 1); }); test('interceptor stops the pipeline', function () use ($state) { $number = new Number(1); $state->reset(); expect($number)->toBe(new Number(1)) ->and($state) ->runCount->toMatchArray([ 'char' => 1, 'number' => 1, 'wildcard' => 0, 'symbol' => 0, ]) ->appliedCount->toMatchArray([ 'char' => 0, 'number' => 1, 'wildcard' => 0, 'symbol' => 0, ]); }); test('interceptor is called only when filter is met', function () use ($state) { $state->reset(); expect(1)->toBe(1) ->and($state) ->runCount->toHaveKey('number', 0) ->appliedCount->toHaveKey('number', 0); }); test('interceptor can be filtered with a closure', function () use ($state) { $state->reset(); expect('*')->toBe(1) ->and($state) ->runCount->toHaveKey('wildcard', 1) ->appliedCount->toHaveKey('wildcard', 1); }); test('interceptor can be filter the expected parameter as well', function () use ($state) { $state->reset(); expect('*')->toBe('*') ->and($state) ->runCount->toHaveKey('wildcard', 0) ->appliedCount->toHaveKey('wildcard', 0); }); test('interceptor works with negated expectation', function () { $char = new Number(1); expect($char)->not->toBe(new Char('B')); }); test('intercept can add new parameters to the expectation', function () { $ignoreCase = true; expect('Foo')->toBe('foo', $ignoreCase); }); ================================================ FILE: tests/Features/Expect/ray.php ================================================ ray()->toBe(true); }); ================================================ FILE: tests/Features/Expect/sequence.php ================================================ each->sequence(); })->throws(BadMethodCallException::class, 'Expectation value is not iterable.'); test('an exception is thrown if there are no expectations', function () { expect(['Foobar'])->sequence(); })->throws(InvalidArgumentException::class, 'No sequence expectations defined.'); test('allows for sequences of checks to be run on iterable data', function () { expect([1, 2, 3]) ->sequence( function ($expectation) { $expectation->toBeInt()->toEqual(1); }, function ($expectation) { $expectation->toBeInt()->toEqual(2); }, function ($expectation) { $expectation->toBeInt()->toEqual(3); }, ); expect(static::getCount())->toBe(6); }); test('loops back to the start if it runs out of sequence items', function () { expect([1, 2, 3, 1, 2, 3, 1, 2]) ->sequence( function ($expectation) { $expectation->toBeInt()->toEqual(1); }, function ($expectation) { $expectation->toBeInt()->toEqual(2); }, function ($expectation) { $expectation->toBeInt()->toEqual(3); }, ); expect(static::getCount())->toBe(16); }); test('fails if the number of iterable items is less than the number of expectations', function () { expect([1, 2]) ->sequence( function ($expectation) { $expectation->toBeInt()->toEqual(1); }, function ($expectation) { $expectation->toBeInt()->toEqual(2); }, function ($expectation) { $expectation->toBeInt()->toEqual(3); }, ); })->throws(OutOfRangeException::class, 'Sequence expectations are more than the iterable items.'); test('it works with associative arrays', function () { expect(['foo' => 'bar', 'baz' => 'boom']) ->sequence( function ($expectation, $key) { $expectation->toEqual('bar'); $key->toEqual('foo'); }, function ($expectation, $key) { $expectation->toEqual('boom'); $key->toEqual('baz'); }, ); }); test('it can be passed non-callable values', function () { expect(['foo', 'bar', 'baz'])->sequence('foo', 'bar', 'baz'); expect(static::getCount())->toBe(3); }); test('it can be passed a mixture of value types', function () { expect(['foo', 'bar', 'baz'])->sequence( 'foo', function ($expectation) { $expectation->toEqual('bar')->toBeString(); }, 'baz' ); expect(static::getCount())->toBe(4); }); test('it works with traversables', function () { $generator = (function () { yield 'one' => (fn () => yield from [1, 2, 3])(); yield 'two' => (fn () => yield from [4, 5, 6])(); yield 'three' => (fn () => yield from [7, 8, 9])(); })(); expect($generator)->sequence( fn ($value, $key) => $key->toBe('one') ->and($value) ->toBeInstanceOf(Generator::class) ->sequence(1, 2, 3), fn ($value, $key) => $key->toBe('two') ->and($value) ->toBeInstanceOf(Generator::class) ->sequence(4, 5, 6), fn ($value, $key) => $key->toBe('three') ->and($value) ->toBeInstanceOf(Generator::class) ->sequence(7, 8, 9), ); }); ================================================ FILE: tests/Features/Expect/toBe.php ================================================ toBeTrue()->and(false)->toBeFalse(); test('strict comparisons', function () { $nuno = new stdClass; $dries = new stdClass; expect($nuno)->toBe($nuno)->not->toBe($dries); }); test('failures', function () { expect(1)->toBe(2); })->throws(ExpectationFailedException::class); test('failures with custom message', function () { expect(1)->toBe(2, 'oh no!'); })->throws(ExpectationFailedException::class, 'oh no!'); test('not failures', function () { expect(1)->not->toBe(1); })->throws(ExpectationFailedException::class); ================================================ FILE: tests/Features/Expect/toBeAlpha.php ================================================ toBeAlpha(); expect('123')->not->toBeAlpha(); }); test('failures', function () { expect('123')->toBeAlpha(); })->throws(ExpectationFailedException::class); test('failures with custom message', function () { expect('123')->toBeAlpha('oh no!'); })->throws(ExpectationFailedException::class, 'oh no!'); test('not failures', function () { expect('abc')->not->toBeAlpha(); })->throws(ExpectationFailedException::class); ================================================ FILE: tests/Features/Expect/toBeAlphaNumeric.php ================================================ toBeAlphaNumeric(); expect('-')->not->toBeAlphaNumeric(); }); test('failures', function () { expect('-')->toBeAlphaNumeric(); })->throws(ExpectationFailedException::class); test('failures with custom message', function () { expect('-')->toBeAlphaNumeric('oh no!'); })->throws(ExpectationFailedException::class, 'oh no!'); test('not failures', function () { expect('abc123')->not->toBeAlphaNumeric(); })->throws(ExpectationFailedException::class); ================================================ FILE: tests/Features/Expect/toBeArray.php ================================================ toBeArray(); expect('1, 2, 3')->not->toBeArray(); }); test('failures', function () { expect(null)->toBeArray(); })->throws(ExpectationFailedException::class); test('failures with custom message', function () { expect(null)->toBeArray('oh no!'); })->throws(ExpectationFailedException::class, 'oh no!'); test('not failures', function () { expect(['a', 'b', 'c'])->not->toBeArray(); })->throws(ExpectationFailedException::class); ================================================ FILE: tests/Features/Expect/toBeBetween.php ================================================ toBeBetween(1, 3); }); test('passes with float', function () { expect(1.5)->toBeBetween(1.25, 1.75); }); test('passes with float and int', function () { expect(1.5)->toBeBetween(1, 2); }); test('passes with DateTime', function () { expect(new DateTime('2023-09-22'))->toBeBetween(new DateTime('2023-09-21'), new DateTime('2023-09-23')); }); test('failure with int', function () { expect(4)->toBeBetween(1, 3); })->throws(ExpectationFailedException::class); test('failure with float', function () { expect(2)->toBeBetween(1.5, 1.75); })->throws(ExpectationFailedException::class); test('failure with float and int', function () { expect(2.1)->toBeBetween(1, 2); })->throws(ExpectationFailedException::class); test('failure with DateTime', function () { expect(new DateTime('2023-09-20'))->toBeBetween(new DateTime('2023-09-21'), new DateTime('2023-09-23')); })->throws(ExpectationFailedException::class); test('failures with custom message', function () { expect(4)->toBeBetween(1, 3, 'oh no!'); })->throws(ExpectationFailedException::class, 'oh no!'); test('not failures', function () { expect(2)->not->toBeBetween(1, 3); })->throws(ExpectationFailedException::class); ================================================ FILE: tests/Features/Expect/toBeBool.php ================================================ toBeBool(); expect(0)->not->toBeBool(); }); test('failures', function () { expect(null)->toBeBool(); })->throws(ExpectationFailedException::class); test('failures with custom message', function () { expect(null)->toBeBool('oh no!'); })->throws(ExpectationFailedException::class, 'oh no!'); test('not failures', function () { expect(false)->not->toBeBool(); })->throws(ExpectationFailedException::class); ================================================ FILE: tests/Features/Expect/toBeCallable.php ================================================ toBeCallable(); expect(null)->not->toBeCallable(); }); test('failures', function () { $hello = 5; expect($hello)->toBeCallable(); })->throws(ExpectationFailedException::class); test('failures with custom message', function () { $hello = 5; expect($hello)->toBeCallable('oh no!'); })->throws(ExpectationFailedException::class, 'oh no!'); test('not failures', function () { expect(function () { return 42; })->not->toBeCallable(); })->throws(ExpectationFailedException::class); ================================================ FILE: tests/Features/Expect/toBeCamelCase.php ================================================ toBeCamelCase(); expect('abcDef')->toBeCamelCase(); expect('abc-def')->not->toBeCamelCase(); expect('abc-def')->not->toBeCamelCase(); expect('AbcDef')->not->toBeCamelCase(); }); test('failures', function () { expect('Abc')->toBeCamelCase(); })->throws(ExpectationFailedException::class); test('failures with custom message', function () { expect('Abc')->toBeCamelCase('oh no!'); })->throws(ExpectationFailedException::class, 'oh no!'); test('not failures', function () { expect('abcDef')->not->toBeCamelCase(); })->throws(ExpectationFailedException::class); ================================================ FILE: tests/Features/Expect/toBeDigits.php ================================================ toBeDigits(); expect('123.14')->not->toBeDigits(); }); test('failures', function () { expect('123.14')->toBeDigits(); })->throws(ExpectationFailedException::class); test('failures with custom message', function () { expect('123.14')->toBeDigits('oh no!'); })->throws(ExpectationFailedException::class, 'oh no!'); test('not failures', function () { expect('445')->not->toBeDigits(); })->throws(ExpectationFailedException::class); ================================================ FILE: tests/Features/Expect/toBeDirectory.php ================================================ toBeDirectory(); }); test('failures', function () { expect('/random/path/whatever')->toBeDirectory(); })->throws(ExpectationFailedException::class); test('failures with custom message', function () { expect('/random/path/whatever')->toBeDirectory('oh no!'); })->throws(ExpectationFailedException::class, 'oh no!'); test('not failures', function () { expect('.')->not->toBeDirectory(); })->throws(ExpectationFailedException::class); ================================================ FILE: tests/Features/Expect/toBeEmpty.php ================================================ toBeEmpty(); expect(null)->toBeEmpty(); }); test('failures', function () { expect([1, 2])->toBeEmpty(); expect(' ')->toBeEmpty(); })->throws(ExpectationFailedException::class); test('failures with custom message', function () { expect([1, 2])->toBeEmpty('oh no!'); expect(' ')->toBeEmpty('oh no!'); })->throws(ExpectationFailedException::class, 'oh no!'); test('not failures', function () { expect([])->not->toBeEmpty(); expect(null)->not->toBeEmpty(); })->throws(ExpectationFailedException::class); ================================================ FILE: tests/Features/Expect/toBeFalse.php ================================================ toBeFalse(); }); test('failures', function () { expect('')->toBeFalse(); })->throws(ExpectationFailedException::class); test('failures with custom message', function () { expect('')->toBeFalse('oh no!'); })->throws(ExpectationFailedException::class, 'oh no!'); test('not failures', function () { expect(false)->not->toBe(false); })->throws(ExpectationFailedException::class); ================================================ FILE: tests/Features/Expect/toBeFalsy.php ================================================ toBeFalsy(); })->with([false, '', null, 0, '0']); test('passes as not falsy', function ($value) { expect($value)->not->toBeFalsy(); })->with([true, [1], 'false', 1, -1]); test('failures', function () { expect(1)->toBeFalsy(); })->throws(ExpectationFailedException::class); test('failures with custom message', function () { expect(1)->toBeFalsy('oh no!'); })->throws(ExpectationFailedException::class, 'oh no!'); test('not failures', function () { expect(null)->not->toBeFalsy(); })->throws(ExpectationFailedException::class); ================================================ FILE: tests/Features/Expect/toBeFile.php ================================================ tempFile = sys_get_temp_dir().'/fake.file'); }); afterEach(function () { unlink($this->tempFile); }); test('pass', function () { expect($this->tempFile)->toBeFile(); }); test('failures', function () { expect('/random/path/whatever.file')->toBeFile(); })->throws(ExpectationFailedException::class); test('failures with custom message', function () { expect('/random/path/whatever.file')->toBeFile('oh no!'); })->throws(ExpectationFailedException::class, 'oh no!'); test('not failures', function () { expect($this->tempFile)->not->toBeFile(); })->throws(ExpectationFailedException::class); ================================================ FILE: tests/Features/Expect/toBeFloat.php ================================================ toBeFloat(); expect(1)->not->toBeFloat(); }); test('failures', function () { expect(42)->toBeFloat(); })->throws(ExpectationFailedException::class); test('failures with custom message', function () { expect(42)->toBeFloat('oh no!'); })->throws(ExpectationFailedException::class, 'oh no!'); test('not failures', function () { expect(log(3))->not->toBeFloat(); })->throws(ExpectationFailedException::class); ================================================ FILE: tests/Features/Expect/toBeGreaterThan.php ================================================ toBeGreaterThan(41); expect(4)->toBeGreaterThan(3.9); }); test('passes with DateTime and DateTimeImmutable', function () { $now = new DateTime; $past = (new DateTimeImmutable)->modify('-1 day'); expect($now)->toBeGreaterThan($past); expect($past)->not->toBeGreaterThan($now); }); test('passes with strings', function () { expect('b')->toBeGreaterThan('a'); expect('a')->not->toBeGreaterThan('a'); }); test('failures', function () { expect(4)->toBeGreaterThan(4); })->throws(ExpectationFailedException::class); test('failures with custom message', function () { expect(4)->toBeGreaterThan(4, 'oh no!'); })->throws(ExpectationFailedException::class, 'oh no!'); test('not failures', function () { expect(5)->not->toBeGreaterThan(4); })->throws(ExpectationFailedException::class); ================================================ FILE: tests/Features/Expect/toBeGreaterThanOrEqual.php ================================================ toBeGreaterThanOrEqual(41); expect(4)->toBeGreaterThanOrEqual(4); }); test('passes with DateTime and DateTimeImmutable', function () { $now = new DateTime; $past = (new DateTimeImmutable)->modify('-1 day'); expect($now)->toBeGreaterThanOrEqual($now); expect($now)->toBeGreaterThanOrEqual($past); expect($past)->not->toBeGreaterThanOrEqual($now); }); test('passes with strings', function () { expect('b')->toBeGreaterThanOrEqual('a'); expect('a')->toBeGreaterThanOrEqual('a'); }); test('failures', function () { expect(4)->toBeGreaterThanOrEqual(4.1); })->throws(ExpectationFailedException::class); test('failures with custom message', function () { expect(4)->toBeGreaterThanOrEqual(4.1, 'oh no!'); })->throws(ExpectationFailedException::class, 'oh no!'); test('not failures', function () { expect(5)->not->toBeGreaterThanOrEqual(5); })->throws(ExpectationFailedException::class); ================================================ FILE: tests/Features/Expect/toBeIn.php ================================================ toBeIn(['a', 'b', 'c']); expect('d')->not->toBeIn(['a', 'b', 'c']); }); test('failures', function () { expect('d')->toBeIn(['a', 'b', 'c']); })->throws(ExpectationFailedException::class); test('failures with custom message', function () { expect('d')->toBeIn(['a', 'b', 'c'], 'oh no!'); })->throws(ExpectationFailedException::class, 'oh no!'); test('not failures', function () { expect('a')->not->toBeIn(['a', 'b', 'c']); })->throws(ExpectationFailedException::class); ================================================ FILE: tests/Features/Expect/toBeInfinite.php ================================================ toBeInfinite(); expect(log(1))->not->toBeInfinite(); }); test('failures', function () { expect(asin(2))->toBeInfinite(); })->throws(ExpectationFailedException::class); test('failures with custom message', function () { expect(asin(2))->toBeInfinite('oh no!'); })->throws(ExpectationFailedException::class, 'oh no!'); test('not failures', function () { expect(INF)->not->toBeInfinite(); })->throws(ExpectationFailedException::class); ================================================ FILE: tests/Features/Expect/toBeInstanceOf.php ================================================ toBeInstanceOf(Exception::class); expect(new Exception)->not->toBeInstanceOf(RuntimeException::class); }); test('failures', function () { expect(new Exception)->toBeInstanceOf(RuntimeException::class); })->throws(ExpectationFailedException::class); test('failures with custom message', function () { expect(new Exception)->toBeInstanceOf(RuntimeException::class, 'oh no!'); })->throws(ExpectationFailedException::class, 'oh no!'); test('not failures', function () { expect(new Exception)->not->toBeInstanceOf(Exception::class); })->throws(ExpectationFailedException::class); ================================================ FILE: tests/Features/Expect/toBeInt.php ================================================ toBeInt(); expect(42.0)->not->toBeInt(); }); test('failures', function () { expect(42.0)->toBeInt(); })->throws(ExpectationFailedException::class); test('failures with custom message', function () { expect(42.0)->toBeInt('oh no!'); })->throws(ExpectationFailedException::class, 'oh no!'); test('not failures', function () { expect(6 * 7)->not->toBeInt(); })->throws(ExpectationFailedException::class); ================================================ FILE: tests/Features/Expect/toBeIntBackedEnum.php ================================================ expect('Tests\Fixtures\Arch\ToBeIntBackedEnum\HasIntBacking') ->toBeIntBackedEnum(); test('enum is not backed by int') ->expect('Tests\Fixtures\Arch\ToBeIntBackedEnum\HasStringBacking') ->not->toBeIntBackedEnum(); ================================================ FILE: tests/Features/Expect/toBeInvokable.php ================================================ expect('Tests\\Fixtures\\Arch\\ToBeInvokable\\IsInvokable\\InvokableClass') ->toBeInvokable(); test('opposite class is invokable') ->throws(ArchExpectationFailedException::class) ->expect('Tests\\Fixtures\\Arch\\ToBeInvokable\\IsInvokable\\InvokableClass') ->not->toBeInvokable(); test('class is invokable via a parent class') ->expect('Tests\\Fixtures\\Arch\\ToBeInvokable\\IsInvokable\\InvokableClassViaParent') ->toBeInvokable(); test('class is invokable via a trait') ->expect('Tests\\Fixtures\\Arch\\ToBeInvokable\\IsInvokable\\InvokableClassViaTrait') ->toBeInvokable(); test('failure when the class is not invokable') ->throws(ArchExpectationFailedException::class) ->expect('Tests\\Fixtures\\Arch\\ToBeInvokable\\IsNotInvokable\\IsNotInvokableClass') ->toBeInvokable(); test('class is not invokable') ->expect('Tests\\Fixtures\\Arch\\ToBeInvokable\\IsNotInvokable\\IsNotInvokableClass') ->not->toBeInvokable(); ================================================ FILE: tests/Features/Expect/toBeIterable.php ================================================ toBeIterable(); expect(null)->not->toBeIterable(); }); test('failures', function () { expect(42)->toBeIterable(); })->throws(ExpectationFailedException::class); test('failures with custom message', function () { expect(42)->toBeIterable('oh no!'); })->throws(ExpectationFailedException::class, 'oh no!'); test('not failures', function () { function gen(): iterable { yield 1; yield 2; yield 3; } expect(gen())->not->toBeIterable(); })->throws(ExpectationFailedException::class); ================================================ FILE: tests/Features/Expect/toBeJson.php ================================================ toBeJson(); expect('foo')->not->toBeJson(); expect('{"hello"')->not->toBeJson(); }); test('failures', function () { expect(':"world"}')->toBeJson(); })->throws(ExpectationFailedException::class); test('failures with custom message', function () { expect(':"world"}')->toBeJson('oh no!'); })->throws(ExpectationFailedException::class, 'oh no!'); test('not failures', function () { expect('{"hello":"world"}')->not->toBeJson(); })->throws(ExpectationFailedException::class); ================================================ FILE: tests/Features/Expect/toBeKebabCase.php ================================================ toBeKebabCase(); expect('abc-def')->toBeKebabCase(); expect('abc_def')->not->toBeKebabCase(); expect('abcDef')->not->toBeKebabCase(); expect('AbcDef')->not->toBeKebabCase(); }); test('failures', function () { expect('Abc')->toBeKebabCase(); })->throws(ExpectationFailedException::class); test('failures with custom message', function () { expect('Abc')->toBeKebabCase('oh no!'); })->throws(ExpectationFailedException::class, 'oh no!'); test('not failures', function () { expect('abc-def')->not->toBeKebabCase(); })->throws(ExpectationFailedException::class); ================================================ FILE: tests/Features/Expect/toBeLessThan.php ================================================ toBeLessThan(42); expect(4)->toBeLessThan(5); }); test('passes with DateTime and DateTimeImmutable', function () { $now = new DateTime; $past = (new DateTimeImmutable)->modify('-1 day'); expect($past)->toBeLessThan($now); expect($now)->not->toBeLessThan($now); }); test('passes with strings', function () { expect('a')->toBeLessThan('b'); expect('a')->not->toBeLessThan('a'); }); test('failures', function () { expect(4)->toBeLessThan(4); })->throws(ExpectationFailedException::class); test('failures with custom message', function () { expect(4)->toBeLessThan(4, 'oh no!'); })->throws(ExpectationFailedException::class, 'oh no!'); test('not failures', function () { expect(5)->not->toBeLessThan(6); })->throws(ExpectationFailedException::class); ================================================ FILE: tests/Features/Expect/toBeLessThanOrEqual.php ================================================ toBeLessThanOrEqual(42); expect(4)->toBeLessThanOrEqual(4); }); test('passes with DateTime and DateTimeImmutable', function () { $now = new DateTime; $past = (new DateTimeImmutable)->modify('-1 day'); expect($now)->toBeLessThanOrEqual($now); expect($past)->toBeLessThanOrEqual($now); expect($now)->not->toBeLessThanOrEqual($past); }); test('passes with strings', function () { expect('a')->toBeLessThanOrEqual('b'); expect('a')->toBeLessThanOrEqual('a'); }); test('failures', function () { expect(4)->toBeLessThanOrEqual(3.9); })->throws(ExpectationFailedException::class); test('failures with custom message', function () { expect(4)->toBeLessThanOrEqual(3.9, 'oh no!'); })->throws(ExpectationFailedException::class, 'oh no!'); test('not failures', function () { expect(5)->not->toBeLessThanOrEqual(5); })->throws(ExpectationFailedException::class); ================================================ FILE: tests/Features/Expect/toBeList.php ================================================ toBeList(); expect(['a' => 1, 'b' => 2, 'c' => 3])->not->toBeList(); expect('1, 2, 3')->not->toBeList(); }); test('failures', function () { expect(null)->toBeList(); })->throws(ExpectationFailedException::class); test('failures with custom message', function () { expect(null)->toBeList('oh no!'); })->throws(ExpectationFailedException::class, 'oh no!'); test('not failures', function () { expect(['a', 'b', 'c'])->not->toBeList(); })->throws(ExpectationFailedException::class); ================================================ FILE: tests/Features/Expect/toBeLowercase.php ================================================ toBeLowercase(); expect('UPPERCASE')->not->toBeLowercase(); }); test('failures', function () { expect('UPPERCASE')->toBeLowercase(); })->throws(ExpectationFailedException::class); test('failures with custom message', function () { expect('UPPERCASE')->toBeLowercase('oh no!'); })->throws(ExpectationFailedException::class, 'oh no!'); test('not failures', function () { expect('lowercase')->not->toBeLowercase(); })->throws(ExpectationFailedException::class); ================================================ FILE: tests/Features/Expect/toBeNAN.php ================================================ toBeNan(); expect(log(0))->not->toBeNan(); }); test('failures', function () { expect(1)->toBeNan(); })->throws(ExpectationFailedException::class); test('failures with custom message', function () { expect(1)->toBeNan('oh no!'); })->throws(ExpectationFailedException::class, 'oh no!'); test('not failures', function () { expect(acos(1.5))->not->toBeNan(); })->throws(ExpectationFailedException::class); ================================================ FILE: tests/Features/Expect/toBeNull.php ================================================ toBeNull(); expect('')->not->toBeNull(); }); test('failures', function () { expect('hello')->toBeNull(); })->throws(ExpectationFailedException::class); test('failures with custom message', function () { expect('hello')->toBeNull('oh no!'); })->throws(ExpectationFailedException::class, 'oh no!'); test('not failures', function () { expect(null)->not->toBeNull(); })->throws(ExpectationFailedException::class); ================================================ FILE: tests/Features/Expect/toBeNumeric.php ================================================ toBeNumeric(); expect('A')->not->toBeNumeric(); }); test('failures', function () { expect(null)->toBeNumeric(); })->throws(ExpectationFailedException::class); test('failures with custom message', function () { expect(null)->toBeNumeric('oh no!'); })->throws(ExpectationFailedException::class, 'oh no!'); test('not failures', function () { expect(6 * 7)->not->toBeNumeric(); })->throws(ExpectationFailedException::class); ================================================ FILE: tests/Features/Expect/toBeObject.php ================================================ 1])->toBeObject(); expect(['a' => 1])->not->toBeObject(); }); test('failures', function () { expect(null)->toBeObject(); })->throws(ExpectationFailedException::class); test('failures with custom message', function () { expect(null)->toBeObject('oh no!'); })->throws(ExpectationFailedException::class, 'oh no!'); test('not failures', function () { expect((object) 'ciao')->not->toBeObject(); })->throws(ExpectationFailedException::class); ================================================ FILE: tests/Features/Expect/toBeReadableDirectory.php ================================================ toBeReadableDirectory(); }); test('failures', function () { expect('/random/path/whatever')->toBeReadableDirectory(); })->throws(ExpectationFailedException::class); test('failures with custom message', function () { expect('/random/path/whatever')->toBeReadableDirectory('oh no!'); })->throws(ExpectationFailedException::class, 'oh no!'); test('not failures', function () { expect(sys_get_temp_dir())->not->toBeReadableDirectory(); })->throws(ExpectationFailedException::class); ================================================ FILE: tests/Features/Expect/toBeReadableFile.php ================================================ tempFile = sys_get_temp_dir().'/fake.file'); }); afterEach(function () { unlink($this->tempFile); }); test('pass', function () { expect($this->tempFile)->toBeReadableFile(); }); test('failures', function () { expect('/random/path/whatever.file')->toBeReadableFile(); })->throws(ExpectationFailedException::class); test('failures with custom message', function () { expect('/random/path/whatever.file')->toBeReadableFile('oh no!'); })->throws(ExpectationFailedException::class, 'oh no!'); test('not failures', function () { expect($this->tempFile)->not->toBeReadableFile(); })->throws(ExpectationFailedException::class); ================================================ FILE: tests/Features/Expect/toBeResource.php ================================================ toBeResource(); expect(null)->not->toBeResource(); }); test('failures', function () { expect(null)->toBeResource(); })->throws(ExpectationFailedException::class); test('failures with custom message', function () { expect(null)->toBeResource('oh no!'); })->throws(ExpectationFailedException::class, 'oh no!'); test('not failures', function () use ($resource) { expect($resource)->not->toBeResource(); })->throws(ExpectationFailedException::class); ================================================ FILE: tests/Features/Expect/toBeScalar.php ================================================ toBeScalar(); }); test('failures', function () { expect(null)->toBeScalar(); })->throws(ExpectationFailedException::class); test('failures with custom message', function () { expect(null)->toBeScalar('oh no!'); })->throws(ExpectationFailedException::class, 'oh no!'); test('not failures', function () { expect(42)->not->toBeScalar(); })->throws(ExpectationFailedException::class); ================================================ FILE: tests/Features/Expect/toBeSlug.php ================================================ toBeSlug() ->and('Another Test String')->toBeSlug(); }); test('failures', function () { expect('')->toBeSlug(); })->throws(ExpectationFailedException::class); test('failures with custom message', function () { expect('')->toBeSlug('oh no!'); })->throws(ExpectationFailedException::class, 'oh no!'); test('failures with default message', function () { expect('')->toBeSlug(); })->throws(ExpectationFailedException::class, 'Failed asserting that can be converted to a slug.'); test('not failures', function () { expect('This is a Test String!')->not->toBeSlug(); })->throws(ExpectationFailedException::class); ================================================ FILE: tests/Features/Expect/toBeSnakeCase.php ================================================ toBeSnakeCase(); expect('abc_def')->toBeSnakeCase(); expect('abc-def')->not->toBeSnakeCase(); expect('abcDef')->not->toBeSnakeCase(); expect('AbcDef')->not->toBeSnakeCase(); }); test('failures', function () { expect('Abc')->toBeSnakeCase(); })->throws(ExpectationFailedException::class); test('failures with custom message', function () { expect('Abc')->toBeSnakeCase('oh no!'); })->throws(ExpectationFailedException::class, 'oh no!'); test('not failures', function () { expect('abc_def')->not->toBeSnakeCase(); })->throws(ExpectationFailedException::class); ================================================ FILE: tests/Features/Expect/toBeString.php ================================================ toBeString(); expect(1.1)->not->toBeString(); }); test('failures', function () { expect(null)->toBeString(); })->throws(ExpectationFailedException::class); test('failures with custom message', function () { expect(null)->toBeString('oh no!'); })->throws(ExpectationFailedException::class, 'oh no!'); test('not failures', function () { expect('42')->not->toBeString(); })->throws(ExpectationFailedException::class); ================================================ FILE: tests/Features/Expect/toBeStringBackedEnum.php ================================================ expect('Tests\Fixtures\Arch\ToBeStringBackedEnum\HasStringBacking') ->toBeStringBackedEnum(); test('enum is not backed by string') ->expect('Tests\Fixtures\Arch\ToBeStringBackedEnum\HasIntBacking') ->not->toBeStringBackedEnum(); ================================================ FILE: tests/Features/Expect/toBeStudlyCase.php ================================================ toBeStudlyCase(); expect('AbcDef')->toBeStudlyCase(); expect('abc-def')->not->toBeStudlyCase(); expect('abc-def')->not->toBeStudlyCase(); expect('abc')->not->toBeStudlyCase(); }); test('failures', function () { expect('abc')->toBeStudlyCase(); })->throws(ExpectationFailedException::class); test('failures with custom message', function () { expect('abc')->toBeStudlyCase('oh no!'); })->throws(ExpectationFailedException::class, 'oh no!'); test('not failures', function () { expect('AbcDef')->not->toBeStudlyCase(); })->throws(ExpectationFailedException::class); ================================================ FILE: tests/Features/Expect/toBeTrue.php ================================================ toBeTrue(); }); test('failures', function () { expect('')->toBeTrue(); })->throws(ExpectationFailedException::class); test('failures with custom message', function () { expect('')->toBeTrue('oh no!'); })->throws(ExpectationFailedException::class, 'oh no!'); test('not failures', function () { expect(false)->not->toBe(false); })->throws(ExpectationFailedException::class); ================================================ FILE: tests/Features/Expect/toBeTruthy.php ================================================ toBeTruthy(); })->with([true, [1], 'false', 1, -1]); test('passes as not truthy', function ($value) { expect($value)->not->toBeTruthy(); })->with([false, '', null, 0, '0']); test('failures', function () { expect(null)->toBeTruthy(); })->throws(ExpectationFailedException::class); test('failures with custom message', function () { expect(null)->toBeTruthy('oh no!'); })->throws(ExpectationFailedException::class, 'oh no!'); test('not failures', function () { expect(1)->not->toBeTruthy(); })->throws(ExpectationFailedException::class); ================================================ FILE: tests/Features/Expect/toBeUppercase.php ================================================ toBeUppercase(); expect('lowercase')->not->toBeUppercase(); }); test('failures', function () { expect('lowercase')->toBeUppercase(); })->throws(ExpectationFailedException::class); test('failures with custom message', function () { expect('lowercase')->toBeUppercase('oh no!'); })->throws(ExpectationFailedException::class, 'oh no!'); test('not failures', function () { expect('UPPERCASE')->not->toBeUppercase(); })->throws(ExpectationFailedException::class); ================================================ FILE: tests/Features/Expect/toBeUrl.php ================================================ toBeUrl() ->and('pestphp.com')->not->toBeUrl(); }); test('failures', function () { expect('pestphp.com')->toBeUrl(); })->throws(ExpectationFailedException::class); test('failures with custom message', function () { expect('pestphp.com')->toBeUrl('oh no!'); })->throws(ExpectationFailedException::class, 'oh no!'); test('failures with default message', function () { expect('pestphp.com')->toBeUrl(); })->throws(ExpectationFailedException::class, 'Failed asserting that pestphp.com is a url.'); test('not failures', function () { expect('https://pestphp.com')->not->toBeUrl(); })->throws(ExpectationFailedException::class); ================================================ FILE: tests/Features/Expect/toBeUuid.php ================================================ toBeUuid(); })->throws(InvalidExpectationValue::class, 'Invalid expectation value type. Expected [string].'); test('pass', function () { expect('3cafb226-4326-11ee-a516-846993788c86')->toBeUuid(); // version 1 expect('0000415c-4326-21ee-a700-846993788c86')->toBeUuid(); // version 2 expect('3f703955-aaba-3e70-a3cb-baff6aa3b28f')->toBeUuid(); // version 3 expect('ca0a8228-cdf6-41db-b34b-c2f31485796c')->toBeUuid(); // version 4 expect('a35477ae-bfb1-5f2e-b5a4-4711594d855f')->toBeUuid(); // version 5 expect('1ee43263-cf5a-6fd8-8f47-846993788c86')->toBeUuid(); // version 6 expect('018a2bef-09f2-728c-becb-c3f569d91486')->toBeUuid(); // version 7 expect('00112233-4455-8677-8899-aabbccddeeff')->toBeUuid(); // version 8 }); test('failures', function () { expect('foo')->toBeUuid(); })->throws(ExpectationFailedException::class); test('failures with message', function () { expect('bar')->toBeUuid('oh no!'); })->throws(ExpectationFailedException::class, 'oh no!'); test('not failures', function () { expect('foo')->not->toBeUuid(); }); ================================================ FILE: tests/Features/Expect/toBeWritableDirectory.php ================================================ toBeWritableDirectory(); }); test('failures', function () { expect('/random/path/whatever')->toBeWritableDirectory(); })->throws(ExpectationFailedException::class); test('failures with custom message', function () { expect('/random/path/whatever')->toBeWritableDirectory('oh no!'); })->throws(ExpectationFailedException::class, 'oh no!'); test('not failures', function () { expect(sys_get_temp_dir())->not->toBeWritableDirectory(); })->throws(ExpectationFailedException::class); ================================================ FILE: tests/Features/Expect/toBeWritableFile.php ================================================ tempFile = sys_get_temp_dir().'/fake.file'); }); afterEach(function () { unlink($this->tempFile); }); test('pass', function () { expect($this->tempFile)->toBeWritableFile(); }); test('failures', function () { expect('/random/path/whatever.file')->toBeWritableFile(); })->throws(ExpectationFailedException::class); test('failures with custom message', function () { expect('/random/path/whatever.file')->toBeWritableFile('oh no!'); })->throws(ExpectationFailedException::class, 'oh no!'); test('not failures', function () { expect($this->tempFile)->not->toBeWritableFile(); })->throws(ExpectationFailedException::class); ================================================ FILE: tests/Features/Expect/toContain.php ================================================ toContain('Nu'); }); test('passes strings with multiple needles', function () { expect('Nuno')->toContain('Nu', 'no'); }); test('passes arrays', function () { expect([1, 2, 42])->toContain(42); }); test('passes arrays with multiple needles', function () { expect([1, 2, 42])->toContain(42, 2); }); test('passes with array needles', function () { expect([[1, 2, 3], 2, 42])->toContain(42, [1, 2, 3]); }); test('failures', function () { expect([1, 2, 42])->toContain(3); })->throws(ExpectationFailedException::class); test('failures with multiple needles (all failing)', function () { expect([1, 2, 42])->toContain(3, 4); })->throws(ExpectationFailedException::class); test('failures with multiple needles (some failing)', function () { expect([1, 2, 42])->toContain(1, 3, 4); })->throws(ExpectationFailedException::class); test('not failures', function () { expect([1, 2, 42])->not->toContain(42); })->throws(ExpectationFailedException::class); test('not failures with multiple needles (all failing)', function () { expect([1, 2, 42])->not->toContain(42, 2); })->throws(ExpectationFailedException::class); test('not failures with multiple needles (some failing)', function () { expect([1, 2, 42])->not->toContain(42, 1); })->throws(ExpectationFailedException::class); ================================================ FILE: tests/Features/Expect/toContainEqual.php ================================================ toContainEqual('42'); }); test('passes arrays with multiple needles', function () { expect([1, 2, 42])->toContainEqual('42', '2'); }); test('failures', function () { expect([1, 2, 42])->toContainEqual('3'); })->throws(ExpectationFailedException::class); test('failures with multiple needles (all failing)', function () { expect([1, 2, 42])->toContainEqual('3', '4'); })->throws(ExpectationFailedException::class); test('failures with multiple needles (some failing)', function () { expect([1, 2, 42])->toContainEqual('1', '3', '4'); })->throws(ExpectationFailedException::class); test('not failures', function () { expect([1, 2, 42])->not->toContainEqual('42'); })->throws(ExpectationFailedException::class); test('not failures with multiple needles (all failing)', function () { expect([1, 2, 42])->not->toContainEqual('42', '2'); })->throws(ExpectationFailedException::class); test('not failures with multiple needles (some failing)', function () { expect([1, 2, 42])->not->toContainEqual('42', '1'); })->throws(ExpectationFailedException::class); ================================================ FILE: tests/Features/Expect/toContainOnlyInstancesOf.php ================================================ times = [new DateTimeImmutable, new DateTimeImmutable]; }); test('pass', function () { expect($this->times)->toContainOnlyInstancesOf(DateTimeImmutable::class); expect($this->times)->not->toContainOnlyInstancesOf(DateTime::class); }); test('failures', function () { expect($this->times)->toContainOnlyInstancesOf(DateTime::class); })->throws(ExpectationFailedException::class); test('failures with custom message', function () { expect($this->times)->toContainOnlyInstancesOf(DateTime::class, 'oh no!'); })->throws(ExpectationFailedException::class, 'oh no!'); test('not failures', function () { expect($this->times)->not->toContainOnlyInstancesOf(DateTimeImmutable::class); })->throws(ExpectationFailedException::class); ================================================ FILE: tests/Features/Expect/toEndWith.php ================================================ toEndWith('name'); }); test('failures', function () { expect('username')->toEndWith('password'); })->throws(ExpectationFailedException::class); test('failures with custom message', function () { expect('username')->toEndWith('password', 'oh no!'); })->throws(ExpectationFailedException::class, 'oh no!'); test('not failures', function () { expect('username')->not->toEndWith('name'); })->throws(ExpectationFailedException::class); ================================================ FILE: tests/Features/Expect/toEqual.php ================================================ toEqual(123); }); test('failures', function () { expect(['a', 'b', 'c'])->toEqual(['a', 'b']); })->throws(ExpectationFailedException::class); test('failures with custom message', function () { expect(['a', 'b', 'c'])->toEqual(['a', 'b'], 'oh no!'); })->throws(ExpectationFailedException::class, 'oh no!'); test('not failures', function () { expect('042')->not->toEqual(42); })->throws(ExpectationFailedException::class); ================================================ FILE: tests/Features/Expect/toEqualCanonicalizing.php ================================================ toEqualCanonicalizing([3, 1, 2]); expect(['g', 'a', 'z'])->not->toEqualCanonicalizing(['a', 'z']); }); test('failures', function () { expect([3, 2, 1])->toEqualCanonicalizing([1, 2]); })->throws(ExpectationFailedException::class); test('failures with custom message', function () { expect([3, 2, 1])->toEqualCanonicalizing([1, 2], 'oh no!'); })->throws(ExpectationFailedException::class, 'oh no!'); test('not failures', function () { expect(['a', 'b', 'c'])->not->toEqualCanonicalizing(['b', 'a', 'c']); })->throws(ExpectationFailedException::class); ================================================ FILE: tests/Features/Expect/toEqualWithDelta.php ================================================ toEqualWithDelta(1.3, .4); }); test('failures with custom message', function () { expect(1.0)->toEqualWithDelta(1.5, .1, 'oh no!'); })->throws(ExpectationFailedException::class, 'oh no!'); test('not failures', function () { expect(1.0)->not->toEqualWithDelta(1.6, .7); })->throws(ExpectationFailedException::class); ================================================ FILE: tests/Features/Expect/toHaveAttribute.php ================================================ expect('Tests\\Fixtures\\Arch\\ToHaveAttribute\\HaveAttribute') ->toHaveAttribute('Tests\\Fixtures\\Arch\\ToHaveAttribute\\Attributes\\AsAttribute'); test('opposite class has attribute') ->throws(ArchExpectationFailedException::class) ->expect('Tests\\Fixtures\\Arch\\ToHaveAttribute\\HaveAttribute') ->not ->toHaveAttribute('Tests\\Fixtures\\Arch\\ToHaveAttribute\\Attributes\\AsAttribute'); test('class not has attribute') ->expect('Tests\\Fixtures\\Arch\\ToHaveAttribute\\NotHaveAttribute') ->not ->toHaveAttribute('Tests\\Fixtures\\Arch\\ToHaveAttribute\\Attributes\\AsAttribute'); ================================================ FILE: tests/Features/Expect/toHaveCamelCaseKeys.php ================================================ true, 'camelCase' => [ 'camel' => true, 'camelCase' => [ 'camel' => true, 'camelCase' => true, ], 'list' => [ 'abc', 'def', 'ghi', ], ], ]; test('pass', function () use ($array) { expect($array)->toHaveCamelCaseKeys(); }); test('failures', function () { expect('not-an-array')->toHaveCamelCaseKeys(); })->throws(InvalidExpectationValue::class); test('failures with message', function () use ($array) { expect($array)->not->toHaveCamelCaseKeys('oh no!'); })->throws(ExpectationFailedException::class, 'oh no!'); test('not failures', function () use ($array) { expect($array)->not->toHaveCamelCaseKeys(); })->throws(ExpectationFailedException::class); ================================================ FILE: tests/Features/Expect/toHaveConstructor.php ================================================ expect('Tests\Fixtures\Arch\ToHaveConstructor\HasConstructor\HasConstructor') ->toHaveConstructor(); test('class has no constructor') ->expect('Tests\Fixtures\Arch\ToHaveConstructor\HasNoConstructor\HasNoConstructor') ->not->toHaveConstructor(); ================================================ FILE: tests/Features/Expect/toHaveCount.php ================================================ toHaveCount(3); }); test('failures with invalid type', function () { expect('foo')->toHaveCount(3); })->throws(InvalidExpectationValue::class, 'Invalid expectation value type. Expected [countable|iterable]'); test('failures', function () { expect([1, 2, 3])->toHaveCount(4); })->throws(ExpectationFailedException::class); test('failures with message', function () { expect([1, 2, 3])->toHaveCount(4, 'oh no!'); })->throws(ExpectationFailedException::class, 'oh no!'); test('not failures', function () { expect([1, 2, 3])->not->toHaveCount(3); })->throws(ExpectationFailedException::class); ================================================ FILE: tests/Features/Expect/toHaveDestructor.php ================================================ expect('Tests\Fixtures\Arch\ToHaveDestructor\HasDestructor\HasDestructor') ->toHaveDestructor(); test('class has no destructor') ->expect('Tests\Fixtures\Arch\ToHaveDestructor\HasNoDestructor\HasNoDestructor') ->not->toHaveDestructor(); ================================================ FILE: tests/Features/Expect/toHaveFileSystemPermissions.php ================================================ true, 'kebab-case' => [ 'kebab' => true, 'kebab-case' => [ 'kebab' => true, 'kebab-case' => true, ], 'list' => [ 'abc', 'def', 'ghi', ], ], ]; test('pass', function () use ($array) { expect($array)->toHaveKebabCaseKeys(); }); test('failures', function () { expect('not-an-array')->toHaveKebabCaseKeys(); })->throws(InvalidExpectationValue::class); test('failures with message', function () use ($array) { expect($array)->not->toHaveKebabCaseKeys('oh no!'); })->throws(ExpectationFailedException::class, 'oh no!'); test('not failures', function () use ($array) { expect($array)->not->toHaveKebabCaseKeys(); })->throws(ExpectationFailedException::class); ================================================ FILE: tests/Features/Expect/toHaveKey.php ================================================ 1, 'b', 'c' => 'world', 'd' => [ 'e' => 'hello', ], 'key.with.dots' => false, ]; test('pass')->expect($test_array)->toHaveKey('c'); test('pass with nested key')->expect($test_array)->toHaveKey('d.e'); test('pass with plain key with dots')->expect($test_array)->toHaveKey('key.with.dots'); test('pass with value check')->expect($test_array)->toHaveKey('c', 'world'); test('pass with value check and nested key')->expect($test_array)->toHaveKey('d.e', 'hello'); test('pass with value check and plain key with dots')->expect($test_array)->toHaveKey('key.with.dots', false); test('failures', function () use ($test_array) { expect($test_array)->toHaveKey('foo'); })->throws(ExpectationFailedException::class, "Failed asserting that an array has the key 'foo'"); test('failures with custom message', function () use ($test_array) { expect($test_array)->toHaveKey('foo', message: 'oh no!'); })->throws(ExpectationFailedException::class, 'oh no!'); test('failures with custom message and Any matcher', function () use ($test_array) { expect($test_array)->toHaveKey('foo', expect()->any(), 'oh no!'); })->throws(ExpectationFailedException::class, 'oh no!'); test('failures with nested key', function () use ($test_array) { expect($test_array)->toHaveKey('d.bar'); })->throws(ExpectationFailedException::class, "Failed asserting that an array has the key 'd.bar'"); test('failures with nested key and custom message', function () use ($test_array) { expect($test_array)->toHaveKey('d.bar', message: 'oh no!'); })->throws(ExpectationFailedException::class, 'oh no!'); test('failures with nested key and custom message with Any matcher', function () use ($test_array) { expect($test_array)->toHaveKey('d.bar', expect()->any(), 'oh no!'); })->throws(ExpectationFailedException::class, 'oh no!'); test('failures with plain key with dots', function () use ($test_array) { expect($test_array)->toHaveKey('missing.key.with.dots'); })->throws(ExpectationFailedException::class, "Failed asserting that an array has the key 'missing.key.with.dots'"); test('fails with wrong value', function () use ($test_array) { expect($test_array)->toHaveKey('c', 'bar'); })->throws(ExpectationFailedException::class); test('fails with wrong value and nested key', function () use ($test_array) { expect($test_array)->toHaveKey('d.e', 'foo'); })->throws(ExpectationFailedException::class); test('fails with wrong value and plain key with dots', function () use ($test_array) { expect($test_array)->toHaveKey('key.with.dots', true); })->throws(ExpectationFailedException::class); test('not failures', function () use ($test_array) { expect($test_array)->not->toHaveKey('c'); })->throws(ExpectationFailedException::class, "Expecting […] not to have key 'c'"); test('not failures with nested key', function () use ($test_array) { expect($test_array)->not->toHaveKey('d.e'); })->throws(ExpectationFailedException::class, "Expecting […] not to have key 'd.e'"); test('not failures with plain key with dots', function () use ($test_array) { expect($test_array)->not->toHaveKey('key.with.dots'); })->throws(ExpectationFailedException::class, "Expecting […] not to have key 'key.with.dots'"); test('not failures with correct value', function () use ($test_array) { expect($test_array)->not->toHaveKey('c', 'world'); })->throws(ExpectationFailedException::class); test('not failures with correct value and with nested key', function () use ($test_array) { expect($test_array)->not->toHaveKey('d.e', 'hello'); })->throws(ExpectationFailedException::class); test('not failures with correct value and with plain key with dots', function () use ($test_array) { expect($test_array)->not->toHaveKey('key.with.dots', false); })->throws(ExpectationFailedException::class); ================================================ FILE: tests/Features/Expect/toHaveKeys.php ================================================ 1, 'b', 'c' => 'world', 'foo' => ['bar' => 'baz']])->toHaveKeys(['a', 'c', 'foo.bar']); }); test('pass with multi-dimensional arrays', function () { expect(['a' => 1, 'b', 'c' => 'world', 'foo' => ['bar' => ['bir' => 'biz']]])->toHaveKeys(['a', 'c', 'foo' => ['bar' => ['bir']]]); }); test('failures', function () { expect(['a' => 1, 'b', 'c' => 'world', 'foo' => ['bar' => 'baz']])->toHaveKeys(['a', 'd', 'foo.bar', 'hello.world']); })->throws(ExpectationFailedException::class); test('failures with custom message', function () { expect(['a' => 1, 'b', 'c' => 'world', 'foo' => ['bar' => 'baz']])->toHaveKeys(['a', 'd', 'foo.bar', 'hello.world'], 'oh no!'); })->throws(ExpectationFailedException::class, 'oh no!'); test('failures with multi-dimensional arrays', function () { expect(['a' => 1, 'b', 'c' => 'world', 'foo' => ['bar' => ['bir' => 'biz']]])->toHaveKeys(['a', 'd', 'foo' => ['bar' => 'bir'], 'hello.world']); })->throws(ExpectationFailedException::class); test('failures with multi-dimensional arrays and custom message', function () { expect(['a' => 1, 'b', 'c' => 'world', 'foo' => ['bar' => ['bir' => 'biz']]])->toHaveKeys(['a', 'd', 'foo' => ['bar' => 'bir'], 'hello.world'], 'oh no!'); })->throws(ExpectationFailedException::class, 'oh no!'); test('not failures', function () { expect(['a' => 1, 'b', 'c' => 'world', 'foo' => ['bar' => 'baz']])->not->toHaveKeys(['foo.bar', 'c', 'z']); })->throws(ExpectationFailedException::class); test('not failures with multi-dimensional arrays', function () { expect(['a' => 1, 'b', 'c' => 'world', 'foo' => ['bar' => ['bir' => 'biz']]])->not->toHaveKeys(['foo' => ['bar' => 'bir'], 'c', 'z']); })->throws(ExpectationFailedException::class); ================================================ FILE: tests/Features/Expect/toHaveLength.php ================================================ toHaveLength(9); })->with([ 'Fortaleza', 'Sollefteå', 'Ιεράπετρα', (object) [1, 2, 3, 4, 5, 6, 7, 8, 9], ]); it('passes with array', function () { expect([1, 2, 3])->toHaveLength(3); }); it('passes with *not*', function () { expect('')->not->toHaveLength(1); }); it('properly fails with *not*', function () { expect('pest')->not->toHaveLength(4, 'oh no!'); })->throws(ExpectationFailedException::class, 'oh no!'); it('fails', function () { expect([1, 1.5, true, null])->toHaveLength(1); })->throws(ExpectationFailedException::class); it('fails with message', function () { expect([1, 1.5, true, null])->toHaveLength(1, 'oh no!'); })->throws(ExpectationFailedException::class, 'oh no!'); ================================================ FILE: tests/Features/Expect/toHaveLineCountLessThan.php ================================================ toHaveLineCountLessThan(2000); }); it('fails', function () { expect(Expectation::class)->toHaveLineCountLessThan(10); })->throws(ArchExpectationFailedException::class); ================================================ FILE: tests/Features/Expect/toHaveMethod.php ================================================ expect('Tests\Fixtures\Arch\ToHaveMethod\HasMethod\HasMethod') ->toHaveMethod('foo'); test('opposite class has method') ->throws(ArchExpectationFailedException::class) ->expect('Tests\Fixtures\Arch\ToHaveMethod\HasMethod\HasMethod') ->not->toHaveMethod('foo'); test('class has method via a parent class') ->expect('Tests\Fixtures\Arch\ToHaveMethod\HasMethod\HasMethodViaParent') ->toHaveMethod('foo'); test('class has method via a trait') ->expect('Tests\Fixtures\Arch\ToHaveMethod\HasMethod\HasMethodViaTrait') ->toHaveMethod('foo'); test('failure when the class has no method') ->throws(ArchExpectationFailedException::class) ->expect('Tests\Fixtures\Arch\ToHaveMethod\HasNoMethod\HasNoMethodClass') ->toHaveMethod('foo'); test('class has no method') ->expect('Tests\Fixtures\Arch\ToHaveMethod\HasNoMethod\HasNoMethodClass') ->not->toHaveMethod('foo'); ================================================ FILE: tests/Features/Expect/toHaveMethods.php ================================================ expect('Tests\Fixtures\Arch\ToHaveMethod\HasMethod\HasMethod') ->toHaveMethods(['foo']); test('opposite class has method') ->throws(ArchExpectationFailedException::class) ->expect('Tests\Fixtures\Arch\ToHaveMethod\HasMethod\HasMethod') ->not->toHaveMethods(['foo']); test('class has method via a parent class') ->expect('Tests\Fixtures\Arch\ToHaveMethod\HasMethod\HasMethodViaParent') ->toHaveMethods(['foo']); test('class has method via a trait') ->expect('Tests\Fixtures\Arch\ToHaveMethod\HasMethod\HasMethodViaTrait') ->toHaveMethods(['foo']); test('failure when the class has no method') ->throws(ArchExpectationFailedException::class) ->expect('Tests\Fixtures\Arch\ToHaveMethod\HasNoMethod\HasNoMethodClass') ->toHaveMethods(['foo']); test('class has no method') ->expect('Tests\Fixtures\Arch\ToHaveMethod\HasNoMethod\HasNoMethodClass') ->not->toHaveMethods(['foo']); ================================================ FILE: tests/Features/Expect/toHaveMethodsDocumented.php ================================================ toHaveMethodsDocumented() ->and(ExampleTest::class)->not->toHaveMethodsDocumented(); }); it('fails 1', function () { expect(ExampleTest::class)->toHaveMethodsDocumented(); })->throws(ArchExpectationFailedException::class); it('fails 2', function () { expect(Configuration::class)->not->toHaveMethodsDocumented(); })->throws(ArchExpectationFailedException::class); ================================================ FILE: tests/Features/Expect/toHavePrefix.php ================================================ throws(ArchExpectationFailedException::class) ->expect('Tests\\Fixtures\\Arch\\ToHavePrefix\\HasNoPrefix') ->toHavePrefix('Prefix'); test('has prefix') ->expect('Tests\\Fixtures\\Arch\\ToHavePrefix\\HasPrefix') ->toHavePrefix('Prefix'); test('opposite missing prefix') ->throws(ArchExpectationFailedException::class) ->expect('Tests\\Fixtures\\Arch\\ToHavePrefix\\HasPrefix') ->not->toHavePrefix('Prefix'); test('opposite has prefix') ->expect('Tests\\Fixtures\\Arch\\ToHavePrefix\\HasNoPrefix') ->not->toHavePrefix('Prefix'); ================================================ FILE: tests/Features/Expect/toHavePrivateMethodsBesides.php ================================================ not->toHavePrivateMethodsBesides(['privateMethod']); }); test('failures', function () { expect(UserController::class)->not->toHavePrivateMethods(); })->throws(ArchExpectationFailedException::class); ================================================ FILE: tests/Features/Expect/toHaveProperties.php ================================================ name = 'John'; $object->age = 21; expect($object) ->toHaveProperties(['name', 'age']) ->toHaveProperties([ 'name' => 'John', 'age' => 21, ]); }); test('failures', function () { $object = new stdClass; $object->name = 'John'; expect($object) ->toHaveProperties(['name', 'age']) ->toHaveProperties([ 'name' => 'John', 'age' => 21, ]); })->throws(ExpectationFailedException::class); test('failures with custom message', function () { $object = new stdClass; $object->name = 'John'; expect($object) ->toHaveProperties(['name', 'age'], 'oh no!') ->toHaveProperties([ 'name' => 'John', 'age' => 21, ], 'oh no!'); })->throws(ExpectationFailedException::class, 'oh no!'); test('not failures', function () { $object = new stdClass; $object->name = 'John'; $object->age = 21; expect($object)->not->toHaveProperties(['name', 'age']) ->not->toHaveProperties([ 'name' => 'John', 'age' => 21, ]); })->throws(ExpectationFailedException::class); ================================================ FILE: tests/Features/Expect/toHavePropertiesDocumented.php ================================================ toHavePropertiesDocumented() ->and(ExampleTest::class)->not->toHavePropertiesDocumented(); }); it('fails 1', function () { expect(ExampleTest::class)->toHavePropertiesDocumented(); })->throws(ArchExpectationFailedException::class); it('fails 2', function () { expect(TestCaseFactory::class)->not->toHavePropertiesDocumented(); })->throws(ArchExpectationFailedException::class); ================================================ FILE: tests/Features/Expect/toHaveProperty.php ================================================ foo = 'bar'; $obj->fooNull = null; test('pass', function () use ($obj) { expect($obj)->toHaveProperty('foo'); expect($obj)->toHaveProperty('foo', 'bar'); expect($obj)->toHaveProperty('fooNull'); expect($obj)->toHaveProperty('fooNull', null); }); test('failures', function () use ($obj) { expect($obj)->toHaveProperty('bar'); })->throws(ExpectationFailedException::class); test('failures with message', function () use ($obj) { expect($obj)->toHaveProperty(name: 'bar', message: 'oh no!'); })->throws(ExpectationFailedException::class, 'oh no!'); test('failures with message and Any matcher', function () use ($obj) { expect($obj)->toHaveProperty('bar', expect()->any(), 'oh no!'); })->throws(ExpectationFailedException::class, 'oh no!'); test('not failures', function () use ($obj) { expect($obj)->not->toHaveProperty('foo'); })->throws(ExpectationFailedException::class); ================================================ FILE: tests/Features/Expect/toHaveProtectedMethodsBesides.php ================================================ not->toHaveProtectedMethodsBesides(['protectedMethod']); }); test('failures', function () { expect(UserController::class)->not->toHaveProtectedMethods(); })->throws(ArchExpectationFailedException::class); ================================================ FILE: tests/Features/Expect/toHavePublicMethodsBesides.php ================================================ not->toHavePublicMethodsBesides(['publicMethod']); }); test('failures', function () { expect(UserController::class)->not->toHavePublicMethods(); })->throws(ArchExpectationFailedException::class); ================================================ FILE: tests/Features/Expect/toHaveSameSize.php ================================================ toHaveSameSize([1]); })->throws(InvalidExpectationValue::class, 'Invalid expectation value type. Expected [countable|iterable].'); test('pass', function () { expect([1, 2, 3])->toHaveSameSize([4, 5, 6]); }); test('failures', function () { expect([1, 2, 3])->toHaveSameSize([1]); })->throws(ExpectationFailedException::class); test('failures with message', function () { expect([1, 2, 3])->toHaveSameSize([1], 'oh no!'); })->throws(ExpectationFailedException::class, 'oh no!'); test('not failures', function () { expect([1, 2, 3])->not->toHaveSameSize([1]); }); ================================================ FILE: tests/Features/Expect/toHaveSnakeCaseKeys.php ================================================ true, 'snake_case' => [ 'snake' => true, 'snake_case' => [ 'snake' => true, 'snake_case' => true, ], 'list' => [ 'abc', 'def', 'ghi', ], ], ]; test('pass', function () use ($array) { expect($array)->toHaveSnakeCaseKeys(); }); test('failures', function () { expect('not-an-array')->toHaveSnakeCaseKeys(); })->throws(InvalidExpectationValue::class); test('failures with message', function () use ($array) { expect($array)->not->toHaveSnakeCaseKeys('oh no!'); })->throws(ExpectationFailedException::class, 'oh no!'); test('not failures', function () use ($array) { expect($array)->not->toHaveSnakeCaseKeys(); })->throws(ExpectationFailedException::class); ================================================ FILE: tests/Features/Expect/toHaveStudlyCaseKeys.php ================================================ true, 'StudlyCase' => [ 'Studly' => true, 'StudlyCase' => [ 'Studly' => true, 'StudlyCase' => true, ], 'List' => [ 'abc', 'def', 'ghi', ], ], ]; test('pass', function () use ($array) { expect($array)->toHaveStudlyCaseKeys(); }); test('failures', function () { expect('not-an-array')->toHaveStudlyCaseKeys(); })->throws(InvalidExpectationValue::class); test('failures with message', function () use ($array) { expect($array)->not->toHaveStudlyCaseKeys('oh no!'); })->throws(ExpectationFailedException::class, 'oh no!'); test('not failures', function () use ($array) { expect($array)->not->toHaveStudlyCaseKeys(); })->throws(ExpectationFailedException::class); ================================================ FILE: tests/Features/Expect/toHaveSuffix.php ================================================ throws(ArchExpectationFailedException::class) ->expect('Tests\\Fixtures\\Arch\\ToHaveSuffix\\HasNoSuffix') ->toHaveSuffix('Suffix'); test('has suffix') ->expect('Tests\\Fixtures\\Arch\\ToHaveSuffix\\HasSuffix') ->toHaveSuffix('Suffix'); test('opposite missing suffix') ->throws(ArchExpectationFailedException::class) ->expect('Tests\\Fixtures\\Arch\\ToHaveSuffix\\HasSuffix') ->not->toHaveSuffix('Suffix'); test('opposite has suffix') ->expect('Tests\\Fixtures\\Arch\\ToHaveSuffix\\HasNoSuffix') ->not->toHaveSuffix('Suffix'); ================================================ FILE: tests/Features/Expect/toMatch.php ================================================ toMatch('/^hello wo.*$/i'); }); test('failures', function () { expect('Hello World')->toMatch('/^hello$/i'); })->throws(ExpectationFailedException::class); test('failures with custom message', function () { expect('Hello World')->toMatch('/^hello$/i', 'oh no!'); })->throws(ExpectationFailedException::class, 'oh no!'); test('not failures', function () { expect('Hello World')->not->toMatch('/^hello wo.*$/i'); })->throws(ExpectationFailedException::class); ================================================ FILE: tests/Features/Expect/toMatchArray.php ================================================ user = [ 'id' => 1, 'name' => 'Nuno', 'email' => 'enunomaduro@gmail.com', ]; }); test('pass', function () { expect($this->user)->toMatchArray([ 'name' => 'Nuno', 'email' => 'enunomaduro@gmail.com', ]); }); test('failures', function () { expect($this->user)->toMatchArray([ 'name' => 'Not the same name', 'email' => 'enunomaduro@gmail.com', ]); })->throws(ExpectationFailedException::class); test('failures with custom message', function () { expect($this->user)->toMatchArray([ 'name' => 'Not the same name', 'email' => 'enunomaduro@gmail.com', ], 'oh no!'); })->throws(ExpectationFailedException::class, 'oh no!'); test('not failures', function () { expect($this->user)->not->toMatchArray([ 'id' => 1, ]); })->throws(ExpectationFailedException::class); ================================================ FILE: tests/Features/Expect/toMatchConstraint.php ================================================ toMatchConstraint(new IsTrue); }); test('failures', function () { expect(false)->toMatchConstraint(new IsTrue); })->throws(ExpectationFailedException::class); test('failures with custom message', function () { expect(false)->toMatchConstraint(new IsTrue, 'oh no!'); })->throws(ExpectationFailedException::class, 'oh no!'); test('not failures', function () { expect(true)->not->toMatchConstraint(new IsTrue); })->throws(ExpectationFailedException::class); ================================================ FILE: tests/Features/Expect/toMatchObject.php ================================================ user = (object) [ 'id' => 1, 'name' => 'Nuno', 'email' => 'enunomaduro@gmail.com', ]; }); test('pass', function () { expect($this->user)->toMatchObject([ 'name' => 'Nuno', 'email' => 'enunomaduro@gmail.com', ]); }); test('pass with class', function () { expect(new class { public $name = 'Nuno'; public $email = 'enunomaduro@gmail.com'; })->toMatchObject([ 'name' => 'Nuno', 'email' => 'enunomaduro@gmail.com', ]); }); test('failures', function () { expect($this->user)->toMatchObject([ 'name' => 'Not the same name', 'email' => 'enunomaduro@gmail.com', ]); })->throws(ExpectationFailedException::class); test('failures with custom message', function () { expect($this->user)->toMatchObject([ 'name' => 'Not the same name', 'email' => 'enunomaduro@gmail.com', ], 'oh no!'); })->throws(ExpectationFailedException::class, 'oh no!'); test('not failures', function () { expect($this->user)->not->toMatchObject([ 'id' => 1, ]); })->throws(ExpectationFailedException::class); ================================================ FILE: tests/Features/Expect/toMatchSnapshot.php ================================================ snapshotable = <<<'HTML'

Snapshot

HTML; }); test('pass', function () { TestSuite::getInstance()->snapshots->save($this->snapshotable); expect($this->snapshotable)->toMatchSnapshot(); }); expect()->pipe('toMatchSnapshot', function (Closure $next) { if (is_string($this->value)) { $this->value = preg_replace( '/name="_token" value=".*"/', 'name="_token" value="1"', $this->value ); } return $next(); }); test('pass using pipes', function () { expect('') ->toMatchSnapshot(); }); test('pass with `__toString`', function () { TestSuite::getInstance()->snapshots->save($this->snapshotable); $object = new class($this->snapshotable) { public function __construct(protected string $snapshotable) {} public function __toString() { return $this->snapshotable; } }; expect($object)->toMatchSnapshot(); }); test('pass with `toString`', function () { TestSuite::getInstance()->snapshots->save($this->snapshotable); $object = new class($this->snapshotable) { public function __construct(protected string $snapshotable) {} public function toString() { return $this->snapshotable; } }; expect($object)->toMatchSnapshot(); }); test('pass with dataset', function ($data) { TestSuite::getInstance()->snapshots->save($this->snapshotable); [$filename] = TestSuite::getInstance()->snapshots->get(); expect($filename)->toStartWith('tests/.pest/snapshots/') ->toEndWith('pass_with_dataset_with_data_set____my_datas_set_value___.snap') ->and($this->snapshotable)->toMatchSnapshot(); })->with(['my-datas-set-value']); describe('within describe', function () { test('pass with dataset', function ($data) { TestSuite::getInstance()->snapshots->save($this->snapshotable); [$filename] = TestSuite::getInstance()->snapshots->get(); expect($filename)->toStartWith('tests/.pest/snapshots/') ->toEndWith('pass_with_dataset_with_data_set____my_datas_set_value___.snap') ->and($this->snapshotable)->toMatchSnapshot(); }); })->with(['my-datas-set-value']); test('pass with `toArray`', function () { TestSuite::getInstance()->snapshots->save(json_encode(['key' => $this->snapshotable], JSON_PRETTY_PRINT)); $object = new class($this->snapshotable) { public function __construct(protected string $snapshotable) {} public function toArray() { return [ 'key' => $this->snapshotable, ]; } }; expect($object)->toMatchSnapshot(); }); test('pass with array', function () { TestSuite::getInstance()->snapshots->save(json_encode(['key' => $this->snapshotable], JSON_PRETTY_PRINT)); expect([ 'key' => $this->snapshotable, ])->toMatchSnapshot(); }); test('pass with `toSnapshot`', function () { TestSuite::getInstance()->snapshots->save(json_encode(['key' => $this->snapshotable], JSON_PRETTY_PRINT)); $object = new class($this->snapshotable) { public function __construct(protected string $snapshotable) {} public function toSnapshot() { return json_encode([ 'key' => $this->snapshotable, ], JSON_PRETTY_PRINT); } }; expect($object)->toMatchSnapshot(); }); test('failures', function () { TestSuite::getInstance()->snapshots->save($this->snapshotable); expect('contain that does not match snapshot')->toMatchSnapshot(); })->throws(ExpectationFailedException::class, 'Failed asserting that two strings are identical.'); test('failures with custom message', function () { TestSuite::getInstance()->snapshots->save($this->snapshotable); expect('contain that does not match snapshot')->toMatchSnapshot('oh no'); })->throws(ExpectationFailedException::class, 'oh no'); test('not failures', function () { TestSuite::getInstance()->snapshots->save($this->snapshotable); expect($this->snapshotable)->not->toMatchSnapshot(); })->throws(ExpectationFailedException::class); test('multiple snapshot expectations', function () { expect('foo bar 1')->toMatchSnapshot(); expect('foo bar 2')->toMatchSnapshot(); }); test('multiple snapshot expectations with datasets', function () { expect('foo bar 1')->toMatchSnapshot(); expect('foo bar 2')->toMatchSnapshot(); })->with([1, 'foo', 'bar', 'baz']); describe('describable', function () { test('multiple snapshot expectations with describe', function () { expect('foo bar 1')->toMatchSnapshot(); expect('foo bar 2')->toMatchSnapshot(); }); }); test('multiple snapshot expectations with repeat', function () { expect('foo bar 1')->toMatchSnapshot(); expect('foo bar 2')->toMatchSnapshot(); })->repeat(10); ================================================ FILE: tests/Features/Expect/toStartWith.php ================================================ toStartWith('user'); }); test('failures', function () { expect('username')->toStartWith('password'); })->throws(ExpectationFailedException::class); test('failures with custom message', function () { expect('username')->toStartWith('password', 'oh no!'); })->throws(ExpectationFailedException::class, 'oh no!'); test('not failures', function () { expect('username')->not->toStartWith('user'); })->throws(ExpectationFailedException::class); ================================================ FILE: tests/Features/Expect/toThrow.php ================================================ toThrow(RuntimeException::class); expect(function () { throw new RuntimeException; })->toThrow(Exception::class); expect(function () { throw new RuntimeException; })->toThrow(function (RuntimeException $e) {}); expect(function () { throw new RuntimeException('actual message'); })->toThrow(function (Exception $e) { expect($e->getMessage())->toBe('actual message'); }); expect(function () {})->not->toThrow(Exception::class); expect(function () { throw new RuntimeException('actual message'); })->toThrow('actual message'); expect(function () { throw new Exception; })->not->toThrow(RuntimeException::class); expect(function () { throw new RuntimeException('actual message'); })->toThrow(RuntimeException::class, 'actual message'); expect(function () { throw new RuntimeException('actual message'); })->toThrow(function (RuntimeException $e) {}, 'actual message'); expect(function () { throw new CustomException('foo'); })->toThrow(new CustomException('foo')); }); test('failures 1', function () { expect(function () {})->toThrow(RuntimeException::class); })->throws(ExpectationFailedException::class, 'Exception "'.RuntimeException::class.'" not thrown.'); test('failures 2', function () { expect(function () {})->toThrow(function (RuntimeException $e) {}); })->throws(ExpectationFailedException::class, 'Exception "'.RuntimeException::class.'" not thrown.'); test('failures 3', function () { expect(function () { throw new Exception; })->toThrow(function (RuntimeException $e) { // }); })->throws(ExpectationFailedException::class, 'Failed asserting that an instance of class Exception is an instance of class RuntimeException.'); test('failures 4', function () { expect(function () { throw new Exception('actual message'); }) ->toThrow(function (Exception $e) { expect($e->getMessage())->toBe('expected message'); }); })->throws(ExpectationFailedException::class, 'Failed asserting that two strings are identical'); test('failures 5', function () { expect(function () { throw new Exception('actual message'); })->toThrow('expected message'); })->throws(ExpectationFailedException::class, 'Failed asserting that \'actual message\' [ASCII](length: 14) contains "expected message" [ASCII](length: 16).'); test('failures 6', function () { expect(function () {})->toThrow('actual message'); })->throws(ExpectationFailedException::class, 'Exception with message "actual message" not thrown'); test('failures 7', function () { expect(function () { throw new RuntimeException('actual message'); })->toThrow(RuntimeException::class, 'expected message'); })->throws(ExpectationFailedException::class); test('failures 8', function () { expect(function () { throw new CustomException('actual message'); })->toThrow(new CustomException('expected message')); })->throws(ExpectationFailedException::class); test('failures with custom message', function () { expect(function () { throw new RuntimeException('actual message'); })->toThrow(RuntimeException::class, 'expected message', 'oh no!'); })->throws(ExpectationFailedException::class, 'oh no!'); test('not failures', function () { expect(function () { throw new RuntimeException; })->not->toThrow(RuntimeException::class); })->throws(ExpectationFailedException::class); test('closure missing parameter', function () { expect(function () {})->toThrow(function () {}); })->throws(InvalidArgumentException::class, 'The given closure must have a single parameter type-hinted as the class string.'); test('closure missing type-hint', function () { expect(function () {})->toThrow(function ($e) {}); })->throws(InvalidArgumentException::class, 'The given closure\'s parameter must be type-hinted as the class string.'); it('can handle a non-defined exception', function () { expect(function () { throw new NonExistingException; })->toThrow(NonExistingException::class); })->throws(Error::class, 'Class "NonExistingException" not found'); it('can handle a class not found Error', function () { expect(function () { throw new NonExistingException; })->toThrow('Class "NonExistingException" not found'); expect(function () { throw new NonExistingException; })->toThrow(Error::class, 'Class "NonExistingException" not found'); }); ================================================ FILE: tests/Features/Expect/toUseStrictEquality.php ================================================ throws(ArchExpectationFailedException::class) ->expect('Tests\\Fixtures\\Arch\\ToUseStrictEquality\\NotStrictEquality') ->toUseStrictEquality(); test('has strict equality') ->expect('Tests\\Fixtures\\Arch\\ToUseStrictEquality\\StrictEquality') ->toUseStrictEquality(); test('opposite missing strict equality') ->throws(ArchExpectationFailedException::class) ->expect('Tests\\Fixtures\\Arch\\ToUseStrictEquality\\StrictEquality') ->not->toUseStrictEquality(); test('opposite has strict equality') ->expect('Tests\\Fixtures\\Arch\\ToUseStrictEquality\\NotStrictEquality') ->not->toUseStrictEquality(); ================================================ FILE: tests/Features/Expect/toUseStrictTypes.php ================================================ toUseStrictTypes() ->and(HasStrictTypeWithCommentsAbove::class)->toUseStrictTypes(); }); test('failures', function () { expect(HasNoStrictType::class)->toUseStrictTypes(); })->throws(ArchExpectationFailedException::class); ================================================ FILE: tests/Features/Expect/toUseTrait.php ================================================ toUseTrait('Pest\Concerns\Retrievable') ->and('Pest\Expectations\EachExpectation')->not->toUseTrait('Pest\Concerns\Retrievable'); }); test('failures', function () { expect('Pest\Expectations\EachExpectation')->toUseTrait('Pest\Concerns\Foo'); })->throws(ArchExpectationFailedException::class); test('not failures', function () { expect('Pest\Expectations\HigherOrderExpectation')->not->toUseTrait('Pest\Concerns\Retrievable'); })->throws(ArchExpectationFailedException::class); ================================================ FILE: tests/Features/Expect/unless.php ================================================ unlessObject = new stdClass; $this->unlessObject->trueValue = true; $this->unlessObject->foo = 'foo'; }); it('pass', function () { expect('foo') ->unless( true, function ($value) { return $value->toEqual('bar'); } ) ->toEqual('foo'); expect(static::getCount())->toBe(1); }); it('failures', function () { expect('foo') ->unless( false, function ($value) { return $value->toBeTrue(); } ) ->toEqual('foo'); })->throws(ExpectationFailedException::class, 'is true'); it('runs with truthy', function () { expect($this->unlessObject) ->unless( 0, function ($value) { return $value->trueValue->toBeTrue(); } ) ->foo->toEqual('foo'); expect(static::getCount())->toBe(2); }); it('skips with falsy', function () { expect($this->unlessObject) ->unless( 1, function ($value) { return $value->trueValue->toBeFalse(); // fails } ) ->unless( true, function ($value) { return $value->trueValue->toBeFalse(); // fails } ) ->foo->toEqual('foo'); expect(static::getCount())->toBe(1); }); it('runs with truthy closure condition', function () { expect($this->unlessObject) ->unless( function () { return '0'; }, function ($value) { return $value->trueValue->toBeTrue(); } ) ->foo->toEqual('foo'); expect(static::getCount())->toBe(2); }); it('skips with falsy closure condition', function () { expect($this->unlessObject) ->unless( function () { return '1'; }, function ($value) { return $value->trueValue->toBeFalse(); // fails } ) ->foo->toEqual('foo'); expect(static::getCount())->toBe(1); }); it('can be used in higher order tests') ->expect(true) ->unless( function () { return false; }, function ($value) { return $value->toBeFalse(); } ) ->throws(ExpectationFailedException::class, 'true is false'); ================================================ FILE: tests/Features/Expect/when.php ================================================ whenObject = new stdClass; $this->whenObject->trueValue = true; $this->whenObject->foo = 'foo'; }); it('pass', function () { expect('foo') ->when( true, function ($value) { return $value->toEqual('foo'); } ) ->toEqual('foo'); expect(static::getCount())->toBe(2); }); it('failures', function () { expect('foo') ->when( true, function ($value) { return $value->toBeTrue(); } ) ->toEqual('foo'); })->throws(ExpectationFailedException::class, 'is true'); it('runs with truthy', function () { expect($this->whenObject) ->when( 1, function ($value) { return $value->trueValue->toBeTrue(); } ) ->foo->toEqual('foo'); expect(static::getCount())->toBe(2); }); it('skips with falsy', function () { expect($this->whenObject) ->when( 0, function ($value) { return $value->trueValue->toBeFalse(); // fails } ) ->when( false, function ($value) { return $value->trueValue->toBeFalse(); // fails } ) ->foo->toEqual('foo'); expect(static::getCount())->toBe(1); }); it('runs with truthy closure condition', function () { expect($this->whenObject) ->when( function () { return '1'; }, function ($value) { return $value->trueValue->toBeTrue(); } ) ->foo->toEqual('foo'); expect(static::getCount())->toBe(2); }); it('skips with falsy closure condition', function () { expect($this->whenObject) ->when( function () { return '0'; }, function ($value) { return $value->trueValue->toBeFalse(); // fails } ) ->foo->toEqual('foo'); expect(static::getCount())->toBe(1); }); it('can be used in higher order tests') ->expect(false) ->when( function () { return true; }, function ($value) { return $value->toBeTrue(); } ) ->throws(ExpectationFailedException::class, 'false is true'); ================================================ FILE: tests/Features/Fail.php ================================================ fail(); })->throws(AssertionFailedError::class); it('may fail with the given message', function () { $this->fail('this is a failure'); })->throws(AssertionFailedError::class, 'this is a failure'); ================================================ FILE: tests/Features/Fails.php ================================================ fail(); })->fails(); it('may fail with the given message', function () { $this->fail('this is a failure'); })->fails('this is a failure'); ================================================ FILE: tests/Features/Fixture.php ================================================ toBeString() ->toBeFile(); }); it('may throw an exception if the file does not exist', function () { fixture('file-that-does-not-exist.php'); })->throws(InvalidArgumentException::class); ================================================ FILE: tests/Features/Helpers.php ================================================ user = 'nuno'; } it('can set/get properties on $this', function () { addUser(); expect($this->user)->toBe('nuno'); }); it('gets null if property do not exist', function () { expect(test()->wqdwqdqw)->toBe(null); }); class User { public function getName() { return 'nuno'; } } function mockUser() { $mock = test()->createMock(User::class); $mock->method('getName') ->willReturn('maduro'); return $mock; } it('allows to call underlying protected/private methods', function () { $user = mockUser(); expect($user->getName())->toBe('maduro'); }); it('throws error if method do not exist', function () { test()->foo(); })->throws(ReflectionException::class, 'Call to undefined method PHPUnit\Framework\TestCase::foo()'); it('can forward unexpected calls to any global function')->_assertThat(); it('can use helpers from helpers file')->myAssertTrue(true); it('can use helpers from helpers directory')->myDirectoryAssertTrue(true); ================================================ FILE: tests/Features/HigherOrderTests.php ================================================ assertTrue(true); it('proxies calls to object')->assertTrue(true); it('is capable doing multiple assertions') ->assertTrue(true) ->assertFalse(false); it('resolves expect callables correctly') ->expect(function () { return 'foo'; }) ->toBeString() ->toBe('foo') ->and('bar') ->toBeString() ->toBe('bar'); test('does not treat method names as callables') ->expect('it')->toBeString(); it('can defer a method until after test setup') ->expect('foo')->toBeString() ->defer(function () { expect($this)->toBeInstanceOf(TestCase::class); }) ->toBe('foo') ->and('hello world')->toBeString(); it('can pass datasets into the expect callables') ->with([[1, 2, 3]]) ->expect(function (...$numbers) { return $numbers; })->toBe([1, 2, 3]) ->and(function (...$numbers) { return $numbers; })->toBe([1, 2, 3]); it('can pass datasets into the defer callable') ->with([[1, 2, 3]]) ->defer(function (...$numbers) { expect($numbers)->toBe([1, 2, 3]); }); it('can pass shared datasets into callables') ->with('numbers.closure.wrapped') ->expect(function ($value) { return $value; }) ->and(function ($value) { return $value; }) ->defer(function ($value) { expect($value)->toBeInt(); }) ->toBeInt(); afterEach()->assertTrue(true); ================================================ FILE: tests/Features/Incompleted.php ================================================ skip(false); it('is incompleted even with method calls like group')->group('wtv'); it('is not incompleted because of expect')->expect(true)->toBeTrue(); it('is not incompleted because of assert')->assertTrue(true); it('is not incompleted because of test with assertions', function () { expect(true)->toBeTrue(); }); describe('a "describe" group of tests', function () { it('is incompleted'); }); ================================================ FILE: tests/Features/Issue.php ================================================ toBeTrue(); })->issue(1); it('may be associated with an issue', function () { expect(true)->toBeTrue(); })->issue(2); describe('nested', function () { it('may be associated with an issue', function () { expect(true)->toBeTrue(); })->issue('#3'); })->issue(4)->note('an note between an the issue')->issue([5, 6]); ================================================ FILE: tests/Features/It.php ================================================ 'foo'])->toHaveKey('key')->key->toBeString(); }); it('is a higher order message test')->expect(true)->toBeTrue(); describe('a "describe" group of tests', function () { it('is a test', function () { expect(['key' => 'foo'])->toHaveKey('key')->key->toBeString(); }); it('is a higher order message test')->expect(true)->toBeTrue(); }); ================================================ FILE: tests/Features/Note.php ================================================ note('This is before each runtime note'); })->note('This is before each static note'); it('may have a static note', function () { expect(true)->toBeTrue(); })->note('This is a note'); it('may have a runtime note', function () { expect(true)->toBeTrue(true); $this->note('This is a runtime note'); }); it('may have static note and runtime note', function () { expect(true)->toBeTrue(true); $this->note('This is a runtime note'); })->note('This is a static note'); describe('nested', function () { beforeEach(function () { $this->note('This is before each describe runtime note'); })->note('This is before each describe static note'); it('may have static note and runtime note', function () { expect(true)->toBeTrue(true); $this->note('This is a runtime note within describe'); })->note('This is a static note within describe'); describe('describe nested within describe', function () { beforeEach(function () { $this->note('This is before each nested describe runtime note'); })->note('This is before each nested describe static note'); it('may have a static note and runtime note', function () { expect(true)->toBeTrue(true); $this->note('This is a runtime note within a nested describe'); })->note('This is a static note within a nested describe'); })->note('This is a nested describe static note'); })->note('This is describe static note'); describe('matching describe names', function () { beforeEach(function () { $this->note('This is before each matching describe runtime note'); })->note('This is before each matching describe static note'); describe('describe block', function () { beforeEach(function () { $this->note('This is before each matching describe runtime note'); })->note('This is before each matching describe static note'); it('may have a static note and runtime note', function () { expect(true)->toBeTrue(true); $this->note('This is a runtime note within a matching describe'); })->note('This is a static note within a matching describe'); })->note('This is a nested matching static note'); describe('describe block', function () { beforeEach(function () { $this->note('This is before each matching describe runtime note, and should not contain the matching describe notes'); })->note('This is before each matching describe static note, and should not contain the matching describe notes'); it('may have a static note and runtime note, that are different than the matching describe block', function () { expect(true)->toBeTrue(true); $this->note('This is a runtime note within a matching describe, and should not contain the matching describe notes'); })->note('This is a static note within a matching describe, and should not contain the matching describe notes'); })->note('This is a nested matching static note, and should not contain the matching describe notes'); }); test('multiple notes', function () { expect(true)->toBeTrue(true); $this->note([ 'This is a runtime note', 'This is another runtime note', ]); }); ================================================ FILE: tests/Features/Notices.php ================================================ toBeTrue(); }); describe('a "describe" group of tests', function () { test('notice', function () { trigger_error('This is a notice description', E_USER_NOTICE); expect(true)->toBeTrue(); }); }); ================================================ FILE: tests/Features/Pr.php ================================================ toBeTrue(); })->pr(1); it('may be associated with an pr', function () { expect(true)->toBeTrue(); })->pr(2); describe('nested', function () { it('may be associated with an pr', function () { expect(true)->toBeTrue(); })->pr('#3'); })->pr(4)->note('an note between an the pr')->pr(['#5', 6]); ================================================ FILE: tests/Features/References.php ================================================ toBeString(); })->references(Panic::class); it('can reference a specific class method', function () { expect(Panic::with(...))->toBeCallable(); })->references([Panic::class, 'with']); ================================================ FILE: tests/Features/Repeat.php ================================================ toBeTrue(); })->repeat(times: 1); test('multiple times', function () { expect(true)->toBeTrue(); })->repeat(times: 5); test('multiple times with single dataset', function (int $number) { expect([1, 2, 3])->toContain($number); })->repeat(times: 6)->with(['a' => 1, 'b' => 2, 'c' => 3]); test('multiple times with multiple dataset', function (int $numberA, int $numberB) { expect([1, 2, 3])->toContain($numberA) ->and([4, 5, 6])->toContain($numberB); })->repeat(times: 7)->with(['a' => 1, 'b' => 2, 'c' => 3], [4, 5, 6]); test('multiple times with iterator', function (int $iteration) { expect($iteration) ->toBeNumeric() ->toBeGreaterThan(0); })->repeat(times: 2); test('multiple times with repeat iterator with single dataset', function (string $letter, int $iteration) { expect($letter) ->toBeString() ->toBeIn(['a', 'b', 'c']) ->and($iteration) ->toBeNumeric() ->toBeGreaterThan(0); })->repeat(times: 2)->with(['a', 'b', 'c']); test('multiple times with repeat iterator with multiple dataset', function (string $letterA, string $letterB, int $iteration) { expect($letterA) ->toBeString() ->toBeIn(['a', 'b', 'c']) ->and($letterB) ->toBeString() ->toBeIn(['d', 'e', 'f']) ->and($iteration) ->toBeNumeric() ->toBeGreaterThan(0); })->repeat(times: 2)->with(['a', 'b', 'c'], ['d', 'e', 'f']); describe('describe blocks', function () { test('multiple times', function () { expect(true)->toBeTrue(); })->repeat(times: 3); describe('describe with repeat', function () { test('test with no repeat should repeat the number of times specified in the parent describe block', function () { expect(true)->toBeTrue(); }); test('test with repeat should repeat the number of times specified in the test', function () { expect(true)->toBeTrue(); })->repeat(times: 2); describe('nested describe without repeat', function () { test("test with no repeat should repeat the number of times specified in the parent's parent describe block", function () { expect(true)->toBeTrue(); }); test('test with repeat should repeat the number of times specified in the test', function () { expect(true)->toBeTrue(); })->repeat(times: 2); }); describe('nested describe with repeat', function () { test('test with no repeat should repeat the number of times specified in the parent describe block', function () { expect(true)->toBeTrue(); }); test('test with repeat should repeat the number of times specified in the test', function () { expect(true)->toBeTrue(); })->repeat(times: 2); })->repeat(times: 2); })->repeat(times: 3); }); describe('matching describe blocks', function () { describe('describe block', function () { it('should repeat the number of times specified in the parent describe block', function () { expect(true)->toBeTrue(); }); })->repeat(times: 3); describe('describe block', function () { test('should not repeat the number of times of the describe block with the same name', function () { expect(true)->toBeTrue(); }); }); }); ================================================ FILE: tests/Features/ScopedDatasets/Directory/Datasets/Scoped.php ================================================ text = ''; test('uses dataset', function ($value) use ($state) { $state->text .= $value; expect(true)->toBe(true); })->with('numbers.array'); test('the right dataset is taken', function () use ($state) { expect($state->text)->toBe('12345ScopedDatasets/NestedDirectory1/Datasets.php'); }); ================================================ FILE: tests/Features/ScopedDatasets/Directory/NestedDirectory2/TestFileInNestedDirectory.php ================================================ text = ''; test('uses dataset', function ($value) use ($state) { $state->text .= $value; expect(true)->toBe(true); })->with('numbers.array'); test('the right dataset is taken', function () use ($state) { expect($state->text)->toBe('12345ScopedDatasets/Datasets/Scoped.php'); }); ================================================ FILE: tests/Features/ScopedDatasets/Directory/TestFileWithLocallyDefinedDataset.php ================================================ text = ''; test('uses dataset', function ($value) use ($state) { $state->text .= $value; expect(true)->toBe(true); })->with('numbers.array'); test('the right dataset is taken', function () use ($state) { expect($state->text)->toBe('12345ScopedDatasets/ScopedDatasets.php'); }); ================================================ FILE: tests/Features/ScopedDatasets/Directory/TestFileWithScopedDataset.php ================================================ text = ''; test('uses dataset', function ($value) use ($state) { $state->text .= $value; expect(true)->toBe(true); })->with('numbers.array'); test('the right dataset is taken', function () use ($state) { expect($state->text)->toBe('12345ScopedDatasets/Datasets/Scoped.php'); }); ================================================ FILE: tests/Features/ScopedDatasets/TestFileOutOfScope.php ================================================ text = ''; test('uses dataset', function ($value) use ($state) { $state->text .= $value; expect(true)->toBe(true); })->with('numbers.array'); test('the right dataset is taken', function () use ($state) { expect($state->text)->toBe('12'); }); it('can see datasets defined in Pest.php file', function (string $value) use ($state) { $state->text .= $value; expect(true)->toBe(true); })->with('dataset_in_pest_file'); test('Pest.php dataset is taken', function () use ($state) { expect($state->text)->toBe('12AB'); }); ================================================ FILE: tests/Features/See.php ================================================ toBeString(); })->see(Panic::class); it('can reference a specific class method', function () { expect(Panic::with(...))->toBeCallable(); })->see([Panic::class, 'with']); ================================================ FILE: tests/Features/Skip.php ================================================ shouldSkip = true; }); it('do not skips') ->skip(false) ->assertTrue(true); it('skips with truthy') ->skip(1) ->assertTrue(false); it('skips with truthy condition by default') ->skip() ->assertTrue(false); it('skips with message') ->skip('skipped because bar') ->assertTrue(false); it('skips with truthy closure condition') ->skip(function () { return '1'; }) ->assertTrue(false); it('do not skips with falsy closure condition') ->skip(function () { return false; }) ->assertTrue(true); it('skips with condition and message') ->skip(true, 'skipped because foo') ->assertTrue(false); it('skips when skip after assertion') ->assertTrue(true) ->skip(); it('can use something in the test case as a condition') ->skip(function () { return $this->shouldSkip; }, 'This test was skipped') ->assertTrue(false); it('can user higher order callables and skip') ->skip(function () { return $this->shouldSkip; }) ->expect(function () { return $this->shouldSkip; }) ->toBeFalse(); describe('skip on describe', function () { beforeEach(function () { $this->ran = false; }); afterEach(function () { match ($this->name()) { '__pest_evaluable__skip_on_describe__→__skipped_tests__→__nested_inside_skipped_block__→_it_should_not_execute' => expect($this->ran)->toBe(false), '__pest_evaluable__skip_on_describe__→__skipped_tests__→_it_should_not_execute' => expect($this->ran)->toBe(false), '__pest_evaluable__skip_on_describe__→_it_should_execute' => expect($this->ran)->toBe(true), default => $this->fail('Unexpected test name: '.$this->name()), }; }); describe('skipped tests', function () { describe('nested inside skipped block', function () { it('should not execute', function () { $this->ran = true; $this->fail(); }); }); it('should not execute', function () { $this->ran = true; $this->fail(); }); })->skip(); it('should execute', function () { $this->ran = true; expect($this->ran)->toBe(true); }); }); describe('skip on beforeEach', function () { beforeEach(function () { $this->ran = false; }); afterEach(function () { match ($this->name()) { '__pest_evaluable__skip_on_beforeEach__→__skipped_tests__→__nested_inside_skipped_block__→_it_should_not_execute' => expect($this->ran)->toBe(false), '__pest_evaluable__skip_on_beforeEach__→__skipped_tests__→_it_should_not_execute' => expect($this->ran)->toBe(false), '__pest_evaluable__skip_on_beforeEach__→_it_should_execute' => expect($this->ran)->toBe(true), default => $this->fail('Unexpected test name: '.$this->name()), }; }); describe('skipped tests', function () { beforeEach()->skip(); describe('nested inside skipped block', function () { it('should not execute', function () { $this->ran = true; $this->fail(); }); }); it('should not execute', function () { $this->ran = true; $this->fail(); }); }); it('should execute', function () { $this->ran = true; expect($this->ran)->toBe(true); }); }); describe('matching describe with skip', function () { beforeEach(function () { $this->ran = false; }); afterEach(function () { match ($this->name()) { '__pest_evaluable__matching_describe_with_skip__→__describe_block__→_it_should_not_execute' => expect($this->ran)->toBe(false), '__pest_evaluable__matching_describe_with_skip__→__describe_block__→_it_should_execute_a_test_in_a_describe_block_with_the_same_name_as_a_skipped_describe_block' => expect($this->ran)->toBe(true), '__pest_evaluable__matching_describe_with_skip__→_it_should_execute' => expect($this->ran)->toBe(true), default => $this->fail('Unexpected test name: '.$this->name()), }; }); describe('describe block', function () { it('should not execute', function () { $this->ran = true; $this->fail(); }); })->skip(); describe('describe block', function () { it('should execute a test in a describe block with the same name as a skipped describe block', function () { $this->ran = true; }); }); it('should execute', function () { $this->ran = true; expect($this->ran)->toBe(true); }); }); describe('matching describe with skip on beforeEach', function () { beforeEach(function () { $this->ran = false; }); afterEach(function () { match ($this->name()) { '__pest_evaluable__matching_describe_with_skip_on_beforeEach__→__describe_block__→_it_should_not_execute' => expect($this->ran)->toBe(false), '__pest_evaluable__matching_describe_with_skip_on_beforeEach__→__describe_block__→_it_should_execute_a_test_in_a_describe_block_with_the_same_name_as_a_skipped_describe_block' => expect($this->ran)->toBe(true), '__pest_evaluable__matching_describe_with_skip_on_beforeEach__→_it_should_execute' => expect($this->ran)->toBe(true), default => $this->fail('Unexpected test name: '.$this->name()), }; }); describe('describe block', function () { beforeEach()->skip(); it('should not execute', function () { $this->ran = true; $this->fail(); }); }); describe('describe block', function () { it('should execute a test in a describe block with the same name as a skipped describe block', function () { $this->ran = true; }); }); it('should execute', function () { $this->ran = true; expect($this->ran)->toBe(true); }); }); it('does not skip after the describe block', function () { expect(true)->toBeTrue(); }); it('can skip after the describe block', function () { expect(true)->toBeTrue(); })->skip(); ================================================ FILE: tests/Features/SkipOnPhp.php ================================================ skipOnPhp('<=7.4.0') ->assertTrue(true); it('can run on specific php version') ->skipOnPhp('7.4.0') ->assertTrue(true); it('can skip on php versions depending on constraint') ->skipOnPhp('>=7.4.0') ->assertTrue(false); ================================================ FILE: tests/Features/Test.php ================================================ assertArrayHasKey('key', ['key' => 'foo']); }); test('higher order message test')->expect(true)->toBeTrue(); ================================================ FILE: tests/Features/TestCycle.php ================================================ beforeAll = false; $foo->beforeEach = false; $foo->afterEach = false; $foo->afterAll = false; beforeAll(function () use ($foo) { $foo->beforeAll = true; }); beforeEach(function () use ($foo) { $foo->beforeEach = true; }); afterEach(function () use ($foo) { $foo->afterEach = true; }); afterAll(function () use ($foo) { $foo->afterAll = true; }); register_shutdown_function(function () use ($foo) { assertFalse($foo->beforeAll); assertFalse($foo->beforeEach); assertFalse($foo->afterEach); assertFalse($foo->afterAll); }); ================================================ FILE: tests/Features/ThrowsNoExceptions.php ================================================ expectNotToPerformAssertions(); $result = 1 + 1; }); it('allows performing no expectations without being risky', function () { $result = 1 + 1; })->throwsNoExceptions(); describe('a "describe" group of tests', function () { it('allows performing no expectations without being risky', function () { $result = 1 + 1; }); })->throwsNoExceptions(); ================================================ FILE: tests/Features/Ticket.php ================================================ toBeTrue(); })->ticket(1); it('may be associated with an ticket', function () { expect(true)->toBeTrue(); })->ticket(2); describe('nested', function () { it('may be associated with an ticket', function () { expect(true)->toBeTrue(); })->ticket(3); })->ticket(4)->note('an note between an the ticket')->ticket([5, 6]); ================================================ FILE: tests/Features/Todo.php ================================================ todo(); test('something todo later chained and with function body', function () { expect(true)->toBeFalse(); })->todo(); it('does something within a file with a todo', function () { expect(true)->toBeTrue(); }); it('may have an associated assignee', function () { expect(true)->toBeTrue(); })->todo(assignee: 'nunomaduro'); it('may have an associated issue', function () { expect(true)->toBeTrue(); })->todo(issue: 1); it('may have an associated PR', function () { expect(true)->toBeTrue(); })->todo(pr: 1); it('may have an associated note', function () { expect(true)->toBeTrue(); })->todo(note: 'a note'); describe('todo on describe', function () { beforeEach(function () { $this->ran = false; }); afterEach(function () { match ($this->name()) { '__pest_evaluable__todo_on_describe__→__todo_block__→__nested_inside_todo_block__→_it_should_not_execute' => expect($this->ran)->toBe(false), '__pest_evaluable__todo_on_describe__→__todo_block__→__nested_inside_todo_block__→_it_should_set_the_note' => expect($this->ran)->toBe(false), '__pest_evaluable__todo_on_describe__→__todo_block__→__describe_with_note__→_it_should_apply_the_note_to_a_test_without_a_todo' => expect($this->ran)->toBe(false), '__pest_evaluable__todo_on_describe__→__todo_block__→__describe_with_note__→_it_should_apply_the_note_to_a_test_with_a_todo' => expect($this->ran)->toBe(false), '__pest_evaluable__todo_on_describe__→__todo_block__→__describe_with_note__→_it_should_apply_the_note_as_well_as_the_note_from_the_test' => expect($this->ran)->toBe(false), '__pest_evaluable__todo_on_describe__→__todo_block__→__describe_with_note__→__nested_describe_with_note__→_it_should_apply_all_parent_notes_to_a_test_without_a_todo' => expect($this->ran)->toBe(false), '__pest_evaluable__todo_on_describe__→__todo_block__→__describe_with_note__→__nested_describe_with_note__→_it_should_apply_all_parent_notes_to_a_test_with_a_todo' => expect($this->ran)->toBe(false), '__pest_evaluable__todo_on_describe__→__todo_block__→__describe_with_note__→__nested_describe_with_note__→_it_should_apply_all_parent_notes_as_well_as_the_note_from_the_test' => expect($this->ran)->toBe(false), '__pest_evaluable__todo_on_describe__→__todo_block__→_it_should_not_execute' => expect($this->ran)->toBe(false), '__pest_evaluable__todo_on_describe__→_it_should_execute' => expect($this->ran)->toBe(true), default => $this->fail('Unexpected test name: '.$this->name()), }; }); describe('todo block', function () { describe('nested inside todo block', function () { it('should not execute', function () { $this->ran = true; $this->fail(); }); it('should set the note', function () { $this->ran = true; $this->fail(); })->todo(note: 'hi'); }); describe('describe with note', function () { it('should apply the note to a test without a todo', function () { $this->ran = true; $this->fail(); }); it('should apply the note to a test with a todo', function () { $this->ran = true; $this->fail(); })->todo(); it('should apply the note as well as the note from the test', function () { $this->ran = true; $this->fail(); })->todo(note: 'test note'); describe('nested describe with note', function () { it('should apply all parent notes to a test without a todo', function () { $this->ran = true; $this->fail(); }); it('should apply all parent notes to a test with a todo', function () { $this->ran = true; $this->fail(); })->todo(); it('should apply all parent notes as well as the note from the test', function () { $this->ran = true; $this->fail(); })->todo(note: 'test note'); })->todo(note: 'nested describe note'); })->todo(note: 'describe note'); it('should not execute', function () { $this->ran = true; $this->fail(); }); })->todo(); it('should execute', function () { $this->ran = true; expect($this->ran)->toBe(true); }); }); describe('todo on describe with matching name', function () { beforeEach(function () { $this->ran = false; }); afterEach(function () { match ($this->name()) { '__pest_evaluable__todo_on_describe_with_matching_name__→__describe_block__→_it_should_not_execute' => expect($this->ran)->toBe(false), '__pest_evaluable__todo_on_describe_with_matching_name__→__describe_block__→_it_should_execute_a_test_in_a_describe_block_with_the_same_name_as_a_todo_describe_block' => expect($this->ran)->toBe(true), '__pest_evaluable__todo_on_describe_with_matching_name__→_it_should_execute' => expect($this->ran)->toBe(true), default => $this->fail('Unexpected test name: '.$this->name()), }; }); describe('describe block', function () { it('should not execute', function () { $this->ran = true; $this->fail(); }); })->todo(); describe('describe block', function () { it('should execute a test in a describe block with the same name as a todo describe block', function () { $this->ran = true; }); }); it('should execute', function () { $this->ran = true; expect($this->ran)->toBe(true); }); }); test('todo on test after describe block', function () { $this->fail(); })->todo(); test('todo with note on test after describe block', function () { $this->fail(); })->todo(note: 'test note'); describe('todo on beforeEach', function () { beforeEach(function () { $this->ran = false; }); afterEach(function () { match ($this->name()) { '__pest_evaluable__todo_on_beforeEach__→__todo_block__→__nested_inside_todo_block__→_it_should_not_execute' => expect($this->ran)->toBe(false), '__pest_evaluable__todo_on_beforeEach__→__todo_block__→_it_should_not_execute' => expect($this->ran)->toBe(false), '__pest_evaluable__todo_on_beforeEach__→__todo_block__→__describe_with_note__→_it_should_apply_the_note_to_a_test_without_a_todo' => expect($this->ran)->toBe(false), '__pest_evaluable__todo_on_beforeEach__→__todo_block__→__describe_with_note__→_it_should_apply_the_note_to_a_test_with_a_todo' => expect($this->ran)->toBe(false), '__pest_evaluable__todo_on_beforeEach__→__todo_block__→__describe_with_note__→_it_should_apply_the_note_as_well_as_the_note_from_the_test' => expect($this->ran)->toBe(false), '__pest_evaluable__todo_on_beforeEach__→__todo_block__→__describe_with_note__→__nested_describe_with_note__→_it_should_apply_all_parent_notes_to_a_test_without_a_todo' => expect($this->ran)->toBe(false), '__pest_evaluable__todo_on_beforeEach__→__todo_block__→__describe_with_note__→__nested_describe_with_note__→_it_should_apply_all_parent_notes_to_a_test_with_a_todo' => expect($this->ran)->toBe(false), '__pest_evaluable__todo_on_beforeEach__→__todo_block__→__describe_with_note__→__nested_describe_with_note__→_it_should_apply_all_parent_notes_as_well_as_the_note_from_the_test' => expect($this->ran)->toBe(false), '__pest_evaluable__todo_on_beforeEach__→_it_should_execute' => expect($this->ran)->toBe(true), default => $this->fail('Unexpected test name: '.$this->name()), }; }); describe('todo block', function () { beforeEach()->todo(); describe('nested inside todo block', function () { it('should not execute', function () { $this->ran = true; $this->fail(); }); }); describe('describe with note', function () { it('should apply the note to a test without a todo', function () { $this->ran = true; $this->fail(); }); it('should apply the note to a test with a todo', function () { $this->ran = true; $this->fail(); })->todo(); it('should apply the note as well as the note from the test', function () { $this->ran = true; $this->fail(); })->todo(note: 'test note'); describe('nested describe with note', function () { it('should apply all parent notes to a test without a todo', function () { $this->ran = true; $this->fail(); }); it('should apply all parent notes to a test with a todo', function () { $this->ran = true; $this->fail(); })->todo(); it('should apply all parent notes as well as the note from the test', function () { $this->ran = true; $this->fail(); })->todo(note: 'test note'); })->todo(note: 'nested describe note'); })->todo(note: 'describe note'); it('should not execute', function () { $this->ran = true; $this->fail(); }); }); it('should execute', function () { $this->ran = true; expect($this->ran)->toBe(true); }); }); test('todo on test after describe block with beforeEach', function () { $this->fail(); })->todo(); test('todo with note on test after describe block with beforeEach', function () { $this->fail(); })->todo(note: 'test note'); ================================================ FILE: tests/Features/Warnings.php ================================================ fooqwdfwqdfqw; expect(true)->toBeTrue(); }); test('user warning', function () { trigger_error('This is a warning description', E_USER_WARNING); expect(true)->toBeTrue(); }); describe('a "describe" group of tests', function () { test('user warning', function () { trigger_error('This is a warning description', E_USER_WARNING); expect(true)->toBeTrue(); }); }); ================================================ FILE: tests/Features/Wip.php ================================================ toBeTrue(); })->wip(assignee: 'nunomaduro'); it('may have an associated issue', function () { expect(true)->toBeTrue(); })->wip(issue: 1); it('may have an associated PR', function () { expect(true)->toBeTrue(); })->wip(pr: 1); it('may have an associated note', function () { expect(true)->toBeTrue(); })->wip(note: 'a note'); ================================================ FILE: tests/Fixtures/Arch/ToBeIntBackedEnum/HasIntBacking/HasIntBackingEnum.php ================================================ skip(! isset($_SERVER['COLLISION_TEST'])); test('success', function () { expect(true)->toBeTrue(); })->skip(! isset($_SERVER['COLLISION_TEST'])); ================================================ FILE: tests/Fixtures/Covers/CoversClass1.php ================================================ assertTrue(true); ================================================ FILE: tests/Fixtures/ExampleTest.php ================================================ assertTrue(true); ================================================ FILE: tests/Fixtures/Inheritance/Base/ExampleTest.php ================================================ markTestSkipped(); } } ================================================ FILE: tests/Fixtures/Inheritance/ExampleTest.php ================================================ assertTrue(true); } } ================================================ FILE: tests/Fixtures/UnexpectedOutput.php ================================================ toBeTrue(); })->skip(! isset($_SERVER['COLLISION_TEST'])); ================================================ FILE: tests/Fixtures/phpunit-in-isolation.xml ================================================ ================================================ FILE: tests/Fixtures/phpunit-not-in-isolation.xml ================================================ ================================================ FILE: tests/Helpers/Helper.php ================================================ assertTrue($value); } ================================================ FILE: tests/Helpers/TestInHelpers.php ================================================ toBeTrue(); }); ================================================ FILE: tests/Helpers.php ================================================ assertTrue($value); return test(); } ================================================ FILE: tests/Hooks/AfterEachTest.php ================================================ ith = 0; }); pest()->afterEach(function () { expect($this) ->toHaveProperty('ith') ->and($this->ith) ->toBe(3); $this->ith++; }); pest()->afterEach(function () { expect($this) ->toHaveProperty('ith') ->and($this->ith) ->toBe(4); $this->ith++; }); afterEach(function () { expect($this) ->toHaveProperty('ith') ->and($this->ith) ->toBe(5); $this->ith++; }); describe('nested', function () { afterEach(function () { expect($this) ->toHaveProperty('ith') ->and($this->ith) ->toBe(6); $this->ith++; }); test('nested afterEach execution order', function () { expect($this) ->toHaveProperty('ith') ->and($this->ith) ->toBe(0); $this->ith++; }); afterEach(function () { expect($this) ->toHaveProperty('ith') ->and($this->ith) ->toBe(7); $this->ith++; }); }); afterEach(function () { expect($this) ->toHaveProperty('ith') ->and($this->ith) ->toBeBetween(6, 8); $this->ith++; }); test('global afterEach execution order', function () { expect($this) ->toHaveProperty('ith') ->and($this->ith) ->toBe(0); $this->ith++; }); ================================================ FILE: tests/Hooks/BeforeAllTest.php ================================================ beforeAll(function () { expect($_SERVER['globalHook']->calls->beforeAll) ->toBe(0); $_SERVER['globalHook']->calls->beforeAll++; }); it('gets called before all tests 1', function () { expect($_SERVER['globalHook']->calls->beforeAll)->toBe(1); })->repeat(2); it('gets called before all tests 2', function () { expect($_SERVER['globalHook']->calls->beforeAll)->toBe(1); }); ================================================ FILE: tests/Hooks/BeforeEachTest.php ================================================ beforeEach(function () { expect($this) ->toHaveProperty('baz') ->and($this->baz) ->toBe(1); $this->baz = 2; }); beforeEach(function () { expect($this) ->toHaveProperty('baz') ->and($this->baz) ->toBe(2); $this->baz = 3; }); test('global beforeEach execution order', function () { expect($this) ->toHaveProperty('baz') ->and($this->baz) ->toBe(3); }); ================================================ FILE: tests/PHPUnit/CustomAffixes/@#$%^&()-_=+.php ================================================ assertTrue(true); ================================================ FILE: tests/PHPUnit/CustomAffixes/A Test With Spaces.php ================================================ assertTrue(true); ================================================ FILE: tests/PHPUnit/CustomAffixes/AdditionalFileExtension.spec.php ================================================ assertTrue(true); ================================================ FILE: tests/PHPUnit/CustomAffixes/FolderWithAn@/ExampleTest.php ================================================ assertTrue(true); } } pest()->extend(MyCustomClassTest::class); test('custom traits can be used', function () { $this->assertTrueIsTrue(); }); test('trait applied in this file')->assertTrueIsTrue(); ================================================ FILE: tests/PHPUnit/CustomAffixes/ManyExtensions.class.test.php ================================================ assertTrue(true); ================================================ FILE: tests/PHPUnit/CustomAffixes/Test 'Case' With Quotes.php ================================================ assertTrue(true); ================================================ FILE: tests/PHPUnit/CustomAffixes/kebab-case-spec.php ================================================ assertTrue(true); ================================================ FILE: tests/PHPUnit/CustomAffixes/snake_case_spec.php ================================================ assertTrue(true); ================================================ FILE: tests/PHPUnit/CustomTestCase/ChildTest.php ================================================ assertTrue(ExecutedTest::$executed)); ================================================ FILE: tests/PHPUnit/CustomTestCase/ParentTest.php ================================================ getEntity() || true); } } ================================================ FILE: tests/PHPUnit/CustomTestCase/UsesPerDirectory.php ================================================ use(CustomTestCase::class)->in(__DIR__); test('closure was bound to CustomTestCase', function () { $this->assertCustomTrue(); }); ================================================ FILE: tests/PHPUnit/CustomTestCaseInSubFolders/SubFolder/SubFolder/CustomTestCaseInSubFolder.php ================================================ assertTrue(true); } } ================================================ FILE: tests/PHPUnit/CustomTestCaseInSubFolders/SubFolder/SubFolder/UsesPerSubDirectory.php ================================================ assertCustomInSubFolderTrue(); }); ================================================ FILE: tests/PHPUnit/CustomTestCaseInSubFolders/SubFolder2/UsesPerFile.php ================================================ assertTrue(true); } } pest()->extend(MyCustomClass::class)->use(MyCustomTrait::class); test('custom traits can be used', function () { $this->assertTrueIsTrue(); }); test('trait applied in this file')->assertTrueIsTrue(); ================================================ FILE: tests/PHPUnit/GlobPatternTests/SubFolder/InnerFolder/UsesPerDirectoryAsPattern.php ================================================ assertCustomTrue(); }); ================================================ FILE: tests/PHPUnit/GlobPatternTests/SubFolder2/UsesPerFileAsPattern.php ================================================ assertCustomTrue(); }); ================================================ FILE: tests/PHPUnit/IgnorableTest.php ================================================ project()->github('pestphp/pest'); pest()->in('PHPUnit/CustomTestCaseInSubFolders/SubFolder/SubFolder')->use(CustomTestCaseInSubFolder::class); // test case for all the directories inside PHPUnit/GlobPatternTests/SubFolder/ pest()->in('PHPUnit/GlobPatternTests/SubFolder/*')->extend(CustomTestCase::class); // test case for all the files that end with AsPattern.php inside PHPUnit/GlobPatternTests/SubFolder2/ pest()->in('PHPUnit/GlobPatternTests/SubFolder2/*AsPattern.php')->use(CustomTestCase::class); pest()->in('Visual')->group('integration'); // NOTE: global test value container to be mutated and checked across files, as needed $_SERVER['globalHook'] = (object) ['calls' => (object) ['beforeAll' => 0, 'afterAll' => 0]]; pest() ->in('Hooks') ->beforeEach(function () { $this->baz = 0; }) ->beforeAll(function () { $_SERVER['globalHook']->beforeAll = 0; }) ->afterEach(function () { if (! isset($this->ith)) { return; } assert($this->ith === 1, 'Expected $this->ith to be 1, but got '.$this->ith); $this->ith++; }) ->afterAll(function () { $_SERVER['globalHook']->afterAll = 0; $_SERVER['globalHook']->calls->afterAll++; }); pest()->in('Hooks') ->beforeEach(function () { expect($this) ->toHaveProperty('baz') ->and($this->baz) ->toBe(0); $this->baz = 1; }) ->beforeAll(function () { expect($_SERVER['globalHook']) ->toHaveProperty('beforeAll') ->and($_SERVER['globalHook']->beforeAll) ->toBe(0); $_SERVER['globalHook']->beforeAll = 1; }) ->afterEach(function () { if (! isset($this->ith)) { return; } assert($this->ith === 2, 'Expected $this->ith to be 1, but got '.$this->ith); $this->ith++; }) ->afterAll(function () { expect($_SERVER['globalHook']) ->toHaveProperty('afterAll') ->and($_SERVER['globalHook']->afterAll) ->toBe(0); $_SERVER['globalHook']->afterAll = 1; }); function helper_returns_string() { return 'string'; } dataset('dataset_in_pest_file', ['A', 'B']); function removeAnsiEscapeSequences(string $input): ?string { return preg_replace('#\\x1b[[][^A-Za-z]*[A-Za-z]#', '', $input); } ================================================ FILE: tests/Playground.php ================================================ toBeTrue(); }); ================================================ FILE: tests/Plugins/Coverage.php ================================================ $this->computeComparableCoverage($givenValue))->call($plugin); expect($comparableCoverage)->toBe($expectedValue); })->with([ [0, 0], [0.5, 0.5], [1.0, 1.0], [32.51, 32.5], [32.12312321312312, 32.1], [32.53333333333333, 32.5], [32.57777771232132, 32.5], [100.0, 100.0], ]); ================================================ FILE: tests/Plugins/Traits.php ================================================ assertPluginTraitGotRegistered(); it('allows multiple global uses registered in the same path')->assertSecondPluginTraitGotRegistered(); ================================================ FILE: tests/Unit/Configuration/In.php ================================================ in(); expect($in)->toBeInstanceOf(UsesCall::class); }); ================================================ FILE: tests/Unit/Configuration/Theme.php ================================================ printer(); expect($theme)->toBeInstanceOf(Printer::class); }); ================================================ FILE: tests/Unit/Console/Help.php ================================================ fetch())->toContain('Pest Options:'); }); ================================================ FILE: tests/Unit/DatasetsTests.php ================================================ [1], 'two' => [[2]], ], ], __FILE__)); expect($descriptions[0])->toBe('dataset "one"') ->and($descriptions[1])->toBe('dataset "two"'); }); it('show the actual dataset of non-named datasets in their description', function () { $descriptions = array_keys(DatasetsRepository::resolve([ [ [1], [[2]], ], ], __FILE__)); expect($descriptions[0])->toBe('(1)'); expect($descriptions[1])->toBe('([2])'); }); it('show only the names of multiple named datasets in their description', function () { $descriptions = array_keys(DatasetsRepository::resolve([ [ 'one' => [1], 'two' => [[2]], ], [ 'three' => [3], 'four' => [[4]], ], ], __FILE__)); expect($descriptions[0])->toBe('dataset "one" / dataset "three"'); expect($descriptions[1])->toBe('dataset "one" / dataset "four"'); expect($descriptions[2])->toBe('dataset "two" / dataset "three"'); expect($descriptions[3])->toBe('dataset "two" / dataset "four"'); }); it('show the actual dataset of multiple non-named datasets in their description', function () { $descriptions = array_keys(DatasetsRepository::resolve([ [ [1], [[2]], ], [ [3], [[4]], ], ], __FILE__)); expect($descriptions[0])->toBe('(1) / (3)'); expect($descriptions[1])->toBe('(1) / ([4])'); expect($descriptions[2])->toBe('([2]) / (3)'); expect($descriptions[3])->toBe('([2]) / ([4])'); }); it('show the correct description for mixed named and not-named datasets', function () { $descriptions = array_keys(DatasetsRepository::resolve([ [ 'one' => [1], [[2]], ], [ [3], 'four' => [[4]], ], ], __FILE__)); expect($descriptions[0])->toBe('dataset "one" / (3)'); expect($descriptions[1])->toBe('dataset "one" / dataset "four"'); expect($descriptions[2])->toBe('([2]) / (3)'); expect($descriptions[3])->toBe('([2]) / dataset "four"'); }); it('shows the correct description for long texts with newlines', function () { $descriptions = array_keys(DatasetsRepository::resolve([ [ ['some very \nlong text with \n newlines'], ], ], __FILE__)); expect($descriptions[0])->toBe('(\'some very long text with …wlines\')'); }); it('shows the correct description for arrays with many elements', function () { $descriptions = array_keys(DatasetsRepository::resolve([ [ [[1, 2, 3, 4, 5]], ], ], __FILE__)); expect($descriptions[0])->toBe('([1, 2, 3, …])'); }); it('shows the correct description of datasets with html', function () { $descriptions = array_keys(DatasetsRepository::resolve([ [ '
', ], ], __FILE__)); expect($descriptions[0])->toBe('(\'
\')'); }); ================================================ FILE: tests/Unit/Expectations/OppositeExpectation.php ================================================ throwExpectationFailedException('toBe', 'bar'); })->throws(ExpectationFailedException::class, "Expecting 'foo' not to be 'bar'."); it('throw expectation failed exception with array argument', function (): void { $expectation = new OppositeExpectation(expect('foo')); $expectation->throwExpectationFailedException('toBe', ['bar']); })->throws(ExpectationFailedException::class, "Expecting 'foo' not to be 'bar'."); ================================================ FILE: tests/Unit/Overrides/ThrowableBuilder.php ================================================ __DIR__.'/../../Pest.php', 'line' => 5, ]); } }; expect(str_replace(DIRECTORY_SEPARATOR, '/', ThrowableBuilder::from($exception)->stackTrace())) ->toContain('tests/Unit/Overrides/../../Pest.php:5') ->and(str_replace(DIRECTORY_SEPARATOR, '/', ThrowableBuilder::from(new Exception('test'))->stackTrace())) ->toContain('tests/Unit/Overrides/ThrowableBuilder.php:26'); }); ================================================ FILE: tests/Unit/Plugins/Concerns/HandleArguments.php ================================================ hasArgument($argument, $arguments))->toBe($expectedResult); })->with([ ['--long-argument', true], ['-a', true], ['--with-equal-sign', true], ['someValue', true], ['--a', false], ['--undefined-argument', false], ]); ================================================ FILE: tests/Unit/Plugins/Environment.php ================================================ handleArguments(['foo', '--ci', 'bar']); expect(Environment::name())->toBe(Environment::CI); Environment::name($previousName); }); test('environment is set to Local when --ci option is not used', function () { $plugin = new Environment; $plugin->handleArguments(['foo', 'bar', 'baz']); expect(Environment::name())->toBe(Environment::LOCAL); }); ================================================ FILE: tests/Unit/Plugins/Retry.php ================================================ handleArguments(['--retry']); expect($arguments)->toBe([ '--order-by=defects', '--stop-on-failure', ]); }); ================================================ FILE: tests/Unit/Preset.php ================================================ presets()->custom('myFramework', function (array $userNamespaces) { return [ expect($userNamespaces)->toBe(['Pest']), ]; }); test('preset invalid name', function () { $this->preset()->myAnotherFramework(); })->throws(InvalidArgumentException::class, 'The preset [myAnotherFramework] does not exist. The available presets are [php, laravel, strict, security, relaxed, myFramework].'); arch()->preset()->myFramework(); ================================================ FILE: tests/Unit/Support/Arr.php ================================================ toBeFalse(); }); it('should return the last element for an array with a single element', function () { expect(Arr::last([1]))->toBe(1); }); it('should return the last element for an array without changing the internal pointer', function () { $array = [1, 2, 3]; expect(Arr::last($array))->toBe(3); expect(current($array))->toBe(1); next($array); expect(current($array))->toBe(2); expect(Arr::last($array))->toBe(3); expect(current($array))->toBe(2); }); it('should return the last element for an associative array without changing the internal pointer', function () { $array = ['first' => 1, 'second' => 2, 'third' => 3]; expect(Arr::last($array))->toBe(3); expect(current($array))->toBe(1); next($array); expect(current($array))->toBe(2); expect(Arr::last($array))->toBe(3); expect(current($array))->toBe(2); }); it('should return the last element for an mixed key array without changing the internal pointer', function () { $array = ['first' => 1, 2, 'third' => 3]; expect(Arr::last($array))->toBe(3); expect(current($array))->toBe(1); next($array); expect(current($array))->toBe(2); expect(Arr::last($array))->toBe(3); expect(current($array))->toBe(2); }); }); ================================================ FILE: tests/Unit/Support/Backtrace.php ================================================ toBe(__FILE__); }); ================================================ FILE: tests/Unit/Support/Container.php ================================================ group('container'); beforeEach(function () { $this->container = new Container; }); it('exists') ->assertTrue(class_exists(Container::class)); it('gets an instance', function () { $this->container->add(Container::class, $this->container); expect($this->container->get(Container::class))->toBe($this->container); }); test('autowire', function () { expect($this->container->get(Container::class))->toBeInstanceOf(Container::class); }); it('creates an instance and resolves parameters', function () { $this->container->add(Container::class, $this->container); $instance = $this->container->get(ClassWithDependency::class); expect($instance)->toBeInstanceOf(ClassWithDependency::class); }); it('creates an instance and resolves also sub parameters', function () { $this->container->add(Container::class, $this->container); $instance = $this->container->get(ClassWithSubDependency::class); expect($instance)->toBeInstanceOf(ClassWithSubDependency::class); }); it('can resolve builtin value types', function () { $this->container->add('rootPath', getcwd()); $this->container->add('testPath', 'tests'); $instance = $this->container->get(TestSuite::class); expect($instance)->toBeInstanceOf(TestSuite::class); }); it('cannot resolve a parameter without type', function () { $this->container->get(ClassWithoutTypeParameter::class); })->throws(ShouldNotHappen::class); class ClassWithDependency { public function __construct(Container $container) {} } class ClassWithSubDependency { public function __construct(ClassWithDependency $param) {} } class ClassWithoutTypeParameter { public function __construct($param) {} } ================================================ FILE: tests/Unit/Support/DatasetInfo.php ================================================ toBe($inside); })->with([ ['file' => '/var/www/project/tests/Datasets/Numbers.php', 'inside' => true], ['file' => '/var/www/project/tests/Datasets.php', 'inside' => false], ['file' => '/var/www/project/tests/Features/Datasets/Numbers.php', 'inside' => true], ['file' => '/var/www/project/tests/Features/Numbers.php', 'inside' => false], ['file' => '/var/www/project/tests/Features/Datasets.php', 'inside' => false], ]); it('can check if dataset is defined inside a Datasets.php file', function (string $file, bool $inside) { expect(DatasetInfo::isADatasetsFile($file))->toBe($inside); })->with([ ['file' => '/var/www/project/tests/Datasets/Numbers.php', 'inside' => false], ['file' => '/var/www/project/tests/Datasets.php', 'inside' => true], ['file' => '/var/www/project/tests/Features/Datasets/Numbers.php', 'inside' => false], ['file' => '/var/www/project/tests/Features/Numbers.php', 'inside' => false], ['file' => '/var/www/project/tests/Features/Datasets.php', 'inside' => true], ]); it('computes the dataset scope', function (string $file, string $scope) { expect(DatasetInfo::scope($file))->toBe($scope); })->with([ ['file' => '/var/www/project/tests/Datasets/Numbers.php', 'scope' => '/var/www/project/tests'], ['file' => '/var/www/project/tests/Datasets.php', 'scope' => '/var/www/project/tests'], ['file' => '/var/www/project/tests/Features/Datasets/Numbers.php', 'scope' => '/var/www/project/tests/Features'], ['file' => '/var/www/project/tests/Features/Numbers.php', 'scope' => '/var/www/project/tests/Features/Numbers.php'], ['file' => '/var/www/project/tests/Features/Datasets.php', 'scope' => '/var/www/project/tests/Features'], ['file' => '/var/www/project/tests/Features/Controllers/Datasets/Numbers.php', 'scope' => '/var/www/project/tests/Features/Controllers'], ['file' => '/var/www/project/tests/Features/Controllers/Numbers.php', 'scope' => '/var/www/project/tests/Features/Controllers/Numbers.php'], ['file' => '/var/www/project/tests/Features/Controllers/Datasets.php', 'scope' => '/var/www/project/tests/Features/Controllers'], ]); ================================================ FILE: tests/Unit/Support/ExceptionTrace.php ================================================ throws( Exception::class, 'Call to undefined method Tests\IntentionallyNotExisting::testBasic().', ); it('ensures the given closures reports the correct class name and suggests the [pest()] function', function () { $this->get(); })->throws( Error::class, 'Call to undefined method Tests\Unit\Support\ExceptionTrace::get(). Did you forget to use the [pest()->extend()] function? Read more at: https://pestphp.com/docs/configuring-tests', ); ================================================ FILE: tests/Unit/Support/HigherOrderMessage.php ================================================ $message->call($this))->toThrow(function (ReflectionException $exception) { expect($exception) ->getMessage()->toBe('Call to undefined method PHPUnit\Framework\TestCase::foqwdqwd()') ->getLine()->toBe(1) ->getFile()->toBe(__FILE__); }); }); ================================================ FILE: tests/Unit/Support/Reflection.php ================================================ toBe(__FILE__); }); it('gets property values', function () { $class = new class { private $foo = 'bar'; }; $value = Reflection::getPropertyValue($class, 'foo'); expect($value)->toBe('bar'); }); class Asd { protected $foo = 'bar'; public function getFoo() { return $this->foo; } } trait Zxc { protected $baz = 'qux'; public function getBaz() { return $this->baz; } } class Qwe extends Asd { use Zxc; protected $bar = 'baz'; public function getBar() { return $this->bar; } } it('gets properties from classes', function () { $reflectionClass = new ReflectionClass(Qwe::class); $properties = Reflection::getPropertiesFromReflectionClass($reflectionClass); $properties = array_map(fn ($property) => $property->getName(), $properties); expect($properties)->toBe([ 'bar', ]); }); it('gets methods from classes', function () { $reflectionClass = new ReflectionClass(Qwe::class); $methods = Reflection::getMethodsFromReflectionClass($reflectionClass); $methods = array_map(fn ($method) => $method->getName(), $methods); expect($methods)->toBe([ 'getBar', ]); }); ================================================ FILE: tests/Unit/Support/Str.php ================================================ toBe($expected); })->with([ ['version()', '__pest_evaluable_version__'], ['version__ ', '__pest_evaluable_version_____'], ['version\\', '__pest_evaluable_version_'], ]); ================================================ FILE: tests/Unit/TestName.php ================================================ toBe($toBePrinted); })->with([ ['P\Tests\BarTest', 'Tests\BarTest'], ['P\Packages\Foo', 'Packages\Foo'], ['P\PPPackages\Foo', 'PPPackages\Foo'], ['PPPackages\Foo', 'PPPackages\Foo'], ['PPPackages\Foo', 'PPPackages\Foo'], ]); $names = [ 'test description' => '__pest_evaluable_test_description', 'test_description' => '__pest_evaluable_test__description', 'ふ+が+' => '__pest_evaluable_ふ_が_', 'ほげ' => '__pest_evaluable_ほげ', '卜竹弓一十山' => '__pest_evaluable_卜竹弓一十山', 'アゴデヸ' => '__pest_evaluable_アゴデヸ', '!p8VrB' => '__pest_evaluable__p8VrB', '&xe6VeKWF#n4' => '__pest_evaluable__amp_xe6VeKWF_n4', '%%HurHUnw7zM!' => '__pest_evaluable___HurHUnw7zM_', 'rundeliekend' => '__pest_evaluable_rundeliekend', 'g%%c!Jt9$fy#Kf' => '__pest_evaluable_g__c_Jt9_fy_Kf', 'NRs*Gz2@hmB$W$BPD%%b2U%3P%z%apnwSX' => '__pest_evaluable_NRs_Gz2_hmB_W_BPD__b2U_3P_z_apnwSX', 'ÀĤ{¼÷' => '__pest_evaluable_ÀĤ_¼÷', 'ìèéàòç' => '__pest_evaluable_ìèéàòç', 'زهراء المعادي' => '__pest_evaluable_زهراء_المعادي', 'الجبيهه' => '__pest_evaluable_الجبيهه', 'الظهران' => '__pest_evaluable_الظهران', 'Каролин' => '__pest_evaluable_Каролин', 'অ্যান্টার্কটিকা' => '__pest_evaluable_অ্যান্টার্কটিকা', 'Frýdek-Místek"' => '__pest_evaluable_Frýdek_Místek_', 'Allingåbro&' => '__pest_evaluable_Allingåbro_amp_', 'Κεντροαφρικανική Δημοκρατία' => '__pest_evaluable_Κεντροαφρικανική_Δημοκρατία', 'آذربایجان غربی' => '__pest_evaluable_آذربایجان_غربی', 'זימבבואה' => '__pest_evaluable_זימבבואה', 'Belišće' => '__pest_evaluable_Belišće', 'Գվատեմալա' => '__pest_evaluable_Գվատեմալա', 'パプアニューギニア' => '__pest_evaluable_パプアニューギニア', '富山県' => '__pest_evaluable_富山県', 'Қарағанды' => '__pest_evaluable_Қарағанды', 'Қостанай' => '__pest_evaluable_Қостанай', '안양시 동안구' => '__pest_evaluable_안양시_동안구', 'Itālija' => '__pest_evaluable_Itālija', 'Honningsvåg' => '__pest_evaluable_Honningsvåg', 'Águeda' => '__pest_evaluable_Águeda', 'Râșcani' => '__pest_evaluable_Râșcani', 'Năsăud' => '__pest_evaluable_Năsăud', 'Орехово-Зуево' => '__pest_evaluable_Орехово_Зуево', 'Čereňany' => '__pest_evaluable_Čereňany', 'Moravče' => '__pest_evaluable_Moravče', 'Šentjernej' => '__pest_evaluable_Šentjernej', 'Врање' => '__pest_evaluable_Врање', 'Крушевац' => '__pest_evaluable_Крушевац', 'Åkersberga' => '__pest_evaluable_Åkersberga', 'บอสเนียและเฮอร์เซโกวีนา' => '__pest_evaluable_บอสเนียและเฮอร์เซโกวีนา', 'Birleşik Arap Emirlikleri' => '__pest_evaluable_Birleşik_Arap_Emirlikleri', 'Німеччина' => '__pest_evaluable_Німеччина', 'Nam Định' => '__pest_evaluable_Nam_Định', '呼和浩特' => '__pest_evaluable_呼和浩特', 'test /** with comment */ should do' => '__pest_evaluable_test_____with_comment____should_do', ]; foreach ($names as $name => $methodName) { test($name) ->expect(fn () => static::getLatestPrintableTestCaseMethodName()) ->toBe($name) ->and(fn () => $this->name()) ->toBe($methodName); } ================================================ FILE: tests/Unit/TestSuite.php ================================================ description = 'bar'; $testSuite->tests->set($method); $testSuite->tests->set($method); })->throws( TestAlreadyExist::class, sprintf('A test with the description `%s` already exists in the filename `%s`.', 'bar', 'foo'), ); it('does not allow static closures', function () { $testSuite = new TestSuite(getcwd(), 'tests'); $method = new TestCaseMethodFactory('foo', static function () {}); $method->description = 'bar'; $testSuite->tests->set($method); })->throws( TestClosureMustNotBeStatic::class, 'Test closure must not be static. Please remove the [static] keyword from the [bar] method in [foo].', ); it('alerts users about tests with arguments but no input', function () { $testSuite = new TestSuite(getcwd(), 'tests'); $method = new TestCaseMethodFactory('foo', function (int $arg) {}); $method->description = 'bar'; $testSuite->tests->set($method); })->throws( DatasetMissing::class, sprintf('A test with the description [%s] has [%d] argument(s) ([%s]) and no dataset(s) provided in [%s]', 'bar', 1, 'int $arg', 'foo'), ); it('can return an array of all test suite filenames', function () { $testSuite = new TestSuite(getcwd(), 'tests'); $method = new TestCaseMethodFactory('a', null); $method->description = 'b'; $testSuite->tests->set($method); $method = new TestCaseMethodFactory('c', null); $method->description = 'd'; $testSuite->tests->set($method); expect($testSuite->tests->getFilenames())->toEqual([ 'a', 'c', ]); }); ================================================ FILE: tests/Visual/BeforeEachTestName.php ================================================ description = $this->__description; $this->latestDescription = self::$__latestDescription; }); test('description', function () { expect($this->description)->toBe('description'); }); test('latest description', function () { expect($this->latestDescription)->toBe('latest description'); }); ================================================ FILE: tests/Visual/Collision.php ================================================ 'DefaultPrinter', 'COLLISION_IGNORE_DURATION' => 'true', 'COLLISION_TEST' => true] )); $process->run(); return removeAnsiEscapeSequences($process->getOutput()); }; $outputContent = explode("\n", $output()); array_pop($outputContent); array_pop($outputContent); array_pop($outputContent); if (in_array('--parallel', $arguments)) { array_pop($outputContent); array_pop($outputContent); } expect(implode("\n", $outputContent))->toMatchSnapshot(); })->with([ [['']], // [['--parallel']], ])->skipOnWindows(); ================================================ FILE: tests/Visual/Help.php ================================================ 'DefaultPrinter', 'COLLISION_IGNORE_DURATION' => 'true'])); $process->run(); return removeAnsiEscapeSequences($process->getOutput()); }; expect($output())->toMatchSnapshot(); })->skipOnWindows(); ================================================ FILE: tests/Visual/JUnit.php ================================================ 'DefaultPrinter', 'COLLISION_IGNORE_DURATION' => 'true'], ); $process->run(); $rawXmlContent = file_get_contents($junitLogFile); unlink($junitLogFile); // convert xml to array try { $xml = new SimpleXMLElement(preg_replace("/(<\/?)(\w+):([^>]*>)/", '$1$2$3', $rawXmlContent)); return json_decode(json_encode((array) $xml), true); } catch (Exception $exception) { throw new XmlParseException($exception->getMessage(), $exception->getCode(), $exception->getPrevious()); } }; $normalizedPath = function (string $path) { return str_replace('/', DIRECTORY_SEPARATOR, $path); }; test('junit output', function () use ($normalizedPath, $run) { $result = $run('tests/.tests/SuccessOnly.php'); expect($result['testsuite']['@attributes']) ->name->toBe('Tests\tests\SuccessOnly') ->file->toBe($normalizedPath('tests/.tests/SuccessOnly.php')) ->tests->toBe('4') ->assertions->toBe('4') ->errors->toBe('0') ->failures->toBe('0') ->skipped->toBe('0'); expect($result['testsuite']['testcase']) ->toHaveCount(2); expect($result['testsuite']['testcase'][0]['@attributes']) ->name->toBe('it can pass with comparison') ->file->toBe($normalizedPath('tests/.tests/SuccessOnly.php::it can pass with comparison')) ->class->toBe('Tests\tests\SuccessOnly') ->classname->toBe('Tests.tests.SuccessOnly') ->assertions->toBe('1') ->time->toStartWith('0.0'); }); test('junit with parallel', function () use ($normalizedPath, $run) { $result = $run('tests/.tests/SuccessOnly.php', '--parallel', '--processes=1', '--filter', 'can pass with comparison'); expect($result['testsuite']['@attributes']) ->name->toBe('Tests\tests\SuccessOnly') ->file->toBe($normalizedPath('tests/.tests/SuccessOnly.php')) ->tests->toBe('2') ->assertions->toBe('2') ->errors->toBe('0') ->failures->toBe('0') ->skipped->toBe('0'); expect($result['testsuite']['testcase']) ->toHaveCount(2); expect($result['testsuite']['testcase'][0]['@attributes']) ->name->toBe('it can pass with comparison') ->file->toBe($normalizedPath('tests/.tests/SuccessOnly.php::it can pass with comparison')) ->class->toBe('Tests\tests\SuccessOnly') ->classname->toBe('Tests.tests.SuccessOnly') ->assertions->toBe('1') ->time->toStartWith('0.0'); })->skip('Not working yet'); ================================================ FILE: tests/Visual/Parallel.php ================================================ 'DefaultPrinter', 'COLLISION_IGNORE_DURATION' => 'true'], ); $process->run(); return removeAnsiEscapeSequences($process->getOutput()); }; test('parallel', function () use ($run) { expect($run('--exclude-group=integration')) ->toContain('Tests: 2 deprecated, 4 warnings, 5 incomplete, 2 notices, 39 todos, 26 skipped, 1177 passed (2789 assertions)') ->toContain('Parallel: 3 processes'); })->skipOnWindows(); test('a parallel test can extend another test with same name', function () use ($run) { expect($run('tests/Fixtures/Inheritance'))->toContain('Tests: 1 skipped, 2 passed (2 assertions)'); }); ================================================ FILE: tests/Visual/SingleTestOrDirectory.php ================================================ 'DefaultPrinter', 'COLLISION_IGNORE_DURATION' => 'true'], ); $process->run(); return $decorated ? $process->getOutput() : removeAnsiEscapeSequences($process->getOutput()); }; $snapshot = function ($name) { $testsPath = dirname(__DIR__); return file_get_contents(implode(DIRECTORY_SEPARATOR, [ $testsPath, '.snapshots', "$name.txt", ])); }; test('allows to run a single test', function () use ($run, $snapshot) { expect($run('tests/Fixtures/DirectoryWithTests/ExampleTest.php'))->toContain($snapshot('allows-to-run-a-single-test')); })->skipOnWindows(); test('allows to run a directory', function () use ($run, $snapshot) { expect($run('tests/Fixtures'))->toContain($snapshot('allows-to-run-a-directory')); })->skipOnWindows(); it('disable decorating printer when colors is set to never', function () use ($snapshot) { $process = new Process([ 'php', './bin/pest', '--colors=never', 'tests/Fixtures/DirectoryWithTests/ExampleTest.php', ], dirname(__DIR__, 2), ['COLLISION_PRINTER' => 'DefaultPrinter', 'COLLISION_IGNORE_DURATION' => 'true']); $process->run(); $output = $process->getOutput(); expect($output)->toContain($snapshot('disable-decorating-printer')); })->skipOnWindows(); ================================================ FILE: tests/Visual/Success.php ================================================ 'integration', '--exclude-group' => 'integration', 'REBUILD_SNAPSHOTS' => false, 'PARATEST' => 0, 'COLLISION_PRINTER' => 'DefaultPrinter', 'COLLISION_IGNORE_DURATION' => 'true'], )); $process->run(); return preg_replace([ '#\\x1b[[][^A-Za-z]*[A-Za-z]#', '/(Tests\\\PHPUnit\\\CustomAffixes\\\InvalidTestName)([A-Za-z0-9]*)/', ], [ '', '$1', ], $process->getOutput()); }; if (getenv('REBUILD_SNAPSHOTS')) { $outputContent = explode("\n", $output()); array_pop($outputContent); array_pop($outputContent); array_pop($outputContent); file_put_contents($snapshot, implode("\n", $outputContent)); } elseif (! getenv('EXCLUDE')) { $output = explode("\n", $output()); array_pop($output); array_pop($output); expect(implode("\n", $output))->toContain(file_get_contents($snapshot)); } })->skip(! getenv('REBUILD_SNAPSHOTS') && getenv('EXCLUDE')) ->skipOnWindows(); ================================================ FILE: tests/Visual/TeamCity.php ================================================ 'integration', 'REBUILD_SNAPSHOTS' => false, 'PARATEST' => 0, 'COLLISION_IGNORE_DURATION' => 'true', 'FLOW_ID' => '1234', ], )); $process->run(); return $process->getOutput(); }; if (getenv('REBUILD_SNAPSHOTS')) { file_put_contents($snapshot, normalize_windows_os_output($output())); } elseif (! getenv('EXCLUDE')) { expect(normalize_windows_os_output($output()))->toEqual(file_get_contents($snapshot)); } })->with([ 'Failure.php', 'SuccessOnly.php', ])->skip(! getenv('REBUILD_SNAPSHOTS') && getenv('EXCLUDE')); ================================================ FILE: tests/Visual/Todo.php ================================================ 'DefaultPrinter', 'COLLISION_IGNORE_DURATION' => 'true'], ); $process->run(); expect($process->getExitCode())->toBe(0); $output = $process->getOutput(); return preg_replace('/Duration: \d+\.\d+s/', 'Duration: x.xxs', removeAnsiEscapeSequences($output)); }; test('todos', function () use ($run) { expect($run('--todos', false))->toMatchSnapshot(); })->skipOnWindows()->skip(! getenv('REBUILD_SNAPSHOTS') && getenv('EXCLUDE')); test('todos in parallel', function () use ($run) { expect($run('--todos', true))->toMatchSnapshot(); })->skipOnWindows()->skip(! getenv('REBUILD_SNAPSHOTS') && getenv('EXCLUDE')); test('todo', function () use ($run) { expect($run('--todo', false))->toMatchSnapshot(); })->skipOnWindows()->skip(! getenv('REBUILD_SNAPSHOTS') && getenv('EXCLUDE')); test('todo in parallel', function () use ($run) { expect($run('--todo', true))->toMatchSnapshot(); })->skipOnWindows()->skip(! getenv('REBUILD_SNAPSHOTS') && getenv('EXCLUDE')); ================================================ FILE: tests/Visual/Version.php ================================================ 'DefaultPrinter', 'COLLISION_IGNORE_DURATION' => 'true'])); $process->run(); return removeAnsiEscapeSequences($process->getOutput()); }; expect($output())->toMatchSnapshot(); })->skipOnWindows()->skip(! getenv('REBUILD_SNAPSHOTS') && getenv('EXCLUDE')); ================================================ FILE: tests-external/Features/Expect/toMatchSnapshot.php ================================================ snapshotable = <<<'HTML'

Snapshot

HTML; }); test('pass with dataset', function ($data) { TestSuite::getInstance()->snapshots->save($this->snapshotable); [$filename] = TestSuite::getInstance()->snapshots->get(); expect($filename)->toStartWith('tests/.pest/snapshots-external/') ->toEndWith('pass_with_dataset_with_data_set____my_datas_set_value___.snap') ->and($this->snapshotable)->toMatchSnapshot(); })->with(['my-datas-set-value']); describe('within describe', function () { test('pass with dataset', function ($data) { TestSuite::getInstance()->snapshots->save($this->snapshotable); [$filename] = TestSuite::getInstance()->snapshots->get(); expect($filename)->toStartWith('tests/.pest/snapshots-external/') ->toEndWith('pass_with_dataset_with_data_set____my_datas_set_value___.snap') ->and($this->snapshotable)->toMatchSnapshot(); }); })->with(['my-datas-set-value']);