Repository: pestphp/pest-intellij Branch: master Commit: 8c7aca5430a1 Files: 461 Total size: 542.8 KB Directory structure: gitextract_h0dnztv0/ ├── .editorconfig ├── .github/ │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ └── feature-request.md │ ├── PULL_REQUEST_TEMPLATE.md │ ├── dependabot.yml │ └── workflows/ │ ├── auto-close.yml │ ├── build.yml │ ├── qodana.yml │ ├── release.yml │ └── run-ui-tests.yml ├── .gitignore ├── .run/ │ ├── Run IDE with Plugin.run.xml │ ├── Run Plugin Tests.run.xml │ ├── Run Plugin Verification.run.xml │ └── Run Qodana.run.xml ├── BUILD.bazel ├── CHANGELOG.md ├── CLAUDE.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── build.gradle.kts ├── coverage/ │ ├── BUILD.bazel │ ├── intellij.pest.coverage.iml │ ├── resources/ │ │ └── intellij.pest.coverage.xml │ ├── src/ │ │ ├── PestCoverageEnabledConfiguration.kt │ │ ├── PestCoverageEngine.kt │ │ ├── PestCoverageProgramRunner.kt │ │ └── features/ │ │ └── mutate/ │ │ ├── PestMutateProgramRunner.kt │ │ └── PestMutateTestExecutor.kt │ └── tests/ │ ├── BUILD.bazel │ ├── intellij.pest.coverage.tests.iml │ ├── src/ │ │ └── com/ │ │ └── intellij/ │ │ └── pest/ │ │ └── coverage/ │ │ ├── PestCoverageProgramRunnerTest.kt │ │ └── features/ │ │ └── mutate/ │ │ └── PestMutateProgramRunnerTest.kt │ └── testData/ │ ├── ATest.php │ ├── features/ │ │ └── mutate/ │ │ ├── ATest.php │ │ └── php │ └── php ├── gradle/ │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── intellij.pest.iml ├── intellij.pest.tests.iml ├── plugin-content.yaml ├── settings.gradle.kts └── src/ ├── main/ │ ├── java/ │ │ └── com/ │ │ └── pestphp/ │ │ └── pest/ │ │ ├── PestIcons.java │ │ └── configuration/ │ │ ├── PestRunConfigurationSettings.java │ │ └── PhpTestRunConfiguration.java │ ├── kotlin/ │ │ └── com/ │ │ └── pestphp/ │ │ └── pest/ │ │ ├── FileUtil.kt │ │ ├── PestBundle.kt │ │ ├── PestComposerConfig.kt │ │ ├── PestFrameworkType.kt │ │ ├── PestFunctionsUtil.kt │ │ ├── PestIconProvider.kt │ │ ├── PestNamingUtil.kt │ │ ├── PestNewTestFromClassAction.kt │ │ ├── PestPluginDisposable.kt │ │ ├── PestSettings.kt │ │ ├── PestTestCreateInfo.kt │ │ ├── PestTestDescriptor.kt │ │ ├── PestTestFileUtil.kt │ │ ├── PestTestRunLineMarkerProvider.kt │ │ ├── PestUtil.kt │ │ ├── annotator/ │ │ │ ├── PestAnnotator.kt │ │ │ └── PestAnnotatorVisitor.kt │ │ ├── completion/ │ │ │ ├── InternalMembersCompletionProvider.kt │ │ │ ├── PestCompletionContributor.kt │ │ │ ├── PestCustomExtensionCompletionProvider.kt │ │ │ └── ThisFieldsCompletionProvider.kt │ │ ├── configuration/ │ │ │ ├── PestDebugRunner.kt │ │ │ ├── PestLocationProvider.kt │ │ │ ├── PestRerunFailedTestsAction.kt │ │ │ ├── PestRerunProfile.kt │ │ │ ├── PestRunConfiguration.kt │ │ │ ├── PestRunConfigurationHandler.kt │ │ │ ├── PestRunConfigurationProducer.kt │ │ │ ├── PestRunConfigurationType.kt │ │ │ ├── PestRunnerSettings.kt │ │ │ ├── PestTestRunConfigurationEditor.kt │ │ │ └── PestVersionDetector.kt │ │ ├── features/ │ │ │ ├── configuration/ │ │ │ │ ├── ConfigurationInDirectoryReferenceProvider.kt │ │ │ │ ├── ConfigurationReferenceContributor.kt │ │ │ │ └── PhpFolderReferenceSet.kt │ │ │ ├── customExpectations/ │ │ │ │ ├── CustomExpectationIndex.kt │ │ │ │ ├── CustomExpectationNotifier.kt │ │ │ │ ├── CustomExpectationParameterInfoHandler.kt │ │ │ │ ├── CustomExpectationRemoveGeneratedFileStartupActivity.kt │ │ │ │ ├── ListMethodDataExternalizer.kt │ │ │ │ ├── MethodDataExternalizer.kt │ │ │ │ ├── expectationUtil.kt │ │ │ │ ├── externalizers/ │ │ │ │ │ ├── ListDataExternalizer.kt │ │ │ │ │ ├── MethodDataExternalizer.kt │ │ │ │ │ ├── ParameterDataExternalizer.kt │ │ │ │ │ └── PhpTypeDataExternalizer.kt │ │ │ │ ├── generators/ │ │ │ │ │ ├── ExpectationGenerator.kt │ │ │ │ │ ├── Method.kt │ │ │ │ │ └── Parameter.kt │ │ │ │ └── symbols/ │ │ │ │ ├── PestCustomExpectationReference.kt │ │ │ │ ├── PestCustomExpectationReferenceProvider.kt │ │ │ │ ├── PestCustomExpectationRenameUsageSearcher.kt │ │ │ │ ├── PestCustomExpectationSymbol.kt │ │ │ │ ├── PestCustomExpectationSymbolDeclaration.kt │ │ │ │ ├── PestCustomExpectationSymbolDeclarationProvider.kt │ │ │ │ └── PestCustomExpectationUsageSearcher.kt │ │ │ ├── datasets/ │ │ │ │ ├── DatasetIndex.kt │ │ │ │ ├── DatasetReference.kt │ │ │ │ ├── DatasetReferenceContributor.kt │ │ │ │ ├── DatasetReferenceProvider.kt │ │ │ │ ├── DatasetUtil.kt │ │ │ │ ├── InvalidDatasetNameCaseInspection.kt │ │ │ │ └── InvalidDatasetReferenceInspection.kt │ │ │ ├── parallel/ │ │ │ │ ├── PestParallelProgramRunner.kt │ │ │ │ ├── PestParallelSMTEventsAdapter.kt │ │ │ │ └── PestParallelTestExecutor.kt │ │ │ └── snapshotTesting/ │ │ │ ├── SnapshotLineMarkerProvider.kt │ │ │ └── SnapshotUtil.kt │ │ ├── goto/ │ │ │ ├── PestDatasetUsagesGotoHandler.kt │ │ │ ├── PestGotoTargetPresentationProvider.kt │ │ │ ├── PestTestFinder.kt │ │ │ └── PestTestGoToSymbolContributor.kt │ │ ├── indexers/ │ │ │ └── PestTestIndex.kt │ │ ├── inspections/ │ │ │ ├── ChangeMultipleExpectCallsToChainableQuickFix.kt │ │ │ ├── ChangeTestNameCasingQuickFix.kt │ │ │ ├── InvalidTestNameCaseInspection.kt │ │ │ ├── MissingScreenshotSnapshotInspection.kt │ │ │ ├── MultipleExpectChainableInspection.kt │ │ │ ├── PestAssertionCanBeSimplifiedInspection.kt │ │ │ ├── PestTestFailedLineInspection.kt │ │ │ ├── SuppressExpressionResultUnusedInspection.kt │ │ │ └── SuppressUndefinedPropertyInspection.kt │ │ ├── notifications/ │ │ │ └── OutdatedNotification.kt │ │ ├── parser/ │ │ │ ├── PestConfigurationFile.kt │ │ │ └── PestConfigurationFileParser.kt │ │ ├── runner/ │ │ │ ├── LocationInfo.kt │ │ │ ├── PestConsoleProperties.kt │ │ │ ├── PestFailedLineManager.kt │ │ │ ├── PestPressToContinueAction.kt │ │ │ ├── PestPromptConsoleFolding.kt │ │ │ └── PestTestStackTraceParser.kt │ │ ├── statistics/ │ │ │ └── PestUsagesCollector.kt │ │ ├── structureView/ │ │ │ ├── PestStructureViewElement.kt │ │ │ └── PestStructureViewExtension.kt │ │ ├── surrounders/ │ │ │ ├── ExpectStatementSurrounder.kt │ │ │ └── StatementSurroundDescriptor.kt │ │ ├── templates/ │ │ │ ├── PestConfigNewDatasetFileAction.kt │ │ │ ├── PestConfigNewFileAction.kt │ │ │ ├── PestDescribePostfixTemplate.kt │ │ │ ├── PestItPostfixTemplate.kt │ │ │ ├── PestPostfixTemplateProvider.kt │ │ │ └── PestRootTemplateContextType.kt │ │ └── types/ │ │ ├── HigherOrderExtendTypeProvider.kt │ │ ├── InnerTestTypeProvider.kt │ │ ├── ThisExtendTypeProvider.kt │ │ ├── ThisFieldTypeProvider.kt │ │ └── ThisTypeProvider.kt │ └── resources/ │ ├── META-INF/ │ │ └── plugin.xml │ ├── fileTemplates/ │ │ └── internal/ │ │ ├── Pest It.php.ft │ │ ├── Pest Scoped Dataset.php.ft │ │ ├── Pest Shared Dataset.php.ft │ │ ├── Pest Test.php.ft │ │ └── Pest file from class.php.ft │ ├── inspectionDescriptions/ │ │ ├── InvalidDatasetNameCaseInspection.html │ │ ├── InvalidDatasetReferenceInspection.html │ │ ├── InvalidTestNameCaseInspection.html │ │ ├── MissingScreenshotSnapshotInspection.html │ │ ├── MultipleExpectChainableInspection.html │ │ ├── PestAssertionCanBeSimplifiedInspection.html │ │ └── PestTestFailedLineInspection.html │ ├── liveTemplates/ │ │ └── PestPHP.xml │ ├── log4j.properties │ ├── messages/ │ │ └── pestBundle.properties │ └── postfixTemplates/ │ ├── PestDescribePostfixTemplate/ │ │ ├── after.php.template │ │ ├── before.php.template │ │ └── description.html │ └── PestItPostfixTemplate/ │ ├── after.php.template │ ├── before.php.template │ └── description.html └── test/ ├── kotlin/ │ └── com/ │ └── pestphp/ │ └── pest/ │ ├── PestIconProviderTest.kt │ ├── PestLightCodeFixture.kt │ ├── PestTestRunLineMarkerProviderTest.kt │ ├── annotator/ │ │ └── PestAnnotatorTest.kt │ ├── codeInsight/ │ │ └── typeInference/ │ │ └── PestTypeInferenceTest.kt │ ├── configuration/ │ │ ├── PestLocationProviderTest.kt │ │ ├── PestRunConfigurationTest.kt │ │ ├── PestVersionDetectorTest.kt │ │ ├── PestVersionParserTest.kt │ │ ├── pest/ │ │ │ └── PestConfigurationFileTest.kt │ │ └── uses/ │ │ └── PestConfigurationFileTest.kt │ ├── customExpectations/ │ │ ├── ListMethodDataExternalizerTest.kt │ │ ├── MethodDataExternalizerTest.kt │ │ └── generators/ │ │ └── ExpectationGeneratorTest.kt │ ├── features/ │ │ ├── configuration/ │ │ │ ├── PestCompletionTest.kt │ │ │ └── UsesCompletionTest.kt │ │ ├── datasets/ │ │ │ ├── DatasetCompletionTest.kt │ │ │ ├── DatasetGoToTest.kt │ │ │ ├── DatasetIndexTest.kt │ │ │ ├── DatasetReferenceTest.kt │ │ │ ├── DatasetUsagesTest.kt │ │ │ ├── InvalidDatasetNameCaseInspectionTest.kt │ │ │ └── InvalidDatasetReferenceInspectionTest.kt │ │ ├── parallel/ │ │ │ ├── PestParallelProgramRunnerTest.kt │ │ │ └── PestParallelSMTEventsAdapterTest.kt │ │ └── snapshotTesting/ │ │ ├── SnapshotLineMarkerProviderTest.kt │ │ └── SnapshotUtilTest.kt │ ├── generateTest/ │ │ └── PestNewTestFromClassActionTest.kt │ ├── goto/ │ │ └── PestTestFinderTest.kt │ ├── higherOrderExpectations/ │ │ ├── HigherOrderExpectationAssertionCompletionTest.kt │ │ └── HigherOrderExpectationCompletionTest.kt │ ├── indexers/ │ │ └── PestTestIndexTest.kt │ ├── inspections/ │ │ ├── InvalidTestNameCaseInspectionTest.kt │ │ ├── MissingScreenshotSnapshotInspectionTest.kt │ │ ├── MultipleExpectChainableInspectionTest.kt │ │ ├── PestAssertionCanBeSimplifiedInspectionTest.kt │ │ ├── PestTestFailedLineInspectionTest.kt │ │ └── PhpStormInspectionsTest.kt │ ├── runner/ │ │ ├── PestPressToContinueActionTest.kt │ │ └── PestTestStackTraceParserTest.kt │ ├── surrounders/ │ │ ├── ExpectStatementSurrounderTest.kt │ │ └── SurroundTestCase.kt │ ├── templates/ │ │ └── PestPostfixTemplateProviderTest.kt │ ├── types/ │ │ ├── BaseTypeTestCase.kt │ │ ├── ExpectCallCompletionTest.kt │ │ ├── FunctionTypeTest.kt │ │ ├── ThisFieldCompletionTest.kt │ │ ├── ThisFieldTypeTest.kt │ │ └── ThisTypeTest.kt │ └── utilTests/ │ ├── GetPestTestNameTests.kt │ ├── GetPestTestsTest.kt │ ├── IsPestTestFileTest.kt │ ├── IsPestTestFunctionTest.kt │ ├── PestUtilTest.kt │ ├── ToPestFqnTests.kt │ └── ToPestTestRegexTests.kt └── resources/ └── com/ └── pestphp/ └── pest/ ├── Dataset.php ├── Pest.php ├── PestTestRunLineMarkerProviderTest/ │ ├── AssignmentFunctionCallNamedTest.php │ ├── AssignmentFunctionCallNamedTestWithoutPest.php │ ├── FunctionCallNamedTestAsArgument.php │ ├── FunctionCallNamedTestInsideDescribeBlock.php │ ├── FunctionCallNamedTestInsideTest.php │ ├── FunctionCallNamedTestWithoutPest.php │ ├── MethodCallNamedItAndVariableTest.php │ ├── NamedDataSets.php │ ├── PestItFunctionCallWithDescriptionAndClosure.php │ ├── PestItFunctionCallWithRedefinition.php │ └── contextProject/ │ └── tests/ │ └── Test.php ├── PestUtil/ │ ├── Login.integration.test.php │ ├── Login.test.php │ ├── MethodCallNamedIt.php │ ├── MethodCallNamedItAndVariableTest.php │ ├── MethodCallNamedTest.php │ ├── NestedDescribeFunctionCalls.php │ ├── PestArchFunctionCall.php │ ├── PestDescribeBlock.php │ ├── PestDescribeBlockAndTestFunctionEndOfLine.php │ ├── PestItFunctionCallWithConcatString.php │ ├── PestItFunctionCallWithDescriptionAndClosure.php │ ├── PestItFunctionCallWithDescriptionAndHigherOrder.php │ ├── PestTestFunctionCallWithCircumflex.php │ ├── PestTestFunctionCallWithConcatString.php │ ├── PestTestFunctionCallWithDescriptionAndClosure.php │ ├── PestTestFunctionCallWithDescriptionAndHigherOrder.php │ ├── PestTestFunctionCallWithNamesapce.php │ ├── PestTestFunctionCallWithParenthesis.php │ ├── PestTestWithPlusAndQuestionMark.php │ ├── User.spec.php │ └── dir.name/ │ └── Test.php ├── SimpleHigherOrderNotTest.php ├── SimpleHigherOrderTestWithName.php ├── SimpleScript.php ├── TestWithDataset.php ├── annotator/ │ ├── DuplicateCustomExpectation.afterDelete.php │ ├── DuplicateCustomExpectation.afterNavigate.php │ ├── DuplicateCustomExpectation.php │ ├── DuplicateTestName.afterDelete.php │ ├── DuplicateTestName.afterNavigate.php │ ├── DuplicateTestName.php │ ├── DuplicateTestNameInDescribeBlock.php │ ├── NoDuplicateCustomExpectation.php │ ├── NoDuplicateTestName.php │ └── stub/ │ └── Functions.php ├── codeInsight/ │ └── typeInference/ │ ├── ThisInInnerClosure.php │ └── ThisInSubproject/ │ └── Test.php ├── configuration/ │ ├── FileWithPestTest.php │ ├── locationProvider/ │ │ ├── DescribeBlock/ │ │ │ └── subdir/ │ │ │ └── Test.php │ │ ├── DescribeBlockIt/ │ │ │ └── subdir/ │ │ │ └── Test.php │ │ ├── SubprojectFor1xVersion/ │ │ │ └── subdir/ │ │ │ └── Test.php │ │ └── SubprojectFor2xVersion/ │ │ └── subdir/ │ │ └── Test.php │ ├── pest/ │ │ ├── tests/ │ │ │ ├── DIRFeature/ │ │ │ │ └── FeatureTest.php │ │ │ ├── DynamicFeature/ │ │ │ │ └── FeatureTest.php │ │ │ ├── Feature/ │ │ │ │ └── FeatureTest.php │ │ │ ├── GlobPattern/ │ │ │ │ ├── DirectoryTest.php │ │ │ │ ├── FileTest.php │ │ │ │ └── FileWithRelativePathTest.php │ │ │ ├── GroupedFeature/ │ │ │ │ └── GroupedFeatureTest.php │ │ │ ├── Pest.php │ │ │ └── Unit/ │ │ │ ├── PestExtendUnitTest.php │ │ │ └── UnitTest.php │ │ └── tests2/ │ │ └── Unit/ │ │ └── UnitTest.php │ ├── php │ └── uses/ │ ├── DIRFeature/ │ │ └── FeatureTest.php │ ├── DynamicFeature/ │ │ └── FeatureTest.php │ ├── Feature/ │ │ └── FeatureTest.php │ ├── GlobPattern/ │ │ ├── DirectoryTest.php │ │ ├── FileTest.php │ │ └── FileWithRelativePathTest.php │ ├── GroupedFeature/ │ │ └── GroupedFeatureTest.php │ ├── Pest.php │ └── Unit/ │ ├── UnitTest.php │ └── UsesUnitTest.php ├── customExpectations/ │ ├── CustomExpectation.php │ ├── CustomExpectationWithParameter.php │ ├── CustomThisExpectation.php │ ├── CustomUserExpectation.php │ ├── UnfinishedCustomExpectation.php │ ├── generators/ │ │ └── ExpectationGenerator/ │ │ └── GeneratedWithMethod.php │ └── subFolder/ │ └── CustomExpectation.php ├── features/ │ ├── configuration/ │ │ ├── pest/ │ │ │ ├── CompleteFakePestInFolder.php │ │ │ ├── CompleteInFolder.php │ │ │ └── Test.php │ │ └── uses/ │ │ ├── CompleteFakeInFolder.php │ │ ├── CompleteInFolder.php │ │ └── Test.php │ ├── datasets/ │ │ ├── AutocompleteDatasetTest.php │ │ ├── DatasetInDescribeBlock.php │ │ ├── DatasetInDescribeBlockCompletion.php │ │ ├── DatasetInDescribeBlockReference.php │ │ ├── DatasetInsideDescribeBlockTest.php │ │ ├── DatasetNoArgsTest.php │ │ ├── DatasetOnNonPestTest.php │ │ ├── DatasetOnNonPestTestCompletion.php │ │ ├── DatasetReference.php │ │ ├── DatasetTest.php │ │ ├── Datasets.php │ │ ├── DoubleWithDatasetReference.php │ │ ├── InvalidDatasetInDescribeBlockTest.php │ │ ├── InvalidDatasetNameCase.after.php │ │ ├── InvalidDatasetNameCase.php │ │ ├── InvalidDatasetTest.php │ │ ├── NotDatasetReference.php │ │ ├── SharedDatasetReference.php │ │ └── ValidDatasetNameCase.php │ └── parallel/ │ ├── ATest.php │ └── php ├── generateTest/ │ ├── testWithNamespace.after.php │ └── testWithNamespace.php ├── goto/ │ ├── PestTestFinder/ │ │ ├── App/ │ │ │ └── User.php │ │ └── test/ │ │ └── App/ │ │ ├── MockTest.php │ │ ├── UserDescribeTest.php │ │ └── UserTest.php │ └── datasetUsages/ │ ├── DatasetDeclaration.php │ └── DatasetUsage.php ├── higherOrderExpectations/ │ ├── .phpstorm.meta.php │ ├── ExpectMethodAssertionCompletion.php │ ├── ExpectMethodAssertionCompletionChained.php │ ├── ExpectMethodAssertionCompletionChainedAssertions.php │ ├── ExpectMethodCompletion.php │ ├── ExpectMethodCompletionChained.php │ ├── ExpectMethodCompletionChainedAssertions.php │ ├── ExpectPropertyAssertionCompletion.php │ ├── ExpectPropertyAssertionCompletionChained.php │ ├── ExpectPropertyAssertionCompletionChainedAssertions.php │ ├── ExpectPropertyCompletion.php │ ├── ExpectPropertyCompletionChained.php │ ├── ExpectPropertyCompletionChainedAssertions.php │ └── stubs.php ├── indexers/ │ └── PestTestIndexTest/ │ ├── FileWithDescribeBlockTest.php │ ├── FileWithPestTest.php │ ├── FileWithPestTodosTest.php │ └── FileWithoutPestTest.php ├── inspections/ │ ├── ExpectCallsWithOtherStatementsBetween.php │ ├── InvalidTestNameAndDatasetName.after.php │ ├── InvalidTestNameAndDatasetName.php │ ├── InvalidTestNameCase.after.php │ ├── InvalidTestNameCase.php │ ├── ManyExpectCall.after.php │ ├── ManyExpectCall.php │ ├── MultipleExpectCall.after.php │ ├── MultipleExpectCall.php │ ├── MultipleExpectCallsWithOtherStatementsBetween.after.php │ ├── MultipleExpectCallsWithOtherStatementsBetween.php │ ├── SingleExpectCall.php │ ├── ValidHigherOrderTestNameCase.php │ ├── ValidItTestNameWithoutSpaces.php │ ├── ValidTestNameCase.php │ ├── ValidTestNameWhenWrongCasingOnOneWord.php │ ├── assertionCanBeSimplified/ │ │ ├── ToBeWithFalse.after.php │ │ ├── ToBeWithFalse.php │ │ ├── ToBeWithNull.after.php │ │ ├── ToBeWithNull.php │ │ ├── ToBeWithTrue.after.php │ │ ├── ToBeWithTrue.php │ │ ├── ToHaveCountWithZero.after.php │ │ └── ToHaveCountWithZero.php │ ├── pestTestFailedLine/ │ │ ├── AnonymousFunction.php │ │ ├── FailedOneLine.php │ │ ├── LambdaFunction.php │ │ ├── MismatchLine.php │ │ ├── MultipleStatementsInOneLine.php │ │ ├── OutRange.php │ │ ├── SingleLeafElementReported.php │ │ ├── TypeBefore.php │ │ ├── TypeInside.php │ │ ├── WithDataSet.php │ │ ├── WithDataSetAndKeys.php │ │ ├── WithDataSetAndSeveralFails.php │ │ └── WithNamedDataSet.php │ ├── phpstorm/ │ │ └── MultipleClassesDeclarationsInPestFileTest.php │ └── screenshotProject/ │ └── tests/ │ ├── .pest/ │ │ └── snapshots/ │ │ └── Feature/ │ │ ├── ScreenshotSnapshot/ │ │ │ └── it_browser_testing.snap │ │ ├── ScreenshotSnapshotComplexName/ │ │ │ └── it_1__2_3_4_.snap │ │ ├── ScreenshotSnapshotMultiple/ │ │ │ ├── it_test.snap │ │ │ └── it_test2.snap │ │ └── nested/ │ │ └── ScreenshotSnapshotNested/ │ │ └── nested.snap │ └── Feature/ │ ├── MissingScreenshotSnapshot.php │ ├── MissingScreenshotSnapshotComplexName.php │ ├── MissingScreenshotSnapshotMultiple.php │ ├── ScreenshotSnapshot.php │ ├── ScreenshotSnapshotComplexName.php │ ├── ScreenshotSnapshotMultiple.php │ └── nested/ │ ├── MissingScreenshotSnapshotNested.php │ └── ScreenshotSnapshotNested.php ├── runner/ │ └── pestTestStacktraceParser/ │ ├── Multiline.php │ ├── OneLine.php │ ├── OneLineRemote.php │ ├── OutRangeLineNumber.php │ └── WrongLineNumber.php ├── snapshotTesting/ │ ├── allSnapshotAssertions.php │ ├── nonSnapshotAssertions.php │ ├── snapshotAssertionUseStatement.php │ ├── snapshotTest.php │ └── tests/ │ └── __snapshots__/ │ └── snapshotTest__it_renders_correctly__1.txt ├── stubs.php ├── templates/ │ ├── describe.after.php │ ├── describe.php │ ├── it.after.php │ └── it.php ├── types/ │ ├── TestCase.php │ ├── expect/ │ │ ├── expectCallCompletion.php │ │ ├── expectCallCompletionChainedNotMethod.php │ │ ├── expectCallCompletionChainedNotProperty.php │ │ ├── expectExtendCallOnNonExpectFunction.php │ │ ├── expectExtendReturnType.php │ │ ├── expectInvalidExtendNoReturnType.php │ │ └── extendCallOnChainedExpectation.php │ ├── function/ │ │ └── testTest.php │ ├── this/ │ │ ├── beforeEach.php │ │ ├── itShortLambdaTest.php │ │ ├── itTest.php │ │ └── testTest.php │ └── thisField/ │ ├── afterEach.php │ ├── afterEachNamespace.php │ ├── beforeEach.php │ ├── beforeEachCompletion.php │ ├── beforeEachNamespace.php │ └── beforeEachNamespaceCompletion.php └── utilTests/ ├── ClassNameResolutionInNamespaceTest.php ├── ClassNameResolutionTest.php └── SimpleTest.php ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ [*] indent_size = 4 [{*.kt,*.kts}] ij_kotlin_code_style_defaults = KOTLIN_OFFICIAL ij_kotlin_continuation_indent_size = 4 ================================================ FILE: .github/FUNDING.yml ================================================ github: olivernybroe ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve title: '' labels: '' assignees: '' --- Please report a bug to [YouTrack](https://youtrack.jetbrains.com/newIssue?project=WI) ================================================ FILE: .github/ISSUE_TEMPLATE/feature-request.md ================================================ --- name: Feature request about: Suggest an idea for this project title: '' labels: '' assignees: '' --- Create a discussion instead. ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ - [ ] Added or updated tests - [ ] Updated `CHANGELOG.md` issues: #... ================================================ FILE: .github/dependabot.yml ================================================ # Dependabot configuration: # https://docs.github.com/en/free-pro-team@latest/github/administering-a-repository/configuration-options-for-dependency-updates version: 2 updates: - package-ecosystem: "gradle" directory: "/" schedule: interval: "daily" ================================================ FILE: .github/workflows/auto-close.yml ================================================ on: issues: types: [opened, edited] jobs: auto_close_issues: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v1 - name: Automatically close issues that don't follow the issue template uses: lucasbento/auto-close-issues@v1.0.2 with: github-token: ${{ secrets.GITHUB_TOKEN }} issue-close-message: "@${issue.user.login}: hello! :wave:\n\nThis issue is being automatically closed because it does not follow the issue template." # optional property ================================================ FILE: .github/workflows/build.yml ================================================ # GitHub Actions Workflow created for testing and preparing the plugin release in following steps: # - validate Gradle Wrapper, # - run 'test' and 'verifyPlugin' tasks, # - run Qodana inspections, # - run 'buildPlugin' task and prepare artifact for the further tests, # - run 'runPluginVerifier' task, # - create a draft release. # # Workflow is triggered on push and pull_request events. # # GitHub Actions reference: https://help.github.com/en/actions # ## JBIJPPTPL name: Build on: # Trigger the workflow on pushes to only the 'main' branch (this avoids duplicate checks being run e.g. for dependabot pull requests) push: branches: [main] # Trigger the workflow on any pull request pull_request: jobs: # Run Gradle Wrapper Validation Action to verify the wrapper's checksum # Run verifyPlugin, IntelliJ Plugin Verifier, and test Gradle tasks # Build plugin and provide the artifact for the next workflow jobs build: name: Build runs-on: ubuntu-latest outputs: version: ${{ steps.properties.outputs.version }} changelog: ${{ steps.properties.outputs.changelog }} steps: # Check out current repository - name: Fetch Sources uses: actions/checkout@v2.4.0 # Validate wrapper - name: Gradle Wrapper Validation uses: gradle/wrapper-validation-action@v1.0.4 # Setup Java 11 environment for the next steps - name: Setup Java uses: actions/setup-java@v2 with: distribution: zulu java-version: 11 cache: gradle # Set environment variables - name: Export Properties id: properties shell: bash run: | PROPERTIES="$(./gradlew properties --console=plain -q)" VERSION="$(echo "$PROPERTIES" | grep "^version:" | cut -f2- -d ' ')" NAME="$(echo "$PROPERTIES" | grep "^pluginName:" | cut -f2- -d ' ')" CHANGELOG="$(./gradlew getChangelog --unreleased --no-header --console=plain -q)" CHANGELOG="${CHANGELOG//'%'/'%25'}" CHANGELOG="${CHANGELOG//$'\n'/'%0A'}" CHANGELOG="${CHANGELOG//$'\r'/'%0D'}" echo "::set-output name=version::$VERSION" echo "::set-output name=name::$NAME" echo "::set-output name=changelog::$CHANGELOG" echo "::set-output name=pluginVerifierHomeDir::~/.pluginVerifier" ./gradlew listProductsReleases # prepare list of IDEs for Plugin Verifier # Run tests - name: Run Tests run: ./gradlew test # Collect Tests Result of failed tests - name: Collect Tests Result if: ${{ failure() }} uses: actions/upload-artifact@v2 with: name: tests-result path: ${{ github.workspace }}/build/reports/tests # # Cache Plugin Verifier IDEs # - name: Setup Plugin Verifier IDEs Cache # uses: actions/cache@v2.1.7 # with: # path: ${{ steps.properties.outputs.pluginVerifierHomeDir }}/ides # key: plugin-verifier-${{ hashFiles('build/listProductsReleases.txt') }} # # # Run Verify Plugin task and IntelliJ Plugin Verifier tool # - name: Run Plugin Verification tasks # run: ./gradlew runPluginVerifier -Pplugin.verifier.home.dir=${{ steps.properties.outputs.pluginVerifierHomeDir }} # # # Collect Plugin Verifier Result # - name: Collect Plugin Verifier Result # if: ${{ always() }} # uses: actions/upload-artifact@v2 # with: # name: pluginVerifier-result # path: ${{ github.workspace }}/build/reports/pluginVerifier # TODO: temp needed because verifier disabled - run: ./gradlew buildPlugin # Prepare plugin archive content for creating artifact - name: Prepare Plugin Artifact id: artifact shell: bash run: | cd ${{ github.workspace }}/build/distributions FILENAME=`ls *.zip` unzip "$FILENAME" -d content echo "::set-output name=filename::${FILENAME:0:-4}" # Store already-built plugin as an artifact for downloading - name: Upload artifact uses: actions/upload-artifact@v2.2.4 with: name: ${{ steps.artifact.outputs.filename }} path: ./build/distributions/content/*/* # Prepare a draft release for GitHub Releases page for the manual verification # If accepted and published, release workflow would be triggered releaseDraft: name: Release Draft if: github.event_name != 'pull_request' needs: build runs-on: ubuntu-latest steps: # Check out current repository - name: Fetch Sources uses: actions/checkout@v2.4.0 # Remove old release drafts by using the curl request for the available releases with draft flag - name: Remove Old Release Drafts env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | gh api repos/{owner}/{repo}/releases \ --jq '.[] | select(.draft == true) | .id' \ | xargs -I '{}' gh api -X DELETE repos/{owner}/{repo}/releases/{} # Create new release draft - which is not publicly visible and requires manual acceptance - name: Create Release Draft env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | gh release create v${{ needs.build.outputs.version }} \ --draft \ --title "v${{ needs.build.outputs.version }}" \ --notes "$(cat << 'EOM' ${{ needs.build.outputs.changelog }} EOM )" ================================================ FILE: .github/workflows/qodana.yml ================================================ name: Qodana on: workflow_dispatch: pull_request: branches: - main push: branches: - main jobs: qodana: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 with: fetch-depth: 0 # Setup Java 11 environment for the next steps - name: Setup Java uses: actions/setup-java@v2 with: distribution: zulu java-version: 11 cache: gradle # Build - name: Run Build run: ./gradlew build - name: 'Qodana Scan' uses: JetBrains/qodana-action@v2023.1.0 env: QODANA_TOKEN: ${{ secrets.QODANA_TOKEN }} ================================================ FILE: .github/workflows/release.yml ================================================ # GitHub Actions Workflow created for handling the release process based on the draft release prepared # with the Build workflow. Running the publishPlugin task requires the PUBLISH_TOKEN secret provided. name: Release on: release: types: [prereleased, released] jobs: # Prepare and publish the plugin to the Marketplace repository release: name: Publish Plugin runs-on: ubuntu-latest steps: # Check out current repository - name: Fetch Sources uses: actions/checkout@v2.4.0 with: ref: ${{ github.event.release.tag_name }} # Setup Java 11 environment for the next steps - name: Setup Java uses: actions/setup-java@v2 with: distribution: zulu java-version: 11 cache: gradle # Set environment variables - name: Export Properties id: properties shell: bash run: | CHANGELOG="$(cat << 'EOM' | sed -e 's/^[[:space:]]*$//g' -e '/./,$!d' ${{ github.event.release.body }} EOM )" CHANGELOG="${CHANGELOG//'%'/'%25'}" CHANGELOG="${CHANGELOG//$'\n'/'%0A'}" CHANGELOG="${CHANGELOG//$'\r'/'%0D'}" echo "::set-output name=changelog::$CHANGELOG" # Update Unreleased section with the current release note - name: Patch Changelog if: ${{ steps.properties.outputs.changelog != '' }} env: CHANGELOG: ${{ steps.properties.outputs.changelog }} run: | ./gradlew patchChangelog --release-note="$CHANGELOG" # Publish the plugin to the Marketplace - name: Publish Plugin env: PUBLISH_TOKEN: ${{ secrets.PUBLISH_TOKEN }} run: ./gradlew publishPlugin # Upload artifact as a release asset - name: Upload Release Asset env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: gh release upload ${{ github.event.release.tag_name }} ./build/distributions/* # Create pull request - name: Create Pull Request if: ${{ steps.properties.outputs.changelog != '' }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | VERSION="${{ github.event.release.tag_name }}" BRANCH="changelog-update-$VERSION" git config user.email "action@github.com" git config user.name "GitHub Action" git checkout -b $BRANCH git commit -am "Changelog update - $VERSION" git push --set-upstream origin $BRANCH gh pr create \ --title "Changelog update - \`$VERSION\`" \ --body "Current pull request contains patched \`CHANGELOG.md\` file for the \`$VERSION\` version." \ --base main \ --head $BRANCH ================================================ FILE: .github/workflows/run-ui-tests.yml ================================================ # GitHub Actions Workflow for launching UI tests on Linux, Windows, and Mac in the following steps: # - prepare and launch IDE with your plugin and robot-server plugin, which is needed to interact with UI # - wait for IDE to start # - run UI tests with separate Gradle task # # Please check https://github.com/JetBrains/intellij-ui-test-robot for information about UI tests with IntelliJ Platform # # Workflow is triggered manually. name: Run UI Tests on: workflow_dispatch jobs: testUI: runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: include: - os: ubuntu-latest runIde: | export DISPLAY=:99.0 Xvfb -ac :99 -screen 0 1920x1080x16 & gradle runIdeForUiTests & - os: windows-latest runIde: start gradlew.bat runIdeForUiTests - os: macos-latest runIde: ./gradlew runIdeForUiTests & steps: # Check out current repository - name: Fetch Sources uses: actions/checkout@v2.4.0 # Setup Java 11 environment for the next steps - name: Setup Java uses: actions/setup-java@v2 with: distribution: zulu java-version: 11 cache: gradle # Run IDEA prepared for UI testing - name: Run IDE run: ${{ matrix.runIde }} # Wait for IDEA to be started - name: Health Check uses: jtalk/url-health-check-action@v2 with: url: http://127.0.0.1:8082 max-attempts: 15 retry-delay: 30s # Run tests - name: Tests run: ./gradlew test ================================================ FILE: .gitignore ================================================ .gradle build .idea .intellijPlatform ================================================ FILE: .run/Run IDE with Plugin.run.xml ================================================ true true false ================================================ FILE: .run/Run Plugin Tests.run.xml ================================================ true true false ================================================ FILE: .run/Run Plugin Verification.run.xml ================================================ true true false ================================================ FILE: .run/Run Qodana.run.xml ================================================ true true false ================================================ FILE: BUILD.bazel ================================================ ### auto-generated section `build intellij.pest` start load("@rules_jvm//:jvm.bzl", "jvm_library") jvm_library( name = "pest", module_name = "intellij.pest", visibility = ["//visibility:public"], srcs = glob(["src/main/java/**/*.kt", "src/main/java/**/*.java", "src/main/java/**/*.form", "src/main/kotlin/**/*.kt", "src/main/kotlin/**/*.java", "src/main/kotlin/**/*.form"], allow_empty = True), resources = glob(["src/main/resources/**/*"]), resource_strip_prefix = "src/main/resources", deps = [ "@community//platform/core-api:core", "@community//platform/execution", "@community//platform/execution-impl", "@community//platform/ide-core-impl", "@community//platform/platform-impl:ide-impl", "//phpstorm/php-openapi:php", "@community//platform/analysis-api:analysis", "@community//platform/lang-core", "@community//platform/projectModel-api:projectModel", "@community//platform/remote-core", "@community//platform/structure-view-impl:structureView-impl", "@community//platform/util", "@community//platform/indexing-api:indexing", "//phpstorm/php:php-impl", "@community//platform/analysis-impl", "@community//platform/editor-ui-api:editor-ui", "@community//platform/smRunner", "@community//platform/platform-util-io:ide-util-io", "@community//platform/ide-core", "@community//platform/lang-api:lang", "@community//platform/lang-impl", "@community//xml/xml-psi-impl:psi-impl", "@community//libraries/fastutil", "@community//xml/dom-impl", "@community//platform/core-ui", "@community//platform/core-impl", "@community//platform/util/text-matching", "@community//platform/util:util-ui", "@community//platform/statistics", "@community//platform/testRunner", ] ) jvm_library( name = "pest_test_lib", testonly = True, module_name = "intellij.pest", visibility = ["//visibility:public"], srcs = glob([], allow_empty = True), runtime_deps = [ ":pest", "@community//platform/core-api:core_test_lib", "@community//platform/execution:execution_test_lib", "@community//platform/execution-impl:execution-impl_test_lib", "@community//platform/ide-core-impl:ide-core-impl_test_lib", "@community//platform/platform-impl:ide-impl_test_lib", "//phpstorm/php-openapi:php_test_lib", "@community//platform/analysis-api:analysis_test_lib", "@community//jps/model-api:model", "@community//jps/model-api:model_test_lib", "@community//platform/lang-core:lang-core_test_lib", "@community//platform/projectModel-api:projectModel_test_lib", "@community//platform/remote-core:remote-core_test_lib", "@community//platform/structure-view-impl:structureView-impl_test_lib", "@community//platform/testFramework", "@community//platform/testFramework:testFramework_test_lib", "@community//platform/util:util_test_lib", "@community//platform/indexing-api:indexing_test_lib", "//phpstorm/php:php-impl_test_lib", "@community//platform/analysis-impl:analysis-impl_test_lib", "@community//platform/editor-ui-api:editor-ui_test_lib", "@community//platform/smRunner:smRunner_test_lib", "@community//platform/platform-util-io:ide-util-io_test_lib", "@community//platform/ide-core:ide-core_test_lib", "@community//platform/lang-api:lang_test_lib", "@community//platform/lang-impl:lang-impl_test_lib", "@community//xml/xml-psi-impl:psi-impl_test_lib", "@community//libraries/fastutil:fastutil_test_lib", "@community//xml/dom-impl:dom-impl_test_lib", "@community//platform/core-ui:core-ui_test_lib", "@community//platform/core-impl:core-impl_test_lib", "@community//platform/util/text-matching:text-matching_test_lib", "@community//platform/util:util-ui_test_lib", "@lib//:io-mockk", "@lib//:io-mockk-jvm", "@community//platform/statistics:statistics_test_lib", "@community//platform/testRunner:testRunner_test_lib", ] ) ### auto-generated section `build intellij.pest` end ### auto-generated section `iml intellij.pest` start exports_files([ "intellij.pest.iml", ], visibility = ["//visibility:public"]) ### auto-generated section `iml intellij.pest` end ### auto-generated section `build intellij.pest.tests` start jvm_library( name = "pest-tests", module_name = "intellij.pest.tests", visibility = ["//visibility:public"], srcs = glob([], allow_empty = True) ) jvm_library( name = "pest-tests_test_lib", testonly = True, visibility = ["//visibility:public"], srcs = glob(["src/test/kotlin/**/*.kt", "src/test/kotlin/**/*.java", "src/test/kotlin/**/*.form"], allow_empty = True), resources = glob(["src/test/resources/**/*"]), resource_strip_prefix = "src/test/resources", associates = [ "//phpstorm/pest", "//phpstorm/pest:pest_test_lib", ], deps = [ "@community//platform/core-api:core", "@community//platform/core-api:core_test_lib", "@community//platform/execution", "@community//platform/execution:execution_test_lib", "@community//platform/execution-impl", "@community//platform/execution-impl:execution-impl_test_lib", "@community//platform/ide-core-impl", "@community//platform/platform-impl:ide-impl", "//phpstorm/php-openapi:php", "//phpstorm/php-openapi:php_test_lib", "@community//platform/analysis-api:analysis", "@community//platform/analysis-api:analysis_test_lib", "@community//jps/model-api:model", "@community//jps/model-api:model_test_lib", "@community//platform/lang-core", "@community//platform/lang-core:lang-core_test_lib", "@community//platform/projectModel-api:projectModel", "@community//platform/projectModel-api:projectModel_test_lib", "@community//platform/remote-core", "@community//platform/remote-core:remote-core_test_lib", "@community//platform/structure-view-impl:structureView-impl", "@community//platform/structure-view-impl:structureView-impl_test_lib", "@community//platform/testFramework", "@community//platform/testFramework:testFramework_test_lib", "@community//platform/util", "@community//platform/util:util_test_lib", "@community//platform/indexing-api:indexing", "@community//platform/indexing-api:indexing_test_lib", "//phpstorm/php:php-impl", "//phpstorm/php:php-impl_test_lib", "@community//platform/analysis-impl", "@community//platform/analysis-impl:analysis-impl_test_lib", "@community//platform/editor-ui-api:editor-ui", "@community//platform/editor-ui-api:editor-ui_test_lib", "@community//platform/smRunner", "@community//platform/smRunner:smRunner_test_lib", "@community//platform/platform-util-io:ide-util-io", "@community//platform/ide-core", "@community//platform/lang-api:lang", "@community//platform/lang-api:lang_test_lib", "@community//platform/lang-impl", "@community//platform/lang-impl:lang-impl_test_lib", "@community//xml/xml-psi-impl:psi-impl", "@community//xml/xml-psi-impl:psi-impl_test_lib", "@community//libraries/fastutil", "@community//libraries/fastutil:fastutil_test_lib", "@community//plugins/coverage-common:coverage", "@community//plugins/coverage-common:coverage_test_lib", "@community//xml/dom-impl", "@community//xml/dom-impl:dom-impl_test_lib", "@community//platform/core-ui", "@community//platform/core-ui:core-ui_test_lib", "@community//platform/core-impl", "@community//platform/core-impl:core-impl_test_lib", "@community//platform/util/text-matching", "@community//platform/util/text-matching:text-matching_test_lib", "@community//platform/util:util-ui", "@community//platform/util:util-ui_test_lib", "@lib//:io-mockk", "@lib//:io-mockk-jvm", "@community//platform/statistics", "@community//platform/statistics:statistics_test_lib", "@community//platform/testRunner", "@community//platform/testRunner:testRunner_test_lib", ], runtime_deps = [ ":pest-tests", "@community//platform/ide-core-impl:ide-core-impl_test_lib", "@community//platform/platform-impl:ide-impl_test_lib", "@community//platform/platform-util-io:ide-util-io_test_lib", "@community//platform/ide-core:ide-core_test_lib", ] ) ### auto-generated section `build intellij.pest.tests` end ### auto-generated section `iml intellij.pest.tests` start exports_files([ "intellij.pest.tests.iml", ], visibility = ["//visibility:public"]) ### auto-generated section `iml intellij.pest.tests` end ### auto-generated section `test intellij.pest.tests` start load("@community//build:tests-options.bzl", "jps_test") jps_test( name = "pest-tests_test", runtime_deps = [":pest-tests_test_lib"] ) ### auto-generated section `test intellij.pest.tests` end ================================================ FILE: CHANGELOG.md ================================================ # PEST IntelliJ Changelog ## Unreleased ### Added - Added proper resolve for custom expectations - Added proper rename for custom expectations - Added migration startup activity to delete redundant generated `Expectation.php` file ### Fixed - Fixed infinite "Closing project..." dialog issue on project close ### Changed - Reworked custom expectations engine using Symbol API - Removed `Expectation.php` generation ## 1.11.0 - 2023-09-12 ### Added - Added support for running tests in describe block ([#498](https://github.com/pestphp/pest-intellij/pull/498)) ### Fixed - Fixed property declared dynamically showing warning in pest test cases - Fixed goto and rerun tests not working on new pest versions ## 1.10.1 - 2023-05-31 ### Changed - Changed pest file creation to two actions (tests and dataset) ### Added - Save test flavour preferences when creating a new test ## 1.10.0 - 2023-05-31 ### Added - Added pest file creation support ### Fixed - Remove test sources filter lookup, as it breaks others plugins ## 1.9.3 - 2023-05-31 ### Fixed - Fixed file icon missing if all tests has property calls - Fixed gutter icon not updating state correctly - Fixed test names with `[` and `]` not being matched correctly - Fixed test name casing inspection not working correctly with `it` tests ## 1.9.2 - 2023-03-01 ### Fixed - Fixed "Preferred Coverage Engine" not being saved ## 1.9.1 - 2023-02-28 ### Fixed - Fixed ComposerLibraryManager being nullable now. - Fixed running tests with filenames containing `_`. ### Changed - Changed logic for base path to be from composer.json file. ## 1.9.0 - 2023-01-15 ### Added - Added support for running specific tests on Pest 2.0 - Added support for running todo's as tests ### Fixed - Fixed running tests with `?` in the name ## 1.8.3 ### Added - Added support for test names with string concat statements - Added stacktrace folding for Pest 2.0 output ### Changed - Removed the "test started at" text on the test console output ### Fixed - Fixed regex to match tests that have both named and unnamed datasets ## 1.8.1 ### Fixed - Fixed originalFile in iconProvider sometimes being null - Fixed DuplicateCustomExpectation testing crashing on unfinished inspections ## 1.8.0 ### Added - Added support for using goto location when using remote interpreters ### Fixed - Fixed nested `readAction` calls in Icon Provider ### Changed - Changed Icon Provider to use indexes for better performance ## 1.7.0 ### Added - Added `uses->in` folder reference - Added registry entry for disabling expectation file generation ### Changed - Changed goto and completion contributor to reference provider - Changed icons to use build-in dark mode switching ### Fixed - Fixed dataset reference error when no dataset provided yet. ## 1.6.2 ### Fixed - Fixed duplicate type provider key with nette plugin ## 1.6.1 ### Added - Added inspection for checking if dataset exists ### Fixed - Fixed dataset autocompletion triggering on all strings - Fixed dataset goto triggering on all strings ## 1.6.0 ### Added - Added converting multiple `expect` to `and` calls instead - Added dataset completion - Added dataset goto ### Fixed - Fixed automatic case changing on multicased string ## 1.5.0 ### Added - Added automatic case changing to pest test names ## 1.4.2 ### Fixed - Changed runReadAction to runReadActionInSmartMode in startup activity ## 1.4.1 ### Changed - Reduced custom expectation index size by over 95% ### Fixed - Check if file exist in index (can happen if file is deleted outside IDE) - Handle path separators per OS ## 1.4.0 ### Added - Added support for dynamic paths in `uses->in` statements - Added inspection for duplicate custom expectation name - Add surrounder for `expect` ### Changed - Define root path from phpunit.xml instead of composer path ### Fixed - Remove `-` from the pest generated regex - Escape `/` in regex method name ## 1.3.0 ### Fixed - Changed services to light services for auto disposable - Fixed null pointer error when no virtual file ### Changed - Change reporting on GitHub to contain full stacktracepa ### Added - Added higher order expectation type provider - Added support for xdebug3 and xdebug2 coverage option ## 1.2.2 ### Fixed - Hide snapshot icon for import statements - Fix ArrayIndex error from ExpectationFileService - Fixed wrong file expectation matching in ExpectationFileService ### Added - Add support for in calls - Added support for running key value datasets ### Changed - Changed root path for regex to be based of vendor dir location instead of working directory ### Removed - Remove service message newline requirement as method is deprecated ## 1.2.1 ### Fixed - Moved file generation into smart invocation ## 1.2.0 ### Added - Added gutter icon for snapshots - Added goto snapshot file ### Fixed - Rewrote the custom expectation system to use a more robust system - Updated custom expectation indexer to v2 ### Changed - Removed decorator in favor of implementing interface ## 1.1.0 ### Fixed - Invoke the FileListener PSI part later (should fix indexing issues) - Fixed stub issues on PestIconProvider by wrapping `runReadAction` - Fixed `$this->field` not working when namespace exist - Fixed Concurrent modification errors on expectation file service - Fixed file generation triggering on projects without pest ### Added - Added new context type for the root of a pest file - Added post fix template for `it` tests - Added live template for `it` test - Added live template for `test` test - Added light icon for `pest.php` file ## 1.0.5 ### Changed - Bumped min IntelliJ version to 2021.1 ## 1.0.4 ### Added - Added Suppress inspection for `$this->field` ## 1.0.3 ### Fixed - Fixed php type resolving during event dispatching on file listener - Fixed PSI and index mismatch on file listener ## 1.0.2 ### Fixed - Fixed indexes being out of date in file listener ## 1.0.1 ### Fixed - Removed usage of globalType (needed for 2020.3 support) ## 1.0.0 ### Added - Added structure support for tests - Added autocompletion for custom expectations - Added pest icon for the Pest.php config file - Added symbol contributor for pest tests ### Fixed - Fixed a read only permission bug when used with Code with me - Fixed wrong namespace in custom expectations file generation ## 0.4.3 ### Added - Added IntelliJ version to bug report - Added new Dataset icons (Thanks @caneco!) - Added test state icons - Added run all test in file icon ### Fixed - Fix support for 2021.1 - Fix running tests with circumflex (^) ### Changed - Bumped min IntelliJ version to 2020.3 ## 0.4.2 ### Added - Added path mapping support ([#77](https://github.com/pestphp/pest-intellij/pull/77)) ### Changed - Bumped min plugin version to 2020.2 - Bumped Java version to 11 ### Removed - Disabled version checking (did not work with path mapping) ([#77](https://github.com/pestphp/pest-intellij/pull/77)) ### Fixed - Escape parenthesis in regex for single test ([#80](https://github.com/pestphp/pest-intellij/pull/80)) - Suppressed expression result unused inspection when on Pest test function element. ([#84](https://github.com/pestphp/pest-intellij/pull/84)) ## 0.4.1 ### Added - Added support for auto-generated `it` test names. ([#72](https://github.com/pestphp/pest-intellij/pull/72)) ### Changed - Made the regex tightly bound and reused the same regex in rerun action. ([#72](https://github.com/pestphp/pest-intellij/pull/72)) ## 0.4.0 ### Added - Added support for showing pest version ([#52](https://github.com/pestphp/pest-intellij/pull/52)) - Type provider for Pest test functions ([#48](https://github.com/pestphp/pest-intellij/pull/48)) - Added support for navigation between tests and test subject ([#53](https://github.com/pestphp/pest-intellij/pull/53)) - Added error reporting to GitHub issues ([#55](https://github.com/pestphp/pest-intellij/pull/55)) - Added inspection for duplicate test names in same file. ([#56](https://github.com/pestphp/pest-intellij/pull/56)) - Added completions for static and protected $this methods. ([#66](https://github.com/pestphp/pest-intellij/pull/66)) - Added completions $this fields declared in beforeEach functions. ([#66](https://github.com/pestphp/pest-intellij/pull/66)) - Added pcov coverage engine support ([#64](https://github.com/pestphp/pest-intellij/pull/64)) ### Fixed - Fixed duplicate test name error when no test name is given yet. ([#61](https://github.com/pestphp/pest-intellij/pull/61)) - Fixed finding tests with namespace at the top. ([#65](https://github.com/pestphp/pest-intellij/pull/65)) - Fixed allow location to be null in location provider. ([#68](https://github.com/pestphp/pest-intellij/pull/68)) - Fixed rerunning tests with namespaces ([#69](https://github.com/pestphp/pest-intellij/pull/69)) ## 0.3.3 ### Fixed - Fixed running with dataset ([#47](https://github.com/pestphp/pest-intellij/pull/47)) ## 0.3.2 ### Added - Added dark/light mode icons ([#45](https://github.com/pestphp/pest-intellij/pull/45)) ## 0.3.1 ### Changed - Change the name of the plugin ## 0.3.0 ### Added - Basic autocompletion for `$this` for PhpUnit TestCase base class ([#11](https://github.com/pestphp/pest-intellij/pull/11)) - Line markers now works, for the whole file and the single test. ([#17](https://github.com/pestphp/pest-intellij/pull/17), [#24](https://github.com/pestphp/pest-intellij/pull/24)) - Add running support with debugger ([#19](https://github.com/pestphp/pest-intellij/pull/19)) - `$this->field` type support for fields declared in `beforeEach` and `beforeAll` functions ([#22](https://github.com/pestphp/pest-intellij/pull/22)) - Run tests scoped based on the cursor ([#24](https://github.com/pestphp/pest-intellij/pull/24), [#26](https://github.com/pestphp/pest-intellij/pull/26)) - Added support for rerun tests when using new pest version ([#39](https://github.com/pestphp/pest-intellij/pull/39)) - Added coverage support with clover output ([#39](https://github.com/pestphp/pest-intellij/pull/39)) ### Changed - Migrated all Java classes to Kotlin ### Fixed - Plugin require restart as PhpTestFrameworkType does not support dynamic plugins - Line markers reported false positives with method calls([#31](https://github.com/pestphp/pest-intellij/pull/31)) ## 0.1.1 ### Added - Initial scaffold created from [IntelliJ Platform Plugin Template](https://github.com/JetBrains/intellij-platform-plugin-template) ================================================ FILE: CLAUDE.md ================================================ # Pest Plugin ## Mock Usage Guidelines Prefer MockK to other mocking approaches. When using MockK, prefer explicit stubbing over inline lambda syntax: ```kotlin val config = mockk() every { config.project } returns project every { config.name } returns "Test" ``` ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing to Pest IntelliJ Thank you for wanting to contribute! ## What should I know before getting started? The project is coded in Kotlin using the IntelliJ platform. The IntelliJ platform has a great wiki for documentation which is recommended to get familiar for understanding many of the things happening in this project. [plugins.jetbrains.com/docs/intellij](https://plugins.jetbrains.com/docs/intellij/welcome.html) ## How to run project locally? ================================================ FILE: LICENSE.md ================================================ MIT License Copyright (c) Oliver Nybroe olivernybroe@gmail.com 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 ================================================

GitHub Workflow Status (master) Total Downloads Latest Version Latest EAP Version official JetBrains project

# Pest IntelliJ This plugin adds support for using Pest PHP inside PHPStorm ## Installation - Using IDE built-in plugin system: Preferences > Plugins > Marketplace > Search for "Pest" > Install Plugin - Manually: Download the [latest release](https://github.com/pestphp/pest-intellij/releases/latest) and install it manually using Preferences > Plugins > ⚙️ > Install plugin from disk... - Using Early Access Program (EAP) builds: Preferences > Plugins > ⚙️ > Manage plugin repositories Add a new entry for [`https://plugins.jetbrains.com/plugins/eap/14636`](https://plugins.jetbrains.com/plugins/eap/14636) Then search for the plugin and install it as usual. ## Configuration To configure pest to run properly, you need to setup the the proper local test framework - Navigate to Preferences > Languages & Frameworks > PHP > Test Frameworks And add the following two configuration fields: Set "Path to Pest Executable" to /path/to/your/project/vendor/bin/pest Set the "Test Runner" to /path/to/your/project/phpunit.xml ## Resources For a great video course on how to write tests with Pest, check out [Testing Laravel](https://testing-laravel.com/) or [Pest From Scratch](https://laracasts.com/series/pest-from-scratch). ## Issue? Please report it to [YouTrack](https://youtrack.jetbrains.com/newIssue?project=WI) ## Credits Originally developed by [Oliver Nybroe](https://github.com/olivernybroe) --- Plugin based on the [IntelliJ Platform Plugin Template](https://github.com/JetBrains/intellij-platform-plugin-template). ================================================ FILE: build.gradle.kts ================================================ import org.jetbrains.intellij.platform.gradle.ProductMode import org.jetbrains.changelog.Changelog import org.jetbrains.changelog.markdownToHTML import org.jetbrains.intellij.platform.gradle.TestFrameworkType import org.jetbrains.kotlin.gradle.dsl.JvmTarget fun properties(key: String) = project.findProperty(key).toString() plugins { // Java support id("java") // Kotlin support id("org.jetbrains.kotlin.jvm") version "2.3.0" // Gradle IntelliJ Plugin id("org.jetbrains.intellij.platform") version "2.7.0" // Gradle Changelog Plugin id("org.jetbrains.changelog") version "2.2.0" } group = properties("pluginGroup") version = properties("pluginVersion") // Configure project's dependencies repositories { mavenCentral() intellijPlatform { defaultRepositories() } } dependencies { implementation(kotlin("stdlib")) testImplementation("io.mockk:mockk:1.14.3") { exclude("org.jetbrains.kotlinx", "kotlinx-coroutines-core-jvm") } testImplementation("junit:junit:4.13.2") intellijPlatform { val type = providers.gradleProperty("platformType") val version = providers.gradleProperty("platformVersion") create(type, version) { useInstaller = false productMode = ProductMode.MONOLITH } testFramework(TestFrameworkType.Platform) bundledPlugins(properties("platformBundledPlugins").toPlugins()) bundledModules(properties("platformBundledModules").toPlugins()) } } // Configure Gradle Changelog Plugin - read more: https://github.com/JetBrains/gradle-changelog-plugin changelog { version.set(properties("pluginVersion")) groups.set(emptyList()) } kotlin { jvmToolchain { languageVersion.set(JavaLanguageVersion.of(providers.gradleProperty("javaVersion").get())) } compilerOptions { jvmTarget.set(JvmTarget.fromTarget(providers.gradleProperty("javaVersion").get())) } } tasks { withType().configureEach { val javaVersion = properties("javaVersion") sourceCompatibility = javaVersion targetCompatibility = javaVersion } wrapper { gradleVersion = properties("gradleVersion") } patchPluginXml { version = properties("pluginVersion") sinceBuild.set(properties("pluginSinceBuild")) untilBuild.set(properties("pluginUntilBuild")) // Extract the section from README.md and provide for the plugin's manifest pluginDescription.set( projectDir.resolve("README.md").readText().lines().run { val start = "" val end = "" if (!containsAll(listOf(start, end))) { throw GradleException("Plugin description section not found in README.md:\n$start ... $end") } subList(indexOf(start) + 1, indexOf(end)) }.joinToString("\n").run { markdownToHTML(this) } ) // Get the latest available change notes from the changelog file changeNotes.set(provider { changelog.renderItem(changelog.run { getOrNull(properties("pluginVersion")) ?: getLatest() }, Changelog.OutputType.HTML) }) } signPlugin { certificateChain.set(System.getenv("CERTIFICATE_CHAIN")) privateKey.set(System.getenv("PRIVATE_KEY")) password.set(System.getenv("PRIVATE_KEY_PASSWORD")) } publishPlugin { dependsOn("patchChangelog") token.set(System.getenv("PUBLISH_TOKEN")) // pluginVersion is based on the SemVer (https://semver.org) and supports pre-release labels, like 2.1.7-alpha.3 // Specify pre-release label to publish the plugin in a custom Release Channel automatically. Read more: // https://plugins.jetbrains.com/docs/intellij/deployment.html#specifying-a-release-channel // channels = listOf(properties("pluginVersion").split('-').getOrElse(1) { "default" }.split('.').first()) } } private fun String.toPlugins(): List = split(',') .map(String::trim) .filter(String::isNotEmpty) ================================================ FILE: coverage/BUILD.bazel ================================================ ### auto-generated section `build intellij.pest.coverage` start load("@rules_jvm//:jvm.bzl", "jvm_library") jvm_library( name = "coverage", module_name = "intellij.pest.coverage", visibility = ["//visibility:public"], srcs = glob(["src/**/*.kt", "src/**/*.java", "src/**/*.form"], allow_empty = True), resources = glob(["resources/**/*"]), resource_strip_prefix = "resources", deps = [ "@lib//:kotlin-stdlib", "//phpstorm/pest", "//phpstorm/php:php-impl", "@community//plugins/coverage-common:coverage", "@community//platform/execution", "@community//platform/smRunner", "@community//platform/core-api:core", "@community//platform/util", "@community//platform/util:util-ui", "@community//platform/ide-core", "@community//platform/statistics", "@community//platform/testRunner", "//phpstorm/coverage", ] ) jvm_library( name = "coverage_test_lib", testonly = True, module_name = "intellij.pest.coverage", visibility = ["//visibility:public"], srcs = glob([], allow_empty = True), runtime_deps = [ ":coverage", "//phpstorm/pest:pest_test_lib", "//phpstorm/php:php-impl_test_lib", "@community//plugins/coverage-common:coverage_test_lib", "@community//platform/execution:execution_test_lib", "@community//platform/smRunner:smRunner_test_lib", "@community//platform/core-api:core_test_lib", "@community//platform/util:util_test_lib", "@community//platform/util:util-ui_test_lib", "@community//platform/ide-core:ide-core_test_lib", "@community//platform/statistics:statistics_test_lib", "@community//platform/lang-api:lang", "@community//platform/lang-api:lang_test_lib", "@community//platform/projectModel-impl", "@community//platform/projectModel-impl:projectModel-impl_test_lib", "@community//platform/projectModel-api:projectModel", "@community//platform/projectModel-api:projectModel_test_lib", "@community//platform/platform-util-io:ide-util-io", "@community//platform/platform-util-io:ide-util-io_test_lib", "@community//platform/testRunner:testRunner_test_lib", "//phpstorm/coverage:coverage_test_lib", ] ) ### auto-generated section `build intellij.pest.coverage` end ### auto-generated section `iml intellij.pest.coverage` start exports_files([ "intellij.pest.coverage.iml", ], visibility = ["//visibility:public"]) ### auto-generated section `iml intellij.pest.coverage` end ================================================ FILE: coverage/intellij.pest.coverage.iml ================================================ ================================================ FILE: coverage/resources/intellij.pest.coverage.xml ================================================ ================================================ FILE: coverage/src/PestCoverageEnabledConfiguration.kt ================================================ package com.intellij.pest.coverage import com.intellij.coverage.CoverageRunner import com.intellij.execution.configurations.coverage.CoverageEnabledConfiguration import com.intellij.php.coverage.PhpUnitCoverageRunner import com.pestphp.pest.configuration.PestRunConfiguration class PestCoverageEnabledConfiguration( configuration: PestRunConfiguration ) : CoverageEnabledConfiguration(configuration, CoverageRunner.getInstance(PhpUnitCoverageRunner::class.java)) { override fun coverageFileNameSeparator(): String = "@" } ================================================ FILE: coverage/src/PestCoverageEngine.kt ================================================ package com.intellij.pest.coverage import com.intellij.coverage.CoverageFileProvider import com.intellij.coverage.CoverageRunner import com.intellij.coverage.CoverageSuite import com.intellij.execution.configurations.RunConfigurationBase import com.intellij.execution.configurations.coverage.CoverageEnabledConfiguration import com.intellij.openapi.project.Project import com.intellij.php.coverage.PhpCoverageSuite import com.intellij.php.coverage.PhpUnitCoverageEngine import com.pestphp.pest.configuration.PestRunConfiguration class PestCoverageEngine : PhpUnitCoverageEngine() { override fun isApplicableTo(conf: RunConfigurationBase<*>): Boolean { return conf is PestRunConfiguration } override fun createCoverageEnabledConfiguration(conf: RunConfigurationBase<*>): CoverageEnabledConfiguration { return PestCoverageEnabledConfiguration(conf as PestRunConfiguration) } @Deprecated("Deprecated in Java") override fun createCoverageSuite( covRunner: CoverageRunner, name: String, coverageDataFileProvider: CoverageFileProvider, config: CoverageEnabledConfiguration ): CoverageSuite? { if (config is PestCoverageEnabledConfiguration) { return PhpCoverageSuite(name, config.configuration.project, covRunner, coverageDataFileProvider, config.createTimestamp()) } return null } override fun createCoverageSuite( name: String, project: Project, covRunner: CoverageRunner, coverageDataFileProvider: CoverageFileProvider, timestamp: Long, config: CoverageEnabledConfiguration ): CoverageSuite? { if (config is PestCoverageEnabledConfiguration) { return PhpCoverageSuite(name, project, covRunner, coverageDataFileProvider, timestamp) } return null } } ================================================ FILE: coverage/src/PestCoverageProgramRunner.kt ================================================ package com.intellij.pest.coverage import com.intellij.execution.configurations.RunProfile import com.intellij.execution.configurations.RunProfileState import com.intellij.execution.runners.ExecutionEnvironment import com.intellij.php.coverage.PhpCoverageRunner import com.jetbrains.php.config.commandLine.PhpCommandSettings import com.jetbrains.php.config.commandLine.PhpCommandSettingsBuilder import com.jetbrains.php.config.interpreters.PhpInterpreter import com.jetbrains.php.debug.xdebug.options.XdebugConfigurationOptionsManager import com.jetbrains.php.phpunit.coverage.PhpUnitCoverageEngine.CoverageEngine import com.jetbrains.php.run.PhpConfigurationOption import com.jetbrains.php.run.PhpRunConfigurationHolder import com.pestphp.pest.configuration.PestRunConfiguration import com.pestphp.pest.features.parallel.addParallelArguments open class PestCoverageProgramRunner : PhpCoverageRunner() { companion object { const val EXECUTOR_ID: String = "Coverage" const val RUNNER_ID: String = "PestCoverageRunner" } override fun canRun(executorId: String, profile: RunProfile): Boolean { return executorId == EXECUTOR_ID && profile is PestRunConfiguration } override fun createCoverageArguments(targetCoverage: String?): MutableList { val coverageArguments: ArrayList = ArrayList() coverageArguments.add("--coverage-clover") targetCoverage?.let { coverageArguments.add(it) } return coverageArguments } override fun getRunnerId(): String = RUNNER_ID override fun createState( env: ExecutionEnvironment, interpreter: PhpInterpreter, runConfigurationHolder: PhpRunConfigurationHolder<*>, coverageArguments: MutableList, localCoverage: String, targetCoverage: String ): RunProfileState? { val runConfiguration = runConfigurationHolder.runConfiguration as PestRunConfiguration val command = createPestCoverageCommand(runConfiguration, interpreter, coverageArguments, localCoverage, targetCoverage) return runConfiguration.checkAndGetState(env, command) } fun createPestCoverageCommand( runConfiguration: PestRunConfiguration, interpreter: PhpInterpreter, coverageArguments: List, localCoverage: String, targetCoverage: String ): PhpCommandSettings { val command = PhpCommandSettingsBuilder(runConfiguration.project, interpreter) .loadDebugExtension().build().apply { runConfiguration.applyTestArguments(this, coverageArguments) } val options = when (runConfiguration.pestSettings.pestRunnerSettings.coverageEngine) { CoverageEngine.XDEBUG -> XdebugConfigurationOptionsManager .getConfigurationOptionsProvider(runConfiguration.project, interpreter) .enableCoverage() .createXdebugConfigurations() CoverageEngine.PCOV -> listOf(PhpConfigurationOption("pcov.enabled", 1)) else -> throw IllegalArgumentException("Unsupported coverage engine.") } command.addConfigurationOptions(options) addParallelArguments(runConfiguration, command) setAdditionalMapping(localCoverage, targetCoverage, command) return command } } ================================================ FILE: coverage/src/features/mutate/PestMutateProgramRunner.kt ================================================ package com.intellij.pest.coverage.features.mutate import com.intellij.execution.configurations.RunProfile import com.intellij.execution.configurations.RunProfileState import com.intellij.execution.runners.ExecutionEnvironment import com.intellij.execution.ui.RunContentDescriptor import com.intellij.pest.coverage.PestCoverageProgramRunner import com.pestphp.pest.PestBundle import com.pestphp.pest.configuration.PestRunConfiguration import com.pestphp.pest.features.parallel.postprocessExecutionResult import com.pestphp.pest.statistics.PestUsagesCollector private val MUTATE_ARGUMENTS: MutableList = mutableListOf("--mutate") open class PestMutateProgramRunner : PestCoverageProgramRunner() { companion object { const val RUNNER_ID: String = "PestMutateRunner" } override fun doExecute(state: RunProfileState, environment: ExecutionEnvironment): RunContentDescriptor? { PestUsagesCollector.logMutationTestExecution(environment.project) val contentDescriptor = super.doExecute(state, environment) if (contentDescriptor != null) { postprocessExecutionResult(contentDescriptor, environment, PestBundle.message("MUTATION_TESTING_IS_AVAILABLE_FROM_VERSION_3")) } return contentDescriptor } override fun canRun(executorId: String, profile: RunProfile): Boolean = executorId == PestMutateTestExecutor.EXECUTOR_ID && profile is PestRunConfiguration override fun createCoverageArguments(targetCoverage: String?): MutableList = MUTATE_ARGUMENTS override fun getRunnerId(): String = RUNNER_ID } ================================================ FILE: coverage/src/features/mutate/PestMutateTestExecutor.kt ================================================ package com.intellij.pest.coverage.features.mutate import com.intellij.execution.Executor import com.intellij.icons.AllIcons import com.intellij.openapi.util.IconLoader.getDisabledIcon import com.intellij.openapi.util.text.StringUtil import com.intellij.openapi.util.text.TextWithMnemonic import com.intellij.openapi.wm.ToolWindowId import com.pestphp.pest.PestBundle import com.pestphp.pest.PestIcons import org.jetbrains.annotations.Nls import org.jetbrains.annotations.NonNls import javax.swing.Icon internal class PestMutateTestExecutor : Executor() { companion object { const val EXECUTOR_ID: @NonNls String = "PestMutateTestExecutor" const val CONTEXT_ACTION_ID: @NonNls String = "PestRunMutate" } override fun getToolWindowId(): String = ToolWindowId.RUN override fun getToolWindowIcon(): Icon = AllIcons.Toolwindows.ToolWindowRun override fun getIcon(): Icon = PestIcons.RunWithMutate override fun getRerunIcon(): Icon = AllIcons.Actions.Rerun override fun getDisabledIcon(): Icon = getDisabledIcon(icon) override fun getDescription(): String = PestBundle.message("ACTION_RUN_SELECTED_CONFIGURATION_WITH_MUTATE_DESCRIPTION") override fun getActionName(): String = PestBundle.message("ACTION_PEST_MUTATE_TEXT") override fun getId(): String = EXECUTOR_ID override fun getStartActionText(): @Nls(capitalization = Nls.Capitalization.Title) String = PestBundle.message("RUN_PEST_WITH_MUTATE") override fun getStartActionText(configurationName: String): String { val configName = if (StringUtil.isEmpty(configurationName)) "" else " '${shortenNameIfNeeded(configurationName)}'" return TextWithMnemonic.parse(PestBundle.message("RUN_S_WITH_MUTATE")).replaceFirst("%s", configName).toString() } override fun getContextActionId(): String = CONTEXT_ACTION_ID override fun getHelpId(): String? = null } ================================================ FILE: coverage/tests/BUILD.bazel ================================================ ### auto-generated section `build intellij.pest.coverage.tests` start load("@rules_jvm//:jvm.bzl", "jvm_library") jvm_library( name = "tests", module_name = "intellij.pest.coverage.tests", visibility = ["//visibility:public"], srcs = glob([], allow_empty = True), runtime_deps = ["@community//platform/ide-core"] ) jvm_library( name = "tests_test_lib", testonly = True, visibility = ["//visibility:public"], srcs = glob(["src/**/*.kt", "src/**/*.java", "src/**/*.form"], allow_empty = True), resources = glob(["testData/**/*"]), resource_strip_prefix = "testData", associates = [ "//phpstorm/pest/coverage", "//phpstorm/pest/coverage:coverage_test_lib", ], deps = [ "//phpstorm/pest:pest-tests", "//phpstorm/pest:pest-tests_test_lib", "@community//platform/lang-api:lang", "@community//platform/lang-api:lang_test_lib", "//phpstorm/php:php-impl", "//phpstorm/php:php-impl_test_lib", "//phpstorm/pest", "//phpstorm/pest:pest_test_lib", "@community//platform/core-api:core", "@community//platform/core-api:core_test_lib", "@community//platform/execution", "@community//platform/execution:execution_test_lib", "@community//platform/projectModel-api:projectModel", "@community//platform/projectModel-api:projectModel_test_lib", "@community//platform/projectModel-impl", "@community//platform/projectModel-impl:projectModel-impl_test_lib", "@community//platform/platform-util-io:ide-util-io", "@community//platform/ide-core", "@community//platform/smRunner", "@community//platform/smRunner:smRunner_test_lib", "@community//plugins/coverage-common:coverage", "@community//plugins/coverage-common:coverage_test_lib", "@community//platform/testRunner", "@community//platform/testRunner:testRunner_test_lib", "//phpstorm/coverage", "//phpstorm/coverage:coverage_test_lib", ], runtime_deps = [ ":tests", "@community//platform/platform-util-io:ide-util-io_test_lib", "@community//platform/ide-core:ide-core_test_lib", ] ) ### auto-generated section `build intellij.pest.coverage.tests` end ### auto-generated section `iml intellij.pest.coverage.tests` start exports_files([ "intellij.pest.coverage.tests.iml", ], visibility = ["//visibility:public"]) ### auto-generated section `iml intellij.pest.coverage.tests` end ### auto-generated section `test intellij.pest.coverage.tests` start load("@community//build:tests-options.bzl", "jps_test") jps_test( name = "tests_test", runtime_deps = [":tests_test_lib"] ) ### auto-generated section `test intellij.pest.coverage.tests` end ================================================ FILE: coverage/tests/intellij.pest.coverage.tests.iml ================================================ ================================================ FILE: coverage/tests/src/com/intellij/pest/coverage/PestCoverageProgramRunnerTest.kt ================================================ package com.intellij.pest.coverage import com.intellij.coverage.CoverageDataManager import com.intellij.coverage.CoverageHelper import com.intellij.execution.PsiLocation import com.intellij.execution.actions.ConfigurationContext import com.intellij.psi.PsiElement import com.intellij.testFramework.TestDataPath import com.intellij.testFramework.fixtures.IdeaTestExecutionPolicy import com.jetbrains.php.config.interpreters.PhpInterpreter import com.jetbrains.php.config.interpreters.PhpInterpretersManagerImpl import com.jetbrains.php.testFramework.PhpTestFrameworkConfiguration import com.jetbrains.php.testFramework.PhpTestFrameworkSettingsManager import com.pestphp.pest.PestFrameworkType import com.pestphp.pest.PestLightCodeFixture import com.pestphp.pest.configuration.PestRunConfiguration import com.pestphp.pest.configuration.PestRunConfigurationProducer import java.nio.file.Path import kotlin.io.path.exists import kotlin.io.path.pathString @TestDataPath($$"$CONTENT_ROOT/testData") class PestCoverageProgramRunnerTest : PestLightCodeFixture() { private lateinit var configurationsBackup: List override fun getTestDataPath(): String { val intellijPath = Path.of(IdeaTestExecutionPolicy.getHomePathWithPolicy(), "phpstorm/pest/coverage/tests/testData") return if (intellijPath.exists()) { intellijPath.pathString } else { "testData" } } fun testCannotRunWrongExecutorId() = doTest { val configuration = createConfiguration(myFixture.file) assertFalse(PestCoverageProgramRunner().canRun(PestCoverageProgramRunner.EXECUTOR_ID + "1", configuration)) } fun testCanRunFile() = doTest { val configuration = createConfiguration(myFixture.file) assertTrue(PestCoverageProgramRunner().canRun(PestCoverageProgramRunner.EXECUTOR_ID, configuration)) } fun testCanRunFunction() = doTest { val testElement = myFixture.file?.firstChild?.lastChild?.firstChild ?: return@doTest val configuration = createConfiguration(testElement) assertTrue(PestCoverageProgramRunner().canRun(PestCoverageProgramRunner.EXECUTOR_ID, configuration)) } fun testCanRunDirectory() = doTest { val testElement = myFixture.file?.containingDirectory ?: return@doTest val configuration = createConfiguration(testElement) assertTrue(PestCoverageProgramRunner().canRun(PestCoverageProgramRunner.EXECUTOR_ID, configuration)) } fun testBuildFile() = doTest { val configuration = createConfiguration(myFixture.file) val pestCoverageCommandSettings = PestCoverageProgramRunner().createPestCoverageCommand(configuration, configuration.interpreter!!, emptyList(), "", "") val expected = "-dxdebug.coverage_enable=1 -dxdebug.mode=coverage randomPath --teamcity /src/ATest.php" assertEquals(expected, pestCoverageCommandSettings.createGeneralCommandLine().parametersList.list.joinToString(" ")) } fun testBuildFunction() = doTest { val testElement = myFixture.file?.firstChild?.lastChild?.firstChild ?: return@doTest val configuration = createConfiguration(testElement) val pestCoverageCommandSettings = PestCoverageProgramRunner().createPestCoverageCommand(configuration, configuration.interpreter!!, emptyList(), "", "") val expected = "-dxdebug.coverage_enable=1 -dxdebug.mode=coverage randomPath --teamcity /src/ATest.php" assertEquals( expected, pestCoverageCommandSettings.createGeneralCommandLine().parametersList.list .joinToString(" ") .substringBefore(" --filter") ) } fun testBuildDirectory() = doTest { val testElement = myFixture.file?.containingDirectory ?: return@doTest val configuration = createConfiguration(testElement) val pestCoverageCommandSettings = PestCoverageProgramRunner().createPestCoverageCommand(configuration, configuration.interpreter!!, emptyList(), "", "") val expected = "-dxdebug.coverage_enable=1 -dxdebug.mode=coverage randomPath --teamcity /src" assertEquals(expected, pestCoverageCommandSettings.createGeneralCommandLine().parametersList.list.joinToString(" ")) } fun testBuildFileWithEnabledParallelTesting() = doTest { val configuration = createConfiguration(myFixture.file) configuration.pestSettings.pestRunnerSettings.parallelTestingEnabled = true val pestCoverageCommandSettings = PestCoverageProgramRunner().createPestCoverageCommand(configuration, configuration.interpreter!!, emptyList(), "", "") val expected = "-dxdebug.coverage_enable=1 -dxdebug.mode=coverage randomPath --teamcity /src/ATest.php --parallel --log-teamcity php://stdout" assertEquals(expected, pestCoverageCommandSettings.createGeneralCommandLine().parametersList.list.joinToString(" ")) } fun testCreateCoverageSuiteOnRunningCoverageTests() = doTest { val configuration = createConfiguration(myFixture.file) CoverageHelper.resetCoverageSuit(configuration) assertSize(1, CoverageDataManager.getInstance(project).getSuites()) } private fun createConfiguration(psiElement: PsiElement): PestRunConfiguration { createPestFrameworkConfiguration() val context = ConfigurationContext.createEmptyContextForLocation(PsiLocation.fromPsiElement(psiElement)) val runConfiguration = PestRunConfigurationProducer().createConfigurationFromContext(context)?.configuration as? PestRunConfiguration runConfiguration!!.settings.commandLineSettings.interpreterSettings.interpreterName = getTestName(false) return runConfiguration } private fun doTest(block: () -> Unit) { myFixture.configureByFile("ATest.php") block() } override fun setUp() { super.setUp() val interpreter = PhpInterpreter().apply { name = getTestName(false) homePath = "$testDataPath/php" } PhpInterpretersManagerImpl.getInstance(project).addInterpreter(interpreter) configurationsBackup = PhpTestFrameworkSettingsManager.getInstance(project).getConfigurations(PestFrameworkType.Companion.instance) } override fun tearDown() { try { PhpTestFrameworkSettingsManager.getInstance(project).setConfigurations(PestFrameworkType.Companion.instance, configurationsBackup) PhpInterpretersManagerImpl.getInstance(project).interpreters = emptyList() } catch (e: Throwable) { addSuppressedException(e) } finally { super.tearDown() } } } ================================================ FILE: coverage/tests/src/com/intellij/pest/coverage/features/mutate/PestMutateProgramRunnerTest.kt ================================================ package com.intellij.pest.coverage.features.mutate import com.intellij.execution.PsiLocation import com.intellij.execution.actions.ConfigurationContext import com.intellij.psi.PsiElement import com.intellij.testFramework.TestDataPath import com.intellij.testFramework.fixtures.IdeaTestExecutionPolicy import com.jetbrains.php.config.interpreters.PhpInterpreter import com.jetbrains.php.config.interpreters.PhpInterpretersManagerImpl import com.jetbrains.php.testFramework.PhpTestFrameworkConfiguration import com.jetbrains.php.testFramework.PhpTestFrameworkSettingsManager import com.pestphp.pest.PestFrameworkType import com.pestphp.pest.PestLightCodeFixture import com.pestphp.pest.configuration.PestRunConfiguration import com.pestphp.pest.configuration.PestRunConfigurationProducer import java.nio.file.Path import kotlin.io.path.exists import kotlin.io.path.pathString @TestDataPath($$"$CONTENT_ROOT/testData/features/mutate") class PestMutateProgramRunnerTest : PestLightCodeFixture() { private lateinit var configurationsBackup: List override fun getTestDataPath(): String { val intellijPath = Path.of(IdeaTestExecutionPolicy.getHomePathWithPolicy(), "phpstorm/pest/coverage/tests/testData/features/mutate") return if (intellijPath.exists()) { intellijPath.pathString } else { "testData/features/mutate" } } fun testCannotRunWrongExecutorId() = doTest { val configuration = createConfiguration(myFixture.file) assertFalse(PestMutateProgramRunner().canRun(PestMutateTestExecutor.EXECUTOR_ID + "1", configuration)) } fun testCanRunFile() = doTest { val configuration = createConfiguration(myFixture.file) assertTrue(PestMutateProgramRunner().canRun(PestMutateTestExecutor.EXECUTOR_ID, configuration)) } fun testCanRunFunction() = doTest { val testElement = myFixture.file?.firstChild?.lastChild?.firstChild ?: return@doTest val configuration = createConfiguration(testElement) assertTrue(PestMutateProgramRunner().canRun(PestMutateTestExecutor.EXECUTOR_ID, configuration)) } fun testCanRunDirectory() = doTest { val testElement = myFixture.file?.containingDirectory ?: return@doTest val configuration = createConfiguration(testElement) assertTrue(PestMutateProgramRunner().canRun(PestMutateTestExecutor.EXECUTOR_ID, configuration)) } private fun createConfiguration(psiElement: PsiElement): PestRunConfiguration { createPestFrameworkConfiguration() val context = ConfigurationContext.createEmptyContextForLocation(PsiLocation.fromPsiElement(psiElement)) val runConfiguration = PestRunConfigurationProducer().createConfigurationFromContext(context)?.configuration as? PestRunConfiguration runConfiguration!!.settings.commandLineSettings.interpreterSettings.interpreterName = getTestName(false) return runConfiguration } private fun doTest(block: () -> Unit) { myFixture.configureByFile("ATest.php") block() } override fun setUp() { super.setUp() val interpreter = PhpInterpreter().apply { name = getTestName(false) homePath = "$testDataPath/php" } PhpInterpretersManagerImpl.getInstance(project).addInterpreter(interpreter) configurationsBackup = PhpTestFrameworkSettingsManager.getInstance(project).getConfigurations(PestFrameworkType.instance) } override fun tearDown() { try { PhpTestFrameworkSettingsManager.getInstance(project).setConfigurations(PestFrameworkType.instance, configurationsBackup) PhpInterpretersManagerImpl.getInstance(project).interpreters = emptyList() } catch (e: Throwable) { addSuppressedException(e) } finally { super.tearDown() } } } ================================================ FILE: coverage/tests/testData/ATest.php ================================================ https://plugins.jetbrains.com/docs/intellij/intellij-artifacts.html pluginGroup = com.pestphp pluginName = PEST PHP pluginVersion = 1.12.0 # See https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html # for insight into build numbers and IntelliJ Platform versions. pluginSinceBuild = 232.10072.32 pluginUntilBuild = platformType = PS platformVersion = LATEST-EAP-SNAPSHOT platformDownloadSources = true # https://plugins.jetbrains.com/plugin/6610-php/versions platformBundledPlugins = com.jetbrains.php platformBundledModules = intellij.platform.coverage # Java language level used to compile sources and to generate the files for - Java 11 is required since 2020.3 javaVersion = 21 gradleVersion = 8.13 # Opt-out flag for bundling Kotlin standard library. # See https://plugins.jetbrains.com/docs/intellij/kotlin.html#kotlin-standard-library for details. # suppress inspection "UnusedProperty" kotlin.stdlib.default.dependency = false org.gradle.jvmargs=-Xmx2048m ================================================ FILE: gradlew ================================================ #!/bin/sh # # Copyright © 2015-2021 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ############################################################################## # # Gradle start up script for POSIX generated by Gradle. # # Important for running: # # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is # noncompliant, but you have some other compliant shell such as ksh or # bash, then to run this script, type that shell name before the whole # command line, like: # # ksh Gradle # # Busybox and similar reduced shells will NOT work, because this script # requires all of these POSIX shell features: # * functions; # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», # «${var#prefix}», «${var%suffix}», and «$( cmd )»; # * compound commands having a testable exit status, especially «case»; # * various built-in commands including «command», «set», and «ulimit». # # Important for patching: # # (2) This script targets any POSIX shell, so it avoids extensions provided # by Bash, Ksh, etc; in particular arrays are avoided. # # The "traditional" practice of packing multiple parameters into a # space-separated string is a well documented source of bugs and security # problems, so this is (mostly) avoided, by progressively accumulating # options in "$@", and eventually passing that to Java. # # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; # see the in-line comments for details. # # There are tweaks for specific operating systems such as AIX, CygWin, # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. # ############################################################################## # Attempt to set APP_HOME # Resolve links: $0 may be a link app_path=$0 # Need this for daisy-chained symlinks. while APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path [ -h "$app_path" ] do ls=$( ls -ld "$app_path" ) link=${ls#*' -> '} case $link in #( /*) app_path=$link ;; #( *) app_path=$APP_HOME$link ;; esac done APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit APP_NAME="Gradle" APP_BASE_NAME=${0##*/} # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum warn () { echo "$*" } >&2 die () { echo echo "$*" echo exit 1 } >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false case "$( uname )" in #( CYGWIN* ) cygwin=true ;; #( Darwin* ) darwin=true ;; #( MSYS* | MINGW* ) msys=true ;; #( NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD=$JAVA_HOME/jre/sh/java else JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else JAVACMD=java which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi # Increase the maximum file descriptors if we can. if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac fi # Collect all arguments for the java command, stacking in reverse order: # * args from the command line # * the main class name # * -classpath # * -D...appname settings # * --module-path (only if needed) # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. # For Cygwin or MSYS, switch paths to Windows format before running java if "$cygwin" || "$msys" ; then APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) JAVACMD=$( cygpath --unix "$JAVACMD" ) # Now convert the arguments - kludge to limit ourselves to /bin/sh for arg do if case $arg in #( -*) false ;; # don't mess with options #( /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath [ -e "$t" ] ;; #( *) false ;; esac then arg=$( cygpath --path --ignore --mixed "$arg" ) fi # Roll the args list around exactly as many times as the number of # args, so each arg winds up back in the position where it started, but # possibly modified. # # NB: a `for` loop captures its iteration list before it begins, so # changing the positional parameters here affects neither the number of # iterations, nor the values presented in `arg`. shift # remove old arg set -- "$@" "$arg" # push replacement arg done fi # Collect all arguments for the java command; # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of # shell script including quotes and variable substitutions, so put them in # double quotes to make sure that they get re-expanded; and # * put everything else in single quotes, so that it's not re-expanded. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ -classpath "$CLASSPATH" \ org.gradle.wrapper.GradleWrapperMain \ "$@" # Use "xargs" to parse quoted args. # # With -n1 it outputs one arg per line, with the quotes and backslashes removed. # # In Bash we could simply go: # # readarray ARGS < <( xargs -n1 <<<"$var" ) && # set -- "${ARGS[@]}" "$@" # # but POSIX shell has neither arrays nor command substitution, so instead we # post-process each arg (as a line of input to sed) to backslash-escape any # character that might be a shell metacharacter, then use eval to reverse # that process (while maintaining the separation between arguments), and wrap # the whole thing up as a single "set" statement. # # This will of course break if any of these variables contains a newline or # an unmatched quote. # eval "set -- $( printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | xargs -n1 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | tr '\n' ' ' )" '"$@"' exec "$JAVACMD" "$@" ================================================ FILE: gradlew.bat ================================================ @rem @rem Copyright 2015 the original author or authors. @rem @rem Licensed under the Apache License, Version 2.0 (the "License"); @rem you may not use this file except in compliance with the License. @rem You may obtain a copy of the License at @rem @rem https://www.apache.org/licenses/LICENSE-2.0 @rem @rem Unless required by applicable law or agreed to in writing, software @rem distributed under the License is distributed on an "AS IS" BASIS, @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @rem @rem ########################################################################## @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Resolve any "." and ".." in APP_HOME to make it shorter. for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if "%ERRORLEVEL%" == "0" goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* :end @rem End local scope for the variables with windows NT shell if "%ERRORLEVEL%"=="0" goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 exit /b 1 :mainEnd if "%OS%"=="Windows_NT" endlocal :omega ================================================ FILE: intellij.pest.iml ================================================ ================================================ FILE: intellij.pest.tests.iml ================================================ ================================================ FILE: plugin-content.yaml ================================================ - name: lib/modules/intellij.pest.coverage.jar contentModules: - name: intellij.pest.coverage - name: lib/pest.jar modules: - name: intellij.pest ================================================ FILE: settings.gradle.kts ================================================ rootProject.name = "pest-intellij" pluginManagement { repositories { maven { url = uri("https://oss.sonatype.org/content/repositories/snapshots/") } gradlePluginPortal() } } ================================================ FILE: src/main/java/com/pestphp/pest/PestIcons.java ================================================ package com.pestphp.pest; import com.intellij.ui.IconManager; import org.jetbrains.annotations.NotNull; import javax.swing.*; /** * NOTE THIS FILE IS AUTO-GENERATED * DO NOT EDIT IT BY HAND, run "Generate icon classes" configuration instead */ public final class PestIcons { private static @NotNull Icon load(@NotNull String path, int cacheKey, int flags) { return IconManager.getInstance().loadRasterizedIcon(path, PestIcons.class.getClassLoader(), cacheKey, flags); } /** 16x16 */ public static final @NotNull Icon Config = load("config.svg", 710701582, 2); /** 16x16 */ public static final @NotNull Icon Dataset = load("dataset.svg", -1986461428, 2); /** 16x16 */ public static final @NotNull Icon File = load("file.svg", -1158724446, 2); /** 16x16 */ public static final @NotNull Icon Logo = load("logo.svg", -2116012898, 2); public static final class METAINF { /** 16x16 */ public static final @NotNull Icon PluginIcon = load("META-INF/pluginIcon.svg", 1914567053, 0); } /** 16x16 */ public static final @NotNull Icon Run = load("run.svg", -452832596, 2); /** 16x16 */ public static final @NotNull Icon RunWithMutate = load("runWithMutate.svg", 226904416, 2); } ================================================ FILE: src/main/java/com/pestphp/pest/configuration/PestRunConfigurationSettings.java ================================================ package com.pestphp.pest.configuration; import com.intellij.util.xmlb.annotations.Property; import com.intellij.util.xmlb.annotations.Transient; import com.jetbrains.php.testFramework.run.PhpTestRunConfigurationSettings; import com.jetbrains.php.testFramework.run.PhpTestRunnerSettings; import org.jetbrains.annotations.NotNull; public class PestRunConfigurationSettings extends PhpTestRunConfigurationSettings { @Override protected @NotNull PestRunnerSettings createDefault() { return new PestRunnerSettings(); } @Property(surroundWithTag = false) public @NotNull PestRunnerSettings getPestRunnerSettings() { final PhpTestRunnerSettings settings = super.getRunnerSettings(); if (settings instanceof PestRunnerSettings) { return (PestRunnerSettings)settings; } final PestRunnerSettings copy = PestRunnerSettings.fromPhpTestRunnerSettings(settings); setPestRunnerSettings(copy); return copy; } public void setPestRunnerSettings(PestRunnerSettings runnerSettings) { setRunnerSettings(runnerSettings); } /** * @deprecated Use {@link #getPestRunnerSettings()} **/ @Deprecated @Transient @Override public @NotNull PhpTestRunnerSettings getRunnerSettings() { return super.getRunnerSettings(); } } ================================================ FILE: src/main/java/com/pestphp/pest/configuration/PhpTestRunConfiguration.java ================================================ package com.pestphp.pest.configuration; import com.intellij.execution.BeforeRunTask; import com.intellij.execution.configurations.ConfigurationFactory; import com.intellij.openapi.project.Project; import com.jetbrains.php.PhpTestFrameworkVersionDetector; import com.jetbrains.php.testFramework.PhpTestFrameworkType; import com.jetbrains.php.testFramework.run.PhpTestRunConfigurationHandler; import com.jetbrains.php.testFramework.run.PhpTestRunnerSettingsValidator; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.List; public abstract class PhpTestRunConfiguration extends com.jetbrains.php.testFramework.run.PhpTestRunConfiguration { protected PhpTestRunConfiguration(Project project, ConfigurationFactory factory, String name, @NotNull PhpTestFrameworkType frameworkType, @NotNull PhpTestRunnerSettingsValidator validator, @NotNull PhpTestRunConfigurationHandler handler, @Nullable PhpTestFrameworkVersionDetector versionDetector) { super(project, factory, name, frameworkType, validator, handler, versionDetector); } @Override public void setBeforeRunTasks(@NotNull List> value) { super.setBeforeRunTasks(value); } } ================================================ FILE: src/main/kotlin/com/pestphp/pest/FileUtil.kt ================================================ package com.pestphp.pest import com.intellij.psi.PsiFile import com.intellij.testFramework.LightVirtualFile /** * Takes care of getting the path of a file even if it's a light file. */ val PsiFile.realPath: String get() { var virtualFile = this.viewProvider.virtualFile if (virtualFile is LightVirtualFile && virtualFile.originalFile != null) { virtualFile = virtualFile.originalFile } return virtualFile.path } ================================================ FILE: src/main/kotlin/com/pestphp/pest/PestBundle.kt ================================================ package com.pestphp.pest import com.intellij.DynamicBundle import org.jetbrains.annotations.NonNls import org.jetbrains.annotations.PropertyKey import java.util.function.Supplier @NonNls private const val BUNDLE = "messages.pestBundle" object PestBundle : DynamicBundle(BUNDLE) { @Suppress("SpreadOperator") @JvmStatic fun message(@PropertyKey(resourceBundle = BUNDLE) key: String, vararg params: Any) = getMessage(key, *params) @JvmStatic fun messagePointer(@PropertyKey(resourceBundle = BUNDLE) key: String, vararg params: Any): Supplier { return getLazyMessage(key, *params) } } ================================================ FILE: src/main/kotlin/com/pestphp/pest/PestComposerConfig.kt ================================================ package com.pestphp.pest import com.intellij.execution.configurations.ConfigurationType import com.jetbrains.php.testFramework.PhpTestFrameworkComposerConfig import com.pestphp.pest.configuration.PestRunConfigurationType.Companion.instance class PestComposerConfig : PhpTestFrameworkComposerConfig(PestFrameworkType.instance, PACKAGE, RELATIVE_PATH) { override fun getDefaultConfigName(): String { return "phpunit.xml" } override fun getConfigurationType(): ConfigurationType { return instance } companion object { private const val PACKAGE = "pestphp/pest" private const val RELATIVE_PATH = "pestphp/pest/bin/pest" } } ================================================ FILE: src/main/kotlin/com/pestphp/pest/PestFrameworkType.kt ================================================ package com.pestphp.pest import com.intellij.openapi.project.Project import com.jetbrains.php.testFramework.PhpTestDescriptor import com.jetbrains.php.testFramework.PhpTestFrameworkFormDecorator import com.jetbrains.php.testFramework.PhpTestFrameworkFormDecorator.PhpDownloadableTestFormDecorator import com.jetbrains.php.testFramework.PhpTestFrameworkType import com.jetbrains.php.testFramework.ui.PhpTestFrameworkBaseConfigurableForm import com.jetbrains.php.testFramework.ui.PhpTestFrameworkBySdkConfigurableForm import com.jetbrains.php.testFramework.ui.PhpTestFrameworkConfigurableForm import com.pestphp.pest.configuration.PestVersionDetector import org.jetbrains.annotations.Nls import org.jetbrains.annotations.NonNls import javax.swing.Icon /** * Registers a framework type for PHP. * * This class is used to show the menu in * `Preferences -> Languages & Frameworks -> PHP -> Test Frameworks` */ class PestFrameworkType : PhpTestFrameworkType() { private val pestUrl = "https://github.com/pestphp/pest/releases" override fun getDisplayName(): @Nls String { return PestBundle.message("FRAMEWORK_NAME") } override fun getID(): String { return ID } override fun getIcon(): Icon { return PestIcons.Logo } override fun getDecorator(): PhpTestFrameworkFormDecorator { return object : PhpDownloadableTestFormDecorator(pestUrl) { override fun decorate( project: Project, form: PhpTestFrameworkBaseConfigurableForm<*> ): PhpTestFrameworkConfigurableForm<*> { if (form !is PhpTestFrameworkBySdkConfigurableForm) { form.setVersionDetector(PestVersionDetector.instance) } return super.decorate(project, form) } } } override fun getDescriptor(): PhpTestDescriptor { return PestTestDescriptor } companion object { @NonNls val ID = "Pest" val instance: PhpTestFrameworkType get() = getTestFrameworkType(ID) } } ================================================ FILE: src/main/kotlin/com/pestphp/pest/PestFunctionsUtil.kt ================================================ package com.pestphp.pest import com.intellij.psi.PsiElement import com.intellij.psi.PsiFile import com.jetbrains.php.PhpIndex import com.jetbrains.php.lang.psi.elements.FieldReference import com.jetbrains.php.lang.psi.elements.Function import com.jetbrains.php.lang.psi.elements.FunctionReference import com.jetbrains.php.lang.psi.elements.GroupStatement import com.jetbrains.php.lang.psi.elements.MethodReference import com.jetbrains.php.lang.psi.elements.PhpExpression import com.jetbrains.php.lang.psi.elements.PhpNamespace import com.jetbrains.php.lang.psi.elements.PhpPsiElement import com.jetbrains.php.lang.psi.elements.Statement import com.jetbrains.php.lang.psi.elements.impl.FunctionReferenceImpl import com.jetbrains.php.lang.psi.resolve.types.PhpType val PEST_TEST_CALL_TYPE = PhpType.from( "\\Pest\\PendingCalls\\TestCall", // for Pest versions >= 2.x "\\Pest\\PendingObjects\\TestCall" // for Pest versions 1.x ) fun PsiElement?.isPestTestReference(isSmart: Boolean = false): Boolean { return when (this) { null -> false is MethodReference -> this.isPestTestMethodReference(isSmart) is FunctionReferenceImpl -> this.isPestTestFunction(isSmart) else -> false } } private val testNames = setOf("it", "test", "todo", "describe", "arch") fun FunctionReferenceImpl.isPestTestFunction(isSmart: Boolean = false): Boolean { if (this.canonicalText !in testNames) return false return !isSmart || (this.resolveLocal().isEmpty() && PhpIndex.getInstance(project).getFunctionsByName(this.canonicalText).any { function -> PEST_TEST_CALL_TYPE.isConvertibleFromGlobal(project, function.type) }) } fun FunctionReferenceImpl.isPestBeforeFunction(): Boolean { return this.canonicalText == "beforeEach" } fun FunctionReferenceImpl.isPestAfterFunction(): Boolean { return this.canonicalText == "afterEach" } private val allPestNames = setOf("it", "test", "todo", "beforeEach", "afterEach", "dataset", "describe", "arch") fun FunctionReferenceImpl.isAnyPestFunction(): Boolean { return this.canonicalText in allPestNames } fun FunctionReferenceImpl.isDescribeFunction(): Boolean { return this.canonicalText == "describe" } fun MethodReference.isPestTestMethodReference(isSmart: Boolean = false): Boolean { return when (val reference = classReference) { is FunctionReferenceImpl -> reference.isPestTestFunction(isSmart) is MethodReference -> reference.isPestTestMethodReference(isSmart) is FieldReference -> reference.isPestTestMethodReference(isSmart) else -> false } } fun FieldReference.isPestTestMethodReference(isSmart: Boolean = false): Boolean { return when (val reference = classReference) { is FunctionReferenceImpl -> reference.isPestTestFunction(isSmart) is MethodReference -> reference.isPestTestMethodReference(isSmart) is FieldReference -> reference.isPestTestMethodReference(isSmart) else -> false } } fun PsiFile.getRoot(): List { val element = this.firstChild return element.children.filterIsInstance() .mapNotNull { it.statements } .getOrElse( 0 ) { element } .children .filterIsInstance() .mapNotNull { it.firstChild } } /** * Traverses elements and recursively enters describe blocks, collecting items via the collector function. */ internal fun collectFromDescribeBlocks( elements: List, collector: (PhpPsiElement) -> T? ): List { val result = mutableListOf() for (element in elements) { collector(element)?.let { result.add(it) } val funcRef = element as? FunctionReferenceImpl if (funcRef != null && funcRef.isDescribeFunction()) { val closure = (funcRef.parameters.getOrNull(1) as? PhpExpression)?.firstChild as? Function val body = closure?.children?.filterIsInstance()?.firstOrNull() val statements = body?.statements?.mapNotNull { it.firstChild }?.filterIsInstance() ?: emptyList() result.addAll(collectFromDescribeBlocks(statements, collector)) } } return result } fun PsiFile.getPestTests(isSmart: Boolean = false): Set { return collectFromDescribeBlocks(this.getRootPhpPsiElements()) { element -> if (element.isPestTestReference(isSmart)) element as? FunctionReference else null }.toSet() } ================================================ FILE: src/main/kotlin/com/pestphp/pest/PestIconProvider.kt ================================================ package com.pestphp.pest import com.intellij.ide.FileIconProvider import com.intellij.openapi.project.DumbService import com.intellij.openapi.project.Project import com.intellij.openapi.vfs.VirtualFile import com.intellij.psi.PsiManager import com.jetbrains.php.lang.psi.PhpFile import com.pestphp.pest.features.datasets.isIndexedPestDatasetFile import javax.swing.Icon class PestIconProvider : FileIconProvider { override fun getIcon(vFile: VirtualFile, flags: Int, project: Project?): Icon? { if (project == null || DumbService.isDumb(project)) return null val file = PsiManager.getInstance(project).findFile(vFile) as? PhpFile ?: return null if (file.isIndexedPestTestFile()) { return PestIcons.File } if (file.isIndexedPestDatasetFile()) { return PestIcons.Dataset } if (file.isPestFile()) { return PestIcons.Logo } return null } } ================================================ FILE: src/main/kotlin/com/pestphp/pest/PestNamingUtil.kt ================================================ package com.pestphp.pest import com.intellij.psi.PsiElement import com.intellij.psi.util.findParentOfType import com.intellij.psi.util.parents import com.intellij.remote.RemoteSdkProperties import com.jetbrains.php.config.interpreters.PhpInterpretersManagerImpl import com.jetbrains.php.lang.psi.elements.ArrayCreationExpression import com.jetbrains.php.lang.psi.elements.ClassConstantReference import com.jetbrains.php.lang.psi.elements.ClassReference import com.jetbrains.php.lang.psi.elements.ConcatenationExpression import com.jetbrains.php.lang.psi.elements.FunctionReference import com.jetbrains.php.lang.psi.elements.MethodReference import com.jetbrains.php.lang.psi.elements.ParameterListOwner import com.jetbrains.php.lang.psi.elements.PhpPsiElement import com.jetbrains.php.lang.psi.elements.PhpReference import com.jetbrains.php.lang.psi.elements.Statement import com.jetbrains.php.lang.psi.elements.StringLiteralExpression import com.jetbrains.php.lang.psi.elements.impl.FunctionReferenceImpl import com.jetbrains.php.run.remote.PhpRemoteInterpreterManager import com.jetbrains.php.util.pathmapper.PhpPathMapper import com.pestphp.pest.runner.getLocationUrl import java.util.Locale fun FunctionReferenceImpl.getPestTestName(): String? { val testName = getParameter(0)?.stringValue ?: return tryGetArchTestName(this) val parent = this.findParentOfType() val prepend = if (parent is FunctionReferenceImpl && parent.isDescribeFunction()) { parent.getPestTestName() } else { "" } return when (this.canonicalText) { "it" -> "${prepend}it $testName" "describe" -> "${prepend}`$testName` → " else -> "${prepend}$testName" } } private fun tryGetArchTestName(functionReference: FunctionReference): String? = if (functionReference.canonicalText == "arch") { getArchTestName(functionReference) } else { null } private fun getArchTestName(functionReference: FunctionReference): String { val parents = functionReference.parents(false).takeWhile { it !is Statement }.toList() return parents.joinToString(separator = " → ") { element -> val name = if (element is PhpReference) element.canonicalText else element.text val parameters = if (element is ParameterListOwner) getParametersString(element) else "" "$name$parameters" } } private fun getParametersString(element: ParameterListOwner) = " " + when (val elem = element.parameters.firstOrNull()) { is ArrayCreationExpression -> elem.children.filterIsInstance().joinToString(prefix = "[", postfix = "]") { it.text } is StringLiteralExpression -> elem.text else -> "" }.replace("\"", "'") val PsiElement.stringValue: String? get() = when (this) { is StringLiteralExpression -> this.contents is ConcatenationExpression -> this.contents is ClassConstantReference -> { val classRef = this.classReference if (classRef is ClassReference && this.isStatic && this.lastChild.text == "class") { classRef.fqn ?.removePrefix("\\") ?.replace("\\", "\\\\") } else null } else -> null } val ConcatenationExpression.contents: String? get() { val left = this.leftOperand?.stringValue val right = this.rightOperand?.stringValue if (left === null || right === null) { return null } return left + right } fun PsiElement?.getPestTestName(): String? { return when (this) { is MethodReference -> (this.classReference as? FunctionReference)?.getPestTestName() is FunctionReferenceImpl -> this.getPestTestName() else -> null } } fun PsiElement?.getInitialFunctionReference(): FunctionReference? { return when (this) { is MethodReference -> (this.classReference as? FunctionReference).getInitialFunctionReference() is FunctionReferenceImpl -> this else -> null } } fun PsiElement.toPestTestRegex(workingDirectory: String): String? { return this.getPestTestName()?.toPestTestRegex( workingDirectory, this.containingFile.virtualFile.path, PhpPathMapper.create(this.project) ) } fun PsiElement.toPestFqn(): List { val testName = this.getPestTestName() ?: return emptyList() val file = this.containingFile.virtualFile.path return PhpInterpretersManagerImpl.getInstance(this.project) .interpreters .asSequence() .map { it.phpSdkAdditionalData } .filter { it is RemoteSdkProperties } .mapNotNull { PhpRemoteInterpreterManager.getInstance()?.createPathMappings( this.project, it ) } .map { it.convertToRemote(file) } .map { "pest_qn://$it::$testName" } .plus("${getLocationUrl(this.containingFile)}::$testName") .toList() } fun String.toPestTestRegex(rootPath: String, file: String, pathMapper: PhpPathMapper): String { val mappedWorkingDirectory = pathMapper.getRemoteFilePath(rootPath) ?: rootPath val mappedFile = pathMapper.getRemoteFilePath(file) ?: file // Follow the steps for class name generation // 1. Take the path of the PEST file from the cwd. val fqn = mappedFile.withoutFirstFileSeparator .removePrefix(mappedWorkingDirectory.withoutFirstFileSeparator) .withoutFirstFileSeparator // 2. Make the first folder's first letter uppercase. .replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() } // 3. Remove file extension (.php) and compound suffixes (.test, .spec, etc.) from filename. .removeSuffix(".php") .let { path -> truncateAtFirstDot(path) } // 4. Make directory separators to namespace separators. .replace("\\", "\\\\") .replace("/", "\\\\") // 5. Remove unsupported characters (keep only alphanumeric and escaped backslashes). .replace(Regex("[^A-Za-z0-9\\\\]"), "") // 6. Add P as a namespace before the generated namespace. .let { "(P\\\\)?$it" } // Allow substring matching only for "describe" block execution val possibleEndOfLine = if (this.endsWith(" → ")) "" else "$" // Escape characters val testName = this .replace(" ", "\\s") .replace("(", "\\(") .replace(")", "\\)") .replace("[", "\\[") .replace("]", "\\]") .replace("^", "\\^") .replace("/", "\\/") .replace("?", "\\?") .replace("+", "\\+") // Match the description of a single data set val dataSet = """(data\sset\s".*"|\(.*\))""" return """^$fqn::$testName(\swith\s$dataSet(\s\/\s$dataSet)*(\s#\d+)?)?$possibleEndOfLine""" } private fun truncateAtFirstDot(path: String): String { val lastSep = maxOf(path.lastIndexOf('/'), path.lastIndexOf('\\')) return if (lastSep >= 0) { val dir = path.substring(0, lastSep + 1) val filename = path.substring(lastSep + 1).substringBefore('.') dir + filename } else { path.substringBefore('.') } } val String.withoutFirstFileSeparator: String get() { return this.removePrefix("/").removePrefix("\\") } ================================================ FILE: src/main/kotlin/com/pestphp/pest/PestNewTestFromClassAction.kt ================================================ package com.pestphp.pest import com.intellij.openapi.project.Project import com.intellij.openapi.vfs.VirtualFile import com.jetbrains.php.phpunit.codeGeneration.PhpNewTestAction import com.jetbrains.php.testFramework.PhpTestCreateInfo open class PestNewTestFromClassAction: PhpNewTestAction(PestBundle.messagePointer("action.Pest.New.File.text"), PestBundle.messagePointer("ACTIONS_NEW_PEST_TEST_ACTION_DESCRIPTION"), PestIcons.Logo) { override fun getDefaultTestCreateInfo(project: Project, locationContext: VirtualFile?): PhpTestCreateInfo { return PestTestCreateInfo } } ================================================ FILE: src/main/kotlin/com/pestphp/pest/PestPluginDisposable.kt ================================================ package com.pestphp.pest import com.intellij.openapi.Disposable import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.components.Service import com.intellij.openapi.project.Project /** * The service is intended to be used instead of a project/application as a parent disposable. */ @Service(Service.Level.APP, Service.Level.PROJECT) class PestPluginDisposable : Disposable { override fun dispose() {} companion object { @JvmStatic fun getInstance(): Disposable = ApplicationManager.getApplication().getService(PestPluginDisposable::class.java) @JvmStatic fun getInstance(project: Project): Disposable = project.getService(PestPluginDisposable::class.java) } } ================================================ FILE: src/main/kotlin/com/pestphp/pest/PestSettings.kt ================================================ package com.pestphp.pest import com.intellij.openapi.components.PersistentStateComponent import com.intellij.openapi.components.Service import com.intellij.openapi.components.State import com.intellij.openapi.components.Storage import com.intellij.openapi.components.service import com.intellij.openapi.project.Project import com.intellij.openapi.vfs.VirtualFile import com.intellij.util.xmlb.XmlSerializerUtil import com.pestphp.pest.parser.PestConfigurationFile import com.pestphp.pest.parser.PestConfigurationFileParser @Service(Service.Level.PROJECT) @State(name = "PestSettings", storages = [Storage("pest.xml")]) class PestSettings : PersistentStateComponent { var pestFilePath = "tests/Pest.php" var preferredTestFlavor = TestFlavor.IT enum class TestFlavor { IT, TEST } override fun getState(): PestSettings { return this } override fun loadState(state: PestSettings) { XmlSerializerUtil.copyBean(state, this) } private val parser = PestConfigurationFileParser(this) fun getPestConfiguration(project: Project, virtualFile: VirtualFile? = null): PestConfigurationFile { return parser.parse(project, virtualFile) } companion object { fun getInstance(project: Project): PestSettings { return project.service() } } } ================================================ FILE: src/main/kotlin/com/pestphp/pest/PestTestCreateInfo.kt ================================================ package com.pestphp.pest import com.intellij.openapi.project.Project import com.intellij.psi.PsiFile import com.intellij.psi.util.PsiTreeUtil import com.jetbrains.php.lang.psi.elements.FunctionReference import com.jetbrains.php.testFramework.PhpUnitAbstractTestCreateInfo import com.pestphp.pest.inspections.convertTestNameToSentenceCase import javax.swing.Icon const val INTERNAL_PEST_FILE_TEMPLATE_NAME = "Pest file from class" object PestTestCreateInfo : PhpUnitAbstractTestCreateInfo() { override fun getName(): String { return "Pest" } override fun getTemplateName(): String { return INTERNAL_PEST_FILE_TEMPLATE_NAME } override fun getIcon(): Icon { return PestIcons.Logo } override fun getTestMethodText(project: Project, classFqn: String, methodName: String): String { return "test('${convertTestNameToSentenceCase(methodName)}', function(){})" } override fun shouldPostprocessTemplateFile(): Boolean { return true } override fun postprocessTemplateFile(file: PsiFile) { val test = PsiTreeUtil.findChildOfType(file, FunctionReference::class.java) test?.parent?.delete() } } ================================================ FILE: src/main/kotlin/com/pestphp/pest/PestTestDescriptor.kt ================================================ package com.pestphp.pest import com.intellij.util.SmartList import com.jetbrains.php.lang.psi.elements.Method import com.jetbrains.php.lang.psi.elements.PhpClass import com.jetbrains.php.phpunit.PhpUnitTestDescriptor import com.jetbrains.php.testFramework.PhpTestCreateInfo import java.util.Collections /** * findTests, findClasses, and findMethods return empty collections, * since Pest tests are function calls, not methods, and therefore are not located in PHP classes */ object PestTestDescriptor : PhpUnitTestDescriptor() { override fun findTests(clazz: PhpClass): MutableCollection { return Collections.emptySet() } override fun findTests(method: Method): MutableCollection { return Collections.emptySet() } override fun findClasses(test: PhpClass, testName: String): MutableCollection { return Collections.emptySet() } override fun findMethods(testMethod: Method): MutableCollection { return Collections.emptySet() } override fun getTestCreateInfos(): MutableCollection { return SmartList(PestTestCreateInfo) } } ================================================ FILE: src/main/kotlin/com/pestphp/pest/PestTestFileUtil.kt ================================================ package com.pestphp.pest import com.intellij.openapi.util.Key import com.intellij.psi.PsiElement import com.intellij.psi.PsiFile import com.intellij.psi.util.CachedValue import com.intellij.psi.util.CachedValueProvider import com.intellij.psi.util.CachedValuesManager import com.intellij.psi.util.PsiTreeUtil import com.jetbrains.php.lang.psi.elements.AssignmentExpression import com.jetbrains.php.lang.psi.elements.ClassConstantReference import com.jetbrains.php.lang.psi.elements.ClassReference import com.jetbrains.php.lang.psi.elements.FieldReference import com.jetbrains.php.lang.psi.elements.Function import com.jetbrains.php.lang.psi.elements.FunctionReference import com.jetbrains.php.lang.psi.elements.MethodReference import com.jetbrains.php.lang.psi.elements.ParameterList import com.jetbrains.php.lang.psi.elements.Variable import com.jetbrains.php.lang.psi.elements.impl.FunctionReferenceImpl import com.jetbrains.php.lang.psi.resolve.types.PhpType val CONFIGURATION_FUNCTIONS = listOf("pest", "uses") val CONFIGURATION_METHODS = listOf("use", "uses", "extend", "extends") fun PsiElement?.isThisVariableInPest(condition: (FunctionReferenceImpl) -> Boolean): Boolean { if ((this as? Variable)?.name != "this") return false var psiElement = this while (true) { val functionReference = getOuterFunctionReference(psiElement) ?: return false if (condition(functionReference)) { return true } psiElement = functionReference } } fun PsiElement?.isTestAsThisVariableInPest(condition: (FunctionReferenceImpl) -> Boolean): Boolean { val functionReference = this as? FunctionReference ?: return false if (functionReference.name != "test" || !functionReference.parameters.isEmpty()) return false return getOuterFunctionReference(this)?.let { functionReferenceImpl -> condition(functionReferenceImpl) } ?: false } private fun getOuterFunctionReference(element: PsiElement?): FunctionReferenceImpl? { val closure = PsiTreeUtil.getParentOfType(element, Function::class.java) if (closure == null || !closure.isClosure) return null val parameterList = closure.parent?.parent as? ParameterList ?: return null if (parameterList.parent !is FunctionReferenceImpl) return null return parameterList.parent as FunctionReferenceImpl } fun PsiFile.getAllBeforeThisAssignments(): List { return this.getRoot() .filterIsInstance() .filter { it.isPestBeforeFunction() } .flatMap { it.getThisStatements() } } private val cacheKey = Key>>("com.pestphp.pest_assignments") private fun FunctionReferenceImpl.getThisStatements(): List { return CachedValuesManager.getCachedValue(this, cacheKey) { val result = PsiTreeUtil.findChildrenOfType( this.parameterList?.getParameter(0), AssignmentExpression::class.java ) .filter { ((it.variable as? FieldReference)?.classReference as? Variable)?.name == "this" } CachedValueProvider.Result.create(result, this) } } fun FunctionReference.getConfigurationPhpType(): PhpType? { if (this.name !in CONFIGURATION_METHODS) return PhpType() parameters.mapNotNull { val classRef = it as? ClassConstantReference ?: return@mapNotNull null if (classRef.name != "class") return@mapNotNull null (classRef.classReference as? ClassReference)?.fqn }.apply { if (this.isEmpty()) return null val res = PhpType() this.forEach { res.add(it) } return res } } fun FunctionReference.getPestConfigurationPhpType(): PhpType? { if (this is FunctionReferenceImpl && this.name in CONFIGURATION_FUNCTIONS) { return this.getConfigurationPhpType() } val classReference = (this as? MethodReference)?.classReference ?: return null if (classReference is FunctionReference) { val typeFromClassRef = classReference.getPestConfigurationPhpType() val typeFromParameters = this.getConfigurationPhpType() return PhpType().add(typeFromParameters).add(typeFromClassRef) } return null } ================================================ FILE: src/main/kotlin/com/pestphp/pest/PestTestRunLineMarkerProvider.kt ================================================ package com.pestphp.pest import com.intellij.execution.lineMarker.RunLineMarkerContributor import com.intellij.icons.AllIcons.RunConfigurations.TestState.Run import com.intellij.icons.AllIcons.RunConfigurations.TestState.Run_run import com.intellij.openapi.project.Project import com.intellij.psi.PsiElement import com.jetbrains.php.lang.lexer.PhpTokenTypes import com.jetbrains.php.lang.psi.PhpFile import com.jetbrains.php.lang.psi.PhpPsiUtil import com.jetbrains.php.lang.psi.elements.FunctionReference import com.jetbrains.php.lang.psi.elements.Statement import com.jetbrains.php.lang.psi.elements.impl.FunctionReferenceImpl /** * Adds markers on the line on the left side for running a pest specific pest test. */ class PestTestRunLineMarkerProvider : RunLineMarkerContributor() { override fun getInfo(leaf: PsiElement): Info? { if (!leaf.containingFile.isPestTestFile(isSmart = true)) { return null } // Handle icons if the reference is a pest test. if (isPestTestReference(leaf)) { return getPestTest( leaf.parent as FunctionReferenceImpl, leaf.project, ) } // Handle icon for running all tests in the file. if (PhpPsiUtil.isOfType(leaf, PhpTokenTypes.PHP_OPENING_TAG)) { return withExecutorActions(Run_run) } return null } private fun isPestTestReference(leaf: PsiElement): Boolean { if (PhpPsiUtil.isOfType(leaf, PhpTokenTypes.IDENTIFIER)) { (leaf.parent as? FunctionReferenceImpl)?.let { functionReference -> if (!functionReference.isAnyPestFunction()) { return false } val statementChild = PhpPsiUtil.getParentOfClass(functionReference, true, Statement::class.java)?.firstChild val outerFunctionReference = PhpPsiUtil.getParentByCondition( statementChild, { it is FunctionReferenceImpl }, PhpFile.INSTANCEOF ) if (outerFunctionReference == null || outerFunctionReference.isDescribeFunction()) { return statementChild is FunctionReference && statementChild.isPestTestReference(isSmart = true) } } } return false } private fun getPestTest(reference: FunctionReferenceImpl, project: Project): Info { val fqn = reference.toPestFqn() val icon = fqn.firstOrNull { getTestStateIcon(it, project, false) !== Run } ?.let { getTestStateIcon(it, project, false) } ?: Run return withExecutorActions(icon) } } ================================================ FILE: src/main/kotlin/com/pestphp/pest/PestUtil.kt ================================================ package com.pestphp.pest import com.intellij.openapi.project.Project import com.intellij.openapi.project.guessProjectDir import com.intellij.openapi.util.Key import com.intellij.openapi.util.text.StringUtil import com.intellij.openapi.vfs.VirtualFile import com.intellij.psi.PsiFile import com.intellij.psi.search.ProjectScope import com.intellij.psi.util.CachedValue import com.intellij.psi.util.CachedValueProvider import com.intellij.psi.util.CachedValuesManager import com.intellij.util.indexing.FileBasedIndex import com.jetbrains.php.composer.configData.ComposerConfigManager import com.jetbrains.php.lang.psi.PhpExpressionCodeFragment import com.jetbrains.php.lang.psi.PhpFile import com.jetbrains.php.lang.psi.elements.PhpNamespace import com.jetbrains.php.lang.psi.elements.PhpPsiElement import com.jetbrains.php.lang.psi.elements.Statement import com.jetbrains.php.phpunit.PhpUnitUtil import com.jetbrains.php.testFramework.PhpTestFrameworkSettingsManager import com.pestphp.pest.indexers.key val PEST_TEST_FILE_KEY = Key>("isPestTestFile") val PEST_TEST_FILE_SMART_KEY = Key>("smart isPestTestFile") fun PsiFile.isPestTestFile(isSmart: Boolean = false): Boolean { if (this !is PhpFile || this is PhpExpressionCodeFragment) return false return CachedValuesManager.getCachedValue(this, if (isSmart) PEST_TEST_FILE_SMART_KEY else PEST_TEST_FILE_KEY) { val isPestTestFile = this.getRootPhpPsiElements().any { psiElement -> psiElement.isPestTestReference(isSmart) } CachedValueProvider.Result.create(isPestTestFile, this) } } fun PsiFile.isIndexedPestTestFile(): Boolean { return FileBasedIndex.getInstance().getValues( key, this.realPath, ProjectScope.getProjectScope(this.project) ).isNotEmpty() && this.isPestTestFile(isSmart = true) } fun PsiFile.isPestConfigurationFile(): Boolean { return PhpUnitUtil.isPhpUnitConfigurationFile(this) } fun Project.isPestEnabled(): Boolean { return PhpTestFrameworkSettingsManager .getInstance(this) .getConfigurations(PestFrameworkType.instance) .any { StringUtil.isNotEmpty(it.executablePath) } } fun PsiFile.getRootPhpPsiElements(): List { if (this !is PhpFile) return listOf() val element = this.firstChild return element.children.filterIsInstance() .mapNotNull { it.statements } .getOrElse( 0 ) { element } .children .filterIsInstance() .mapNotNull { it.firstChild } .filterIsInstance() } /** * Checks if the file is the `tests/Pest.php` file. */ fun PsiFile.isPestFile(): Boolean { val baseDir = getBaseDir(this.project, this.virtualFile) ?: return false val pestFilePath = PestSettings.getInstance(this.project).pestFilePath return this.virtualFile?.path == baseDir.path + "/" + pestFilePath } fun getBaseDir(project: Project, virtualFile: VirtualFile? = null): VirtualFile? { return ComposerConfigManager.getInstance(project).getConfig(virtualFile)?.parent ?: project.guessProjectDir() } ================================================ FILE: src/main/kotlin/com/pestphp/pest/annotator/PestAnnotator.kt ================================================ package com.pestphp.pest.annotator import com.intellij.codeInsight.daemon.impl.HighlightRangeExtension import com.intellij.lang.annotation.AnnotationHolder import com.intellij.lang.annotation.Annotator import com.intellij.psi.PsiElement import com.intellij.psi.PsiFile import com.jetbrains.php.lang.psi.resolve.types.PhpParameterBasedTypeProvider class PestAnnotator: Annotator, HighlightRangeExtension { override fun annotate(element: PsiElement, holder: AnnotationHolder) { if (PhpParameterBasedTypeProvider.isMeta(holder.currentAnnotationSession.file)) return element.accept(PestAnnotatorVisitor(holder)) } override fun isForceHighlightParents(psiFile: PsiFile): Boolean { return false } } ================================================ FILE: src/main/kotlin/com/pestphp/pest/annotator/PestAnnotatorVisitor.kt ================================================ package com.pestphp.pest.annotator import com.intellij.lang.annotation.AnnotationHolder import com.intellij.lang.annotation.HighlightSeverity import com.intellij.modcommand.ModCommandAction import com.intellij.psi.search.GlobalSearchScope import com.intellij.util.indexing.FileBasedIndex import com.jetbrains.php.lang.annotator.PhpAnnotatorVisitor.createErrorAnnotation import com.jetbrains.php.lang.annotator.PhpDeleteElementQuickFix import com.jetbrains.php.lang.inspections.controlFlow.PhpNavigateToElementQuickFix import com.jetbrains.php.lang.psi.PhpFile import com.jetbrains.php.lang.psi.PhpPsiUtil import com.jetbrains.php.lang.psi.elements.FunctionReference import com.jetbrains.php.lang.psi.elements.MethodReference import com.jetbrains.php.lang.psi.elements.impl.MethodReferenceImpl import com.jetbrains.php.lang.psi.visitors.PhpElementVisitor import com.pestphp.pest.PestBundle import com.pestphp.pest.features.customExpectations.KEY import com.pestphp.pest.features.customExpectations.extendName import com.pestphp.pest.getPestTestName import com.pestphp.pest.getPestTests class PestAnnotatorVisitor( private val holder: AnnotationHolder ) : PhpElementVisitor() { override fun visitPhpMethodReference(reference: MethodReference) { checkDuplicateCustomExpectations(reference) } private fun checkDuplicateCustomExpectations(reference: MethodReference) { if (reference !is MethodReferenceImpl) { return } val extendName = reference.extendName ?: return val duplicates = mutableListOf() FileBasedIndex.getInstance().processValues(KEY, extendName, null, { file, offsets -> reference.manager.findFile(file)?.let { psiFile -> offsets.forEach { offset -> val expectation = psiFile.findElementAt(offset) val methodDescriptor = PhpPsiUtil.getParentOfClass(expectation, MethodReference::class.java) if (methodDescriptor != null) { duplicates.add(methodDescriptor) } } } true }, GlobalSearchScope.allScope(reference.project)) if (duplicates.size > 1) { val fixes = listOfNotNull( PhpDeleteElementQuickFix(reference.parent, PestBundle.message("QUICK_FIX_DELETE_CUSTOM_EXPECTATION", extendName)), getNavigateToCustomExpectationFix(duplicates, reference) ) val builder = holder.newAnnotation( HighlightSeverity.WARNING, PestBundle.message("INSPECTION_DUPLICATE_CUSTOM_EXPECTATION") ).range(reference) fixes.forEach { fix -> builder.withFix(fix.asIntention()) } builder.create() } } private fun getNavigateToCustomExpectationFix( duplicates: List, duplicate: MethodReference ): ModCommandAction? { val duplicateIndex = duplicates.indexOf(duplicate) if (duplicateIndex == -1) { return null } val nextElement = duplicates[(duplicateIndex + 1) % duplicates.size] return PhpNavigateToElementQuickFix( nextElement, PestBundle.message("INTENTION_NAVIGATE_TO_DUPLICATE_CUSTOM_EXPECTATION") ) } override fun visitPhpFile(phpFile: PhpFile) { checkDuplicateTestNames(phpFile) } private fun checkDuplicateTestNames(file: PhpFile) { file.getPestTests(isSmart = true) .groupBy { it.getPestTestName() } .filterKeys { it != null } .filter { it.value.count() > 1 } .forEach { (_, tests) -> tests.forEachIndexed { index, test -> val testName = test.getPestTestName() ?: return@forEachIndexed createErrorAnnotation(holder, test, PestBundle.message("INSPECTION_DUPLICATE_TEST_NAME"), PhpDeleteElementQuickFix(test.parent, PestBundle.message("QUICK_FIX_DELETE_TEST", testName)), getNavigateToTestNameFix(tests, index)) } } } private fun getNavigateToTestNameFix(duplicates: List, duplicateIndex: Int): ModCommandAction { val nextElement = duplicates[(duplicateIndex + 1) % duplicates.size] return PhpNavigateToElementQuickFix( nextElement, PestBundle.message("INTENTION_NAVIGATE_TO_DUPLICATE_TEST_NAME") ) } } ================================================ FILE: src/main/kotlin/com/pestphp/pest/completion/InternalMembersCompletionProvider.kt ================================================ package com.pestphp.pest.completion import com.intellij.codeInsight.completion.CompletionParameters import com.intellij.codeInsight.completion.CompletionProvider import com.intellij.codeInsight.completion.CompletionResultSet import com.intellij.util.ProcessingContext import com.jetbrains.php.PhpIndex import com.jetbrains.php.completion.PhpVariantsUtil import com.jetbrains.php.lang.psi.elements.FieldReference import com.jetbrains.php.lang.psi.elements.Variable import com.pestphp.pest.isAnyPestFunction import com.pestphp.pest.isThisVariableInPest /** * Adds completion for private and protected methods of `$this` variable * when inside a pest test. */ class InternalMembersCompletionProvider : CompletionProvider() { override fun addCompletions( parameters: CompletionParameters, context: ProcessingContext, result: CompletionResultSet ) { val fieldReference = parameters.position.parent as? FieldReference ?: return val variable = fieldReference.classReference as? Variable ?: return if (!variable.isThisVariableInPest { it.isAnyPestFunction() }) return val phpIndex = PhpIndex.getInstance(fieldReference.project) val classes = phpIndex.completeType(fieldReference.project, variable.type, null).types .filter { it.startsWith("\\") } .flatMap { phpIndex.getAnyByFQN(it) } classes.flatMap { phpClass -> phpClass.methods.filter { it.access.isProtected || (!it.access.isPrivate && it.isStatic) } }.forEach { result.addElement(PhpVariantsUtil.getLookupItem(it, null)) } classes.flatMap { phpClass -> phpClass.fields.filter { it.modifier.isProtected } }.forEach { result.addElement(PhpVariantsUtil.getLookupItem(it, null)) } } } ================================================ FILE: src/main/kotlin/com/pestphp/pest/completion/PestCompletionContributor.kt ================================================ package com.pestphp.pest.completion import com.intellij.codeInsight.completion.CompletionContributor import com.intellij.codeInsight.completion.CompletionType import com.intellij.patterns.PlatformPatterns import com.jetbrains.php.lang.lexer.PhpTokenTypes import com.jetbrains.php.lang.psi.elements.FieldReference import com.jetbrains.php.lang.psi.elements.MemberReference /** * Registers the completion providers */ private class PestCompletionContributor : CompletionContributor() { init { extend( CompletionType.BASIC, PlatformPatterns.psiElement() .withElementType(PhpTokenTypes.IDENTIFIER) .withParent(FieldReference::class.java), InternalMembersCompletionProvider() ) extend( CompletionType.BASIC, PlatformPatterns.psiElement() .withElementType(PhpTokenTypes.IDENTIFIER) .withParent(FieldReference::class.java), ThisFieldsCompletionProvider() ) extend( CompletionType.BASIC, PlatformPatterns.psiElement() .withParent(MemberReference::class.java), PestCustomExtensionCompletionProvider() ) } } ================================================ FILE: src/main/kotlin/com/pestphp/pest/completion/PestCustomExtensionCompletionProvider.kt ================================================ @file:Suppress("UnstableApiUsage") package com.pestphp.pest.completion import com.intellij.codeInsight.AutoPopupController import com.intellij.codeInsight.completion.CompletionParameters import com.intellij.codeInsight.completion.CompletionProvider import com.intellij.codeInsight.completion.CompletionResultSet import com.intellij.codeInsight.lookup.LookupElement import com.intellij.codeInsight.lookup.LookupElementBuilder import com.intellij.codeInsight.lookup.LookupElementPresentation import com.intellij.codeInsight.lookup.LookupElementRenderer import com.intellij.psi.PsiFile import com.intellij.psi.search.GlobalSearchScope import com.intellij.util.ProcessingContext import com.intellij.util.indexing.FileBasedIndex import com.jetbrains.php.PhpIcons import com.jetbrains.php.completion.PhpCompletionUtil import com.jetbrains.php.completion.insert.PhpInsertHandlerUtil import com.jetbrains.php.lang.psi.PhpPsiUtil import com.jetbrains.php.lang.psi.elements.MemberReference import com.jetbrains.php.lang.psi.elements.MethodReference import com.jetbrains.php.lang.psi.resolve.types.PhpType import com.pestphp.pest.features.customExpectations.KEY import com.pestphp.pest.features.customExpectations.expectationType import com.pestphp.pest.features.customExpectations.toMethod class PestCustomExtensionCompletionProvider : CompletionProvider() { override fun addCompletions(parameters: CompletionParameters, context: ProcessingContext, result: CompletionResultSet) { val memberReference = parameters.position.originalElement.parent as? MemberReference ?: return val project = parameters.position.project if (PhpType.intersectsGlobal(project, expectationType, memberReference.classReference?.globalType ?: PhpType.EMPTY)) { val index = FileBasedIndex.getInstance() index.getAllKeys(KEY, project).forEach { extensionName -> index.processValues(KEY, extensionName, null, { file, value -> memberReference.manager.findFile(file)?.let { psiFile -> val cheapRenderer = CustomExpectationRenderer() val lookupElement = LookupElementBuilder.create(extensionName) .withRenderer(cheapRenderer) .withExpensiveRenderer(CustomExtensionExpensiveRenderer(cheapRenderer, psiFile, value.first())) .withInsertHandler { context, _ -> val expectation = psiFile.findElementAt(value.first()) val methodDescriptor = PhpPsiUtil.getParentOfClass(expectation, MethodReference::class.java)?.toMethod() PhpInsertHandlerUtil.insertStringAtCaret(context.editor, "()") if (methodDescriptor?.parameters?.isNotEmpty() == true) { PhpCompletionUtil.moveCaretRelativelyWithScroll(context.editor, -1) AutoPopupController.getInstance(project).autoPopupParameterInfo(context.editor, null) } } result.addElement(lookupElement) } true }, GlobalSearchScope.projectScope(project)) } } } } private class CustomExpectationRenderer : LookupElementRenderer() { override fun renderElement(element: LookupElement, presentation: LookupElementPresentation) { presentation.icon = PhpIcons.METHOD presentation.itemText = element.lookupString presentation.isItemTextBold = true } } private class CustomExtensionExpensiveRenderer( private val cheapRenderer: CustomExpectationRenderer, private val targetFile: PsiFile, private val expectationOffset: Int ) : LookupElementRenderer() { override fun renderElement(element: LookupElement, presentation: LookupElementPresentation) { cheapRenderer.renderElement(element, presentation) val expectation = targetFile.findElementAt(expectationOffset) PhpPsiUtil.getParentOfClass(expectation, MethodReference::class.java)?.toMethod()?.let { val params = it.parameters.map { p -> if (p.returnType.isEmpty) { p.name } else { "${p.name}: ${p.returnType}" } } presentation.tailText = "(${params.joinToString(", ")})" presentation.typeText = it.returnType.global(targetFile.project).toString() } } } ================================================ FILE: src/main/kotlin/com/pestphp/pest/completion/ThisFieldsCompletionProvider.kt ================================================ package com.pestphp.pest.completion import com.intellij.codeInsight.completion.CompletionParameters import com.intellij.codeInsight.completion.CompletionProvider import com.intellij.codeInsight.completion.CompletionResultSet import com.intellij.codeInsight.lookup.LookupElementBuilder import com.intellij.codeInsight.navigation.actions.GotoDeclarationHandler import com.intellij.openapi.editor.Editor import com.intellij.psi.PsiElement import com.intellij.psi.util.elementType import com.intellij.util.ProcessingContext import com.jetbrains.php.PhpIcons import com.jetbrains.php.lang.lexer.PhpTokenTypes import com.jetbrains.php.lang.psi.elements.FieldReference import com.jetbrains.php.lang.psi.elements.Variable import com.pestphp.pest.getAllBeforeThisAssignments import com.pestphp.pest.isAnyPestFunction import com.pestphp.pest.isThisVariableInPest /** * Adds completion for variable assignments from `beforeEach` when using `$this` * inside a pest test. */ internal class ThisFieldsCompletionProvider : CompletionProvider(), GotoDeclarationHandler { override fun addCompletions( parameters: CompletionParameters, context: ProcessingContext, result: CompletionResultSet ) { val fieldReference = parameters.position.parent as? FieldReference ?: return val variable = fieldReference.classReference as? Variable ?: return if (!variable.isThisVariableInPest { it.isAnyPestFunction() }) return return (fieldReference.containingFile).getAllBeforeThisAssignments() .filter { it.variable?.name !== null } .forEach { result.addElement( LookupElementBuilder.create(it.variable!!.name!!) .withIcon(PhpIcons.FIELD) .withTypeText(it.type.toStringRelativized("\\")) ) } } override fun getGotoDeclarationTargets( sourceElement: PsiElement?, offset: Int, editor: Editor? ): Array { if (sourceElement?.elementType != PhpTokenTypes.IDENTIFIER) { return PsiElement.EMPTY_ARRAY } val fieldReference = sourceElement?.parent as? FieldReference ?: return PsiElement.EMPTY_ARRAY if (fieldReference.classReference?.isThisVariableInPest { it.isAnyPestFunction() } != true) { return PsiElement.EMPTY_ARRAY } return (fieldReference.containingFile ?: return PsiElement.EMPTY_ARRAY).getAllBeforeThisAssignments() .filter { it.variable?.name == fieldReference.name } .mapNotNull { it.variable } .toTypedArray() } } ================================================ FILE: src/main/kotlin/com/pestphp/pest/configuration/PestDebugRunner.kt ================================================ package com.pestphp.pest.configuration import com.jetbrains.php.testFramework.run.PhpTestDebugRunner /** * Add support for Php's debug runners. */ class PestDebugRunner private constructor() : PhpTestDebugRunner(PestRunConfiguration::class.java) { override fun getRunnerId(): String { return "PestDebugRunner" } } ================================================ FILE: src/main/kotlin/com/pestphp/pest/configuration/PestLocationProvider.kt ================================================ package com.pestphp.pest.configuration import com.intellij.execution.Location import com.intellij.execution.testframework.sm.runner.SMTestLocator import com.intellij.openapi.project.Project import com.intellij.openapi.vfs.VirtualFile import com.intellij.psi.PsiElement import com.intellij.psi.PsiManager import com.intellij.psi.search.GlobalSearchScope import com.jetbrains.php.lang.psi.PhpFile import com.jetbrains.php.phpunit.PhpPsiLocationWithDataSet import com.jetbrains.php.phpunit.PhpUnitQualifiedNameLocationProvider import com.jetbrains.php.util.pathmapper.PhpLocalPathMapper import com.jetbrains.php.util.pathmapper.PhpPathMapper import com.pestphp.pest.features.parallel.convertRuntimeTestNameToRealTestName import com.pestphp.pest.getPestTestName import com.pestphp.pest.getPestTests import com.pestphp.pest.runner.LocationInfo import java.io.File private const val PARALLEL_EXECUTION_URL_MARKER = "eval()'d code::" /** * Adds support for goto test from test results. */ class PestLocationProvider( val pathMapper: PhpPathMapper, private val project: Project, private val configurationFileRootPath: String? = null ) : SMTestLocator { private val phpUnitLocationProvider = PhpUnitQualifiedNameLocationProvider.create(pathMapper) override fun getLocation( protocol: String, path: String, project: Project, scope: GlobalSearchScope ): MutableList> { val isParallelExecution = path.contains(PARALLEL_EXECUTION_URL_MARKER) if (protocol != PROTOCOL_ID && !isParallelExecution) { return phpUnitLocationProvider.getLocation(protocol, path, project, scope) } val locationInfo = if (isParallelExecution) getParallelLocationInfo(path) else getLocationInfo(path) val element = locationInfo?.let { findElement(it, project) } ?: return mutableListOf() return mutableListOf( PhpPsiLocationWithDataSet( project, element, getDataSet(locationInfo) ) ) } private fun getDataSet(locationInfo: LocationInfo): String? { return locationInfo.testName } private fun getLocationInfo(link: String): LocationInfo? { val location = link.split("::") return resolveLocationInfo(location) } private fun getParallelLocationInfo(link: String): LocationInfo? { val rawParallelLocation = link.substringAfter(PARALLEL_EXECUTION_URL_MARKER).split("::") val location = listOfNotNull( convertLocationHintClassNameToFileName(rawParallelLocation[0]), rawParallelLocation.getOrNull(1)?.let { runtimeTestName -> convertRuntimeTestNameToRealTestName(runtimeTestName) } ) return resolveLocationInfo(location) } private fun resolveLocationInfo(location: List): LocationInfo? { val fileUrl = calculateFileUrl(location[0]) val testName = location.getOrNull(1) val file = this.pathMapper.getLocalFile(fileUrl) ?: PhpLocalPathMapper(project).getLocalFile(fileUrl) return file?.let { LocationInfo(it, testName) } } private fun findElement(locationInfo: LocationInfo, project: Project): PsiElement? { return this.getLocation( project, locationInfo.file, locationInfo.testName ) } private fun getLocation(project: Project, virtualFile: VirtualFile, testName: String?): PsiElement? { val file = PsiManager.getInstance(project).findFile(virtualFile) ?: return null if (testName == null) { return file } return (file as PhpFile).getPestTests().firstOrNull { it.getPestTestName() == testName } } override fun getLocation( stacktraceLine: String, project: Project, scope: GlobalSearchScope ): MutableList> { return mutableListOf() } fun calculateFileUrl(locationOutput: String): String { val pathPrefix = configurationFileRootPath ?: project.basePath return if (pathPrefix != null && locationOutput.startsWith(pathPrefix)) { locationOutput // for Pest versions 1.x } else { "$pathPrefix/${locationOutput}" // for Pest versions >= 2.x } } companion object { const val PROTOCOL_ID = "pest_qn" } } private fun convertLocationHintClassNameToFileName(locationHintClassName: String): String { return locationHintClassName.removePrefix("\\P\\").replace("\\", File.separator) + ".php" } ================================================ FILE: src/main/kotlin/com/pestphp/pest/configuration/PestRerunFailedTestsAction.kt ================================================ package com.pestphp.pest.configuration import com.intellij.execution.Executor import com.intellij.execution.configurations.RunProfileState import com.intellij.execution.runners.ExecutionEnvironment import com.intellij.execution.runners.ProgramRunner import com.intellij.execution.testframework.actions.AbstractRerunFailedTestsAction import com.intellij.execution.testframework.sm.runner.SMTRunnerConsoleProperties import com.intellij.openapi.ui.ComponentContainer import com.intellij.psi.PsiElement import com.intellij.psi.search.GlobalSearchScope import com.jetbrains.php.composer.configData.ComposerConfigManager import com.jetbrains.php.config.commandLine.PhpCommandSettings import com.pestphp.pest.PestBundle import com.pestphp.pest.features.parallel.PestParallelProgramRunner import com.pestphp.pest.isPestTestReference import com.pestphp.pest.notifications.OutdatedNotification import com.pestphp.pest.toPestTestRegex /** * Adds support for rerunning failed tests */ class PestRerunFailedTestsAction( componentContainer: ComponentContainer, properties: SMTRunnerConsoleProperties ) : AbstractRerunFailedTestsAction(componentContainer) { override fun getRunProfile(environment: ExecutionEnvironment): MyRunProfile? { val profile = myConsoleProperties.configuration if (profile !is PestRunConfiguration) { return null } val runConfiguration: PestRunConfiguration = profile return object : MyRunProfile(runConfiguration), PestRerunProfile { override fun getState(executor: Executor, environment: ExecutionEnvironment): RunProfileState? { val peerRunConfiguration = this.peer as PestRunConfiguration val project = peerRunConfiguration.project val interpreter = peerRunConfiguration.interpreter ?: return null val failed = getFailedTests(project) .asSequence() .filter { it.isLeaf } .filter { it.parent != null } .map { it.getLocation(project, GlobalSearchScope.allScope(project)) } .mapNotNull { it?.psiElement } .filter { it.isPestTestReference() } .toList() val clone: PestRunConfiguration = peerRunConfiguration.clone() as PestRunConfiguration // If there are no failed tests found, it's prob. // because it's an pest version before the new printer if (failed.isEmpty()) { OutdatedNotification().notify( project, PestBundle.message("NO_FAILED_TESTS_FOUND") ) return peerRunConfiguration.getState( environment, clone.createCommand( interpreter, mutableMapOf(), mutableListOf(), false ), null ) } val command: PhpCommandSettings = clone.createCommand( interpreter, mutableMapOf(), getArgumentsFromRunner(environment.runner), false ) val rootPath = ComposerConfigManager.getInstance(project).getConfig(null as PsiElement?)?.parent?.path ?: command.workingDirectory val testcases = failed.mapNotNull { it.toPestTestRegex(rootPath) } .reduce { result, testName -> "$result|$testName" } command.addArgument( "--filter=/$testcases/" ) return peerRunConfiguration.getState( environment, command, null ) } } } init { init(properties) } private fun getArgumentsFromRunner(pestProgramRunner: ProgramRunner<*>): MutableList { return when (pestProgramRunner) { is PestParallelProgramRunner -> pestProgramRunner.getArguments() else -> mutableListOf() } } } ================================================ FILE: src/main/kotlin/com/pestphp/pest/configuration/PestRerunProfile.kt ================================================ package com.pestphp.pest.configuration interface PestRerunProfile ================================================ FILE: src/main/kotlin/com/pestphp/pest/configuration/PestRunConfiguration.kt ================================================ package com.pestphp.pest.configuration import com.intellij.codeInsight.completion.CompletionResultSet import com.intellij.codeInsight.lookup.LookupElementBuilder import com.intellij.execution.ExecutionException import com.intellij.execution.Executor import com.intellij.execution.configurations.ConfigurationFactory import com.intellij.execution.configurations.RunConfiguration import com.intellij.execution.configurations.RunProfileState import com.intellij.execution.configurations.RuntimeConfigurationException import com.intellij.execution.configurations.RuntimeConfigurationWarning import com.intellij.execution.runners.ExecutionEnvironment import com.intellij.execution.testframework.actions.AbstractRerunFailedTestsAction import com.intellij.execution.testframework.sm.runner.SMTRunnerConsoleProperties import com.intellij.execution.ui.ConsoleView import com.intellij.openapi.options.SettingsEditor import com.intellij.openapi.project.Project import com.intellij.openapi.util.text.StringUtil import com.intellij.openapi.vfs.VfsUtil import com.intellij.util.PathUtil import com.intellij.util.TextFieldCompletionProvider import com.jetbrains.php.PhpBundle import com.jetbrains.php.config.commandLine.PhpCommandLinePathProcessor import com.jetbrains.php.config.commandLine.PhpCommandSettings import com.jetbrains.php.config.interpreters.PhpInterpreter import com.jetbrains.php.run.PhpAsyncRunConfiguration import com.jetbrains.php.run.PhpRunUtil import com.jetbrains.php.run.remote.PhpRemoteInterpreterManager import com.jetbrains.php.testFramework.PhpTestFrameworkConfiguration import com.jetbrains.php.testFramework.PhpTestFrameworkSettingsManager import com.jetbrains.php.testFramework.run.PhpTestRunConfigurationSettings import com.jetbrains.php.testFramework.run.PhpTestRunnerConfigurationEditor import com.jetbrains.php.testFramework.run.PhpTestRunnerSettings import com.pestphp.pest.PestBundle import com.pestphp.pest.PestFrameworkType import com.pestphp.pest.PestIcons import com.pestphp.pest.configuration.PestRunConfigurationProducer.Companion.VALIDATOR import com.pestphp.pest.features.parallel.addParallelArguments import com.pestphp.pest.getPestTestName import com.pestphp.pest.getPestTests import com.pestphp.pest.runner.PestConsoleProperties import java.util.EnumMap import kotlin.io.path.Path class PestRunConfiguration(project: Project, factory: ConfigurationFactory) : PhpTestRunConfiguration( project, factory, PestBundle.message("FRAMEWORK_NAME"), PestFrameworkType.instance, VALIDATOR, PestRunConfigurationHandler.instance, PestVersionDetector.instance ), PhpAsyncRunConfiguration { override fun createSettings(): PestRunConfigurationSettings { return PestRunConfigurationSettings() } override fun getConfigurationEditor(): SettingsEditor { val names = EnumMap(PhpTestRunnerSettings.Scope::class.java) val editor = this.getConfigurationEditor(names) editor.setRunnerOptionsDocumentation("https://pestphp.com/docs/installation") return PestTestRunConfigurationEditor(editor, this) } @Throws(ExecutionException::class) @Suppress("SwallowedException") fun checkAndGetState(env: ExecutionEnvironment, command: PhpCommandSettings): RunProfileState? { try { checkConfiguration() } catch (ignored: RuntimeConfigurationWarning) { } catch (exception: RuntimeConfigurationException) { throw ExecutionException(PestBundle.message("RUNTIME_CONFIGURATION_EXCEPTION_MESSAGE", exception.localizedMessage, this.name)) } return this.getState(env, command, null) } override fun createMethodFieldCompletionProvider( editor: PhpTestRunnerConfigurationEditor ): TextFieldCompletionProvider { return object : TextFieldCompletionProvider() { override fun addCompletionVariants(text: String, offset: Int, prefix: String, result: CompletionResultSet) { val file = PhpRunUtil.findPsiFile(project, settings.runnerSettings.filePath) file?.getPestTests() ?.mapNotNull { it.getPestTestName() } ?.map { LookupElementBuilder.create(it).withIcon(PestIcons.File) } ?.forEach { result.addElement(it) } } } } override fun createRerunAction( consoleView: ConsoleView, properties: SMTRunnerConsoleProperties ): AbstractRerunFailedTestsAction { return PestRerunFailedTestsAction(consoleView, properties) } override fun createTestConsoleProperties(executor: Executor): SMTRunnerConsoleProperties { val manager = PhpRemoteInterpreterManager.getInstance() val pathProcessor = when (this.interpreter?.isRemote) { true -> manager?.createPathMapper(this.project, interpreter!!.phpSdkAdditionalData) else -> null } return this.createTestConsoleProperties( executor, pathProcessor ?: PhpCommandLinePathProcessor.LOCAL ) } private fun createTestConsoleProperties( executor: Executor, processor: PhpCommandLinePathProcessor ): PestConsoleProperties { val pathMapper = processor.createPathMapper(this.project) return PestConsoleProperties( this, executor, PestLocationProvider(pathMapper, this.project, this.getConfigurationFileRootPath()) ) } override fun suggestedName(): String? { val runner = this.settings.runnerSettings return when (val scope = runner.scope) { PhpTestRunnerSettings.Scope.Directory -> PathUtil.getFileName(StringUtil.notNullize(runner.directoryPath)) PhpTestRunnerSettings.Scope.File -> PathUtil.getFileName(StringUtil.notNullize(runner.filePath)) PhpTestRunnerSettings.Scope.Method -> { val file = PathUtil.getFileName(StringUtil.notNullize(runner.filePath)) "$file::${runner.methodName}" } PhpTestRunnerSettings.Scope.ConfigurationFile -> PathUtil.getFileName( StringUtil.notNullize(runner.configurationFilePath) ) else -> { assert(false) { "Unknown scope: $scope" } null } } } fun applyTestArguments(command: PhpCommandSettings, coverageArguments: List) { val config = PhpTestFrameworkSettingsManager.getInstance(project) .getOrCreateByInterpreter(PestFrameworkType.instance, interpreter, true) ?: throw ExecutionException(PestBundle.message("DIALOG_MESSAGE_COULD_NOT_FIND_PHP_INTERPRETER")) val version = null val workingDirectory = getWorkingDirectory(project, settings, config) ?: throw ExecutionException(PhpBundle.message("php.interpreter.base.configuration.working.directory")) PestRunConfigurationHandler.instance.prepareCommand( project, command, config.executablePath!!, version ) command.importCommandLineSettings(settings.commandLineSettings, workingDirectory) fillTestRunnerArguments( project, workingDirectory, settings.runnerSettings, coverageArguments, command, config, PestRunConfigurationHandler.instance ) } override fun createCommand( interpreter: PhpInterpreter, env: MutableMap, arguments: MutableList, withDebugger: Boolean ): PhpCommandSettings { PestRunConfigurationHandler.instance.rootPath = getConfigurationFileRootPath() return super.createCommand(interpreter, env, arguments, withDebugger).apply { addParallelArguments(this@PestRunConfiguration, this) } } override fun getWorkingDirectory( project: Project, settings: PhpTestRunConfigurationSettings, config: PhpTestFrameworkConfiguration? ): String? { val cli = settings.commandLineSettings if (cli.workingDirectory?.isNotEmpty() == true) { return cli.workingDirectory } val configFileRootPath = getConfigurationFileRootPath() if (configFileRootPath.isNullOrEmpty()) { return super.getWorkingDirectory(project, settings, config) } return configFileRootPath } private fun getConfigurationFileRootPath(): String? { val configFile = getConfigurationFile( settings.runnerSettings, PhpTestFrameworkSettingsManager.getInstance(project) .getOrCreateByInterpreter(PestFrameworkType.instance, interpreter, true) ) ?: return null return VfsUtil.findFile(Path(configFile), false)?.parent?.path } val pestSettings: PestRunConfigurationSettings get() { return super.getSettings() as PestRunConfigurationSettings } } ================================================ FILE: src/main/kotlin/com/pestphp/pest/configuration/PestRunConfigurationHandler.kt ================================================ package com.pestphp.pest.configuration import com.intellij.execution.ExecutionException import com.intellij.openapi.project.Project import com.jetbrains.php.config.commandLine.PhpCommandSettings import com.jetbrains.php.testFramework.run.PhpTestRunConfigurationHandler import com.pestphp.pest.PestBundle import com.pestphp.pest.toPestTestRegex class PestRunConfigurationHandler : PhpTestRunConfigurationHandler { var rootPath: String? = null companion object { @JvmField val instance = PestRunConfigurationHandler() } override fun getConfigFileOption(): String { return "--configuration" } override fun prepareCommand(project: Project, commandSettings: PhpCommandSettings, exe: String, version: String?) { commandSettings.setScript(exe, false) commandSettings.addArgument("--teamcity") commandSettings.addEnv("IDE_PEST_EXE", exe) if (!version.isNullOrEmpty()) { commandSettings.addEnv("IDE_PEST_VERSION", version) } } @Throws(ExecutionException::class) override fun runType( project: Project, phpCommandSettings: PhpCommandSettings, type: String, workingDirectory: String ) { throw ExecutionException(PestBundle.message("CANNOT_RUN_PEST_WITH_TYPE_MESSAGE")) } override fun runDirectory( project: Project, phpCommandSettings: PhpCommandSettings, directory: String, workingDirectory: String ) { if (directory.isEmpty()) { return } phpCommandSettings.addPathArgument(directory) } override fun runFile( project: Project, phpCommandSettings: PhpCommandSettings, file: String, workingDirectory: String ) { if (file.isEmpty()) { return } phpCommandSettings.addPathArgument(file) } override fun runMethod( project: Project, phpCommandSettings: PhpCommandSettings, file: String, methodName: String, workingDirectory: String ) { if (file.isEmpty()) { return } val pathMapper = phpCommandSettings.pathProcessor.createPathMapper(project) val rootPath = this.rootPath ?: workingDirectory phpCommandSettings.addPathArgument(file) phpCommandSettings.addArgument("--filter") phpCommandSettings.addArgument("/${methodName.toPestTestRegex(rootPath, file, pathMapper)}/") } } ================================================ FILE: src/main/kotlin/com/pestphp/pest/configuration/PestRunConfigurationProducer.kt ================================================ package com.pestphp.pest.configuration import com.intellij.execution.actions.ConfigurationFromContext import com.intellij.execution.configurations.ConfigurationFactory import com.intellij.ide.highlighter.XmlFileType import com.intellij.openapi.fileTypes.FileType import com.intellij.openapi.project.Project import com.intellij.openapi.util.Condition import com.intellij.openapi.vfs.VirtualFile import com.intellij.psi.PsiDirectory import com.intellij.psi.PsiElement import com.intellij.psi.PsiFile import com.intellij.util.Function import com.jetbrains.php.lang.PhpFileType import com.jetbrains.php.lang.psi.PhpFile import com.jetbrains.php.testFramework.run.PhpDefaultTestRunnerSettingsValidator import com.jetbrains.php.testFramework.run.PhpTestConfigurationProducer import com.pestphp.pest.getPestTestName import com.pestphp.pest.isPestConfigurationFile import com.pestphp.pest.isPestEnabled import com.pestphp.pest.isPestTestFile import com.pestphp.pest.isPestTestReference class PestRunConfigurationProducer : PhpTestConfigurationProducer( VALIDATOR, FILE_TO_SCOPE, METHOD_NAMER, METHOD ) { override fun getConfigurationFactory(): ConfigurationFactory = PestRunConfigurationType.instance override fun isEnabled(project: Project): Boolean = project.isPestEnabled() override fun getWorkingDirectory(element: PsiElement): VirtualFile? { if (element is PsiDirectory) { return element.parentDirectory?.virtualFile } return element.containingFile?.containingDirectory?.virtualFile } companion object { val METHOD = Condition { element: PsiElement? -> element.isPestTestReference() } private val METHOD_NAMER = Function { element: PsiElement? -> element.getPestTestName() } private val FILE_TO_SCOPE = Function { file: PsiFile -> if (file.isPestTestFile()) file else null } val VALIDATOR = PhpDefaultTestRunnerSettingsValidator( setOf(PhpFileType.INSTANCE, XmlFileType.INSTANCE).toList(), { file: PsiFile, _: String -> file.isPestConfigurationFile() || file.isPestTestFile() }, false, false ) } override fun shouldReplace(self: ConfigurationFromContext, other: ConfigurationFromContext): Boolean { val file = self.sourceElement as? PhpFile ?: return false return file.isPestTestFile() } } ================================================ FILE: src/main/kotlin/com/pestphp/pest/configuration/PestRunConfigurationType.kt ================================================ package com.pestphp.pest.configuration import com.intellij.execution.configurations.ConfigurationTypeUtil.findConfigurationType import com.intellij.execution.configurations.RunConfiguration import com.intellij.execution.configurations.SimpleConfigurationType import com.intellij.openapi.project.DumbAware import com.intellij.openapi.project.Project import com.intellij.openapi.util.NotNullLazyValue import com.pestphp.pest.PestBundle import com.pestphp.pest.PestIcons class PestRunConfigurationType private constructor() : SimpleConfigurationType( "PestRunConfigurationType", PestBundle.message("FRAMEWORK_NAME"), PestBundle.message("FRAMEWORK_NAME"), NotNullLazyValue.createValue { PestIcons.Config } ), DumbAware { override fun createTemplateConfiguration(project: Project): RunConfiguration { return PestRunConfiguration(project, this) } companion object { @JvmStatic val instance: PestRunConfigurationType get() = findConfigurationType(PestRunConfigurationType::class.java) } } ================================================ FILE: src/main/kotlin/com/pestphp/pest/configuration/PestRunnerSettings.kt ================================================ package com.pestphp.pest.configuration import com.intellij.util.xmlb.annotations.Attribute import com.intellij.util.xmlb.annotations.Tag import com.jetbrains.php.phpunit.coverage.PhpUnitCoverageEngine.CoverageEngine import com.jetbrains.php.testFramework.run.PhpTestRunnerSettings @Tag("PestRunner") class PestRunnerSettings : PhpTestRunnerSettings() { @Attribute("coverage_engine") var coverageEngine: CoverageEngine = CoverageEngine.XDEBUG @Attribute("parallel_testing_enabled") var parallelTestingEnabled: Boolean = false companion object { @JvmStatic fun fromPhpTestRunnerSettings(settings: PhpTestRunnerSettings): PestRunnerSettings { val pestSettings = PestRunnerSettings() pestSettings.scope = settings.scope pestSettings.selectedType = settings.selectedType pestSettings.directoryPath = settings.directoryPath pestSettings.filePath = settings.filePath pestSettings.methodName = settings.methodName pestSettings.isUseAlternativeConfigurationFile = settings.isUseAlternativeConfigurationFile pestSettings.configurationFilePath = settings.configurationFilePath pestSettings.testRunnerOptions = settings.testRunnerOptions return pestSettings } } override fun equals(other: Any?): Boolean { if (other !is PestRunnerSettings) return false return super.equals(other) && coverageEngine == other.coverageEngine && parallelTestingEnabled == other.parallelTestingEnabled } override fun hashCode(): Int { var result = super.hashCode() result = 31 * result + coverageEngine.hashCode() result = 31 * result + parallelTestingEnabled.hashCode() return result } } ================================================ FILE: src/main/kotlin/com/pestphp/pest/configuration/PestTestRunConfigurationEditor.kt ================================================ package com.pestphp.pest.configuration import com.intellij.openapi.editor.ReadOnlyModificationException import com.intellij.openapi.options.SettingsEditor import com.intellij.openapi.ui.ComboBox import com.intellij.ui.components.JBCheckBox import com.intellij.util.ui.UI import com.jetbrains.php.phpunit.coverage.PhpUnitCoverageEngine.CoverageEngine import com.jetbrains.php.testFramework.run.PhpTestRunConfigurationEditor import com.pestphp.pest.PestBundle import java.lang.reflect.InvocationTargetException import javax.swing.BoxLayout import javax.swing.JComponent import javax.swing.JPanel class PestTestRunConfigurationEditor( private val parentEditor: PhpTestRunConfigurationEditor, settings: PestRunConfiguration ) : SettingsEditor() { private val myMainPanel = JPanel() private var coveragePanel = JPanel() private var parallelPanel = JPanel() private val coverageEngineComboBox = ComboBox(arrayOf(CoverageEngine.XDEBUG, CoverageEngine.PCOV)) private val enabledParallelTestingCheckBox = JBCheckBox() init { coveragePanel = UI.PanelFactory.grid().add( UI.PanelFactory.panel(coverageEngineComboBox).withLabel(PestBundle.message("COVERAGE_ENGINE_LABEL_TEXT")) ).createPanel() parallelPanel = UI.PanelFactory.grid().add( UI.PanelFactory.panel(enabledParallelTestingCheckBox).withLabel(PestBundle.message("ENABLE_PARALLEL_TESTING_LABEL_TEXT")) ).createPanel() myMainPanel.layout = BoxLayout(myMainPanel, BoxLayout.Y_AXIS) myMainPanel.add(parentEditor.component) myMainPanel.add(coveragePanel) myMainPanel.add(parallelPanel) resetEditorFrom(settings) } override fun createEditor(): JComponent { return myMainPanel } private fun doApply(configuration: PestRunConfiguration) { val settings = configuration.settings as PestRunConfigurationSettings val runnerSettings = settings.pestRunnerSettings runnerSettings.coverageEngine = coverageEngineComboBox.selectedItem as CoverageEngine runnerSettings.parallelTestingEnabled = enabledParallelTestingCheckBox.isSelected } private fun doReset(configuration: PestRunConfiguration) { val settings = configuration.settings as PestRunConfigurationSettings val runnerSettings = settings.pestRunnerSettings coverageEngineComboBox.selectedItem = runnerSettings.coverageEngine enabledParallelTestingCheckBox.isSelected = runnerSettings.parallelTestingEnabled } override fun resetEditorFrom(settings: PestRunConfiguration) { doReset(settings) parentEditor.javaClass.declaredMethods.find { it.name == "resetEditorFrom" }!!.let { it.isAccessible = true it.invoke(parentEditor, settings) } } override fun applyEditorTo(settings: PestRunConfiguration) { parentEditor.javaClass.declaredMethods.find { it.name == "applyEditorTo" }!!.let { it.isAccessible = true try { it.invoke(parentEditor, settings) } catch (exception: InvocationTargetException) { // In case the method throws a read only error (happens in code with me) we ignore it. if (exception.cause is ReadOnlyModificationException) { return@let } throw exception } } doApply(settings) } override fun getSnapshot(): PestRunConfiguration { val result = parentEditor.snapshot as PestRunConfiguration doApply(result) return result } } ================================================ FILE: src/main/kotlin/com/pestphp/pest/configuration/PestVersionDetector.kt ================================================ package com.pestphp.pest.configuration import com.intellij.execution.ExecutionException import com.intellij.openapi.project.Project import com.jetbrains.php.PhpTestFrameworkVersionDetector import com.jetbrains.php.config.interpreters.PhpInterpreter import com.pestphp.pest.PestBundle import org.jetbrains.annotations.Nls private val VERSION_REGEX = Regex("(?\\d+)\\.(?\\d+)\\.(?\\d+)") private val VERSION_OPTIONS = arrayOf("--version", "--colors=never") class PestVersionDetector : PhpTestFrameworkVersionDetector() { override fun getPresentableName(): @Nls String { return PestBundle.message("FRAMEWORK_NAME") } override fun getTitle(): String { return PestBundle.message("GETTING_PEST_VERSION") } override fun getVersionOptions(): Array { return VERSION_OPTIONS } public override fun parse(s: String): String { val version = if (s.startsWith("Pest")) { // for <2.0.0 versions s.removePrefix("Pest").substringBefore("\n").trim() } else { // for 2.* versions s.trim().removePrefix("Pest Testing Framework ").substringBeforeLast('.') } if (!version.matches(VERSION_REGEX)) { throw ExecutionException(PestBundle.message("PEST_CONFIGURATION_UI_CAN_NOT_PARSE_VERSION", s)) } return version } override fun getVersion(project: Project, interpreter: PhpInterpreter, executable: String?): String { if (interpreter.isRemote) { throw ExecutionException(PestBundle.message("PEST_VERSION_IS_NOT_SUPPORTED_FOR_REMOTE_INTERPRETER")) } return super.getVersion(project, interpreter, executable) } companion object { val instance = PestVersionDetector() } } ================================================ FILE: src/main/kotlin/com/pestphp/pest/features/configuration/ConfigurationInDirectoryReferenceProvider.kt ================================================ package com.pestphp.pest.features.configuration import com.intellij.psi.PsiElement import com.intellij.psi.PsiReference import com.intellij.psi.PsiReferenceProvider import com.intellij.util.ProcessingContext import com.jetbrains.php.lang.psi.elements.FunctionReference import com.jetbrains.php.lang.psi.elements.MethodReference import com.jetbrains.php.lang.psi.elements.StringLiteralExpression import com.jetbrains.php.lang.psi.elements.impl.FunctionReferenceImpl import com.jetbrains.php.lang.psi.elements.impl.MethodReferenceImpl import com.pestphp.pest.CONFIGURATION_FUNCTIONS class ConfigurationInDirectoryReferenceProvider: PsiReferenceProvider() { override fun getReferencesByElement(element: PsiElement, context: ProcessingContext): Array { val inCall = element.parent.parent as MethodReferenceImpl if (inCall.canonicalText != "in") { return PsiReference.EMPTY_ARRAY } val usesCall = getConfigurationFunctionCall(inCall) as? FunctionReferenceImpl ?: return PsiReference.EMPTY_ARRAY if (usesCall.canonicalText !in CONFIGURATION_FUNCTIONS) { return PsiReference.EMPTY_ARRAY } val referenceSet = PhpFolderReferenceSet(element, element as StringLiteralExpression, this) return referenceSet .allReferences .toList() .toTypedArray() } } fun getConfigurationFunctionCall(inCall: MethodReference): FunctionReference? { val child = inCall.firstPsiChild if (child !is MethodReference) { if (child is FunctionReference) { return child } return null } return getConfigurationFunctionCall(child) } ================================================ FILE: src/main/kotlin/com/pestphp/pest/features/configuration/ConfigurationReferenceContributor.kt ================================================ package com.pestphp.pest.features.configuration import com.intellij.patterns.PlatformPatterns import com.intellij.psi.PsiReferenceContributor import com.intellij.psi.PsiReferenceRegistrar import com.jetbrains.php.lang.psi.elements.ParameterList import com.jetbrains.php.lang.psi.elements.StringLiteralExpression import com.jetbrains.php.lang.psi.elements.impl.MethodReferenceImpl class ConfigurationReferenceContributor : PsiReferenceContributor() { override fun registerReferenceProviders(registrar: PsiReferenceRegistrar) { registrar.registerReferenceProvider( PlatformPatterns.psiElement(StringLiteralExpression::class.java) .withParents( ParameterList::class.java, MethodReferenceImpl::class.java ), ConfigurationInDirectoryReferenceProvider() ) } } ================================================ FILE: src/main/kotlin/com/pestphp/pest/features/configuration/PhpFolderReferenceSet.kt ================================================ package com.pestphp.pest.features.configuration import com.intellij.openapi.util.Condition import com.intellij.psi.PsiElement import com.intellij.psi.PsiFileSystemItem import com.intellij.psi.PsiReferenceProvider import com.intellij.psi.impl.source.resolve.reference.impl.providers.FileReferenceSet import com.jetbrains.php.lang.psi.elements.PhpPsiElement import com.jetbrains.php.lang.psi.elements.impl.PhpFileReferenceSet class PhpFolderReferenceSet(element: PsiElement, argument: PhpPsiElement, provider: PsiReferenceProvider) : PhpFileReferenceSet(element, argument, provider) { override fun getReferenceCompletionFilter(): Condition { return FileReferenceSet.DIRECTORY_FILTER } override fun computeDefaultContexts(): MutableCollection { val containingFile = this.element.containingFile.originalFile val directory = containingFile.virtualFile.parent val fileSystemItems = toFileSystemItems(directory) if (fileSystemItems.isNotEmpty()) { return fileSystemItems } return super.computeDefaultContexts() } } ================================================ FILE: src/main/kotlin/com/pestphp/pest/features/customExpectations/CustomExpectationIndex.kt ================================================ package com.pestphp.pest.features.customExpectations import com.intellij.openapi.vfs.VirtualFile import com.intellij.util.indexing.DataIndexer import com.intellij.util.indexing.DefaultFileTypeSpecificInputFilter import com.intellij.util.indexing.FileBasedIndex import com.intellij.util.indexing.FileBasedIndexExtension import com.intellij.util.indexing.FileContent import com.intellij.util.indexing.ID import com.intellij.util.io.DataExternalizer import com.intellij.util.io.EnumeratorStringDescriptor import com.intellij.util.io.KeyDescriptor import com.jetbrains.php.lang.PhpFileType import com.jetbrains.php.lang.psi.PhpFile import com.jetbrains.php.lang.psi.elements.MethodReference import com.jetbrains.php.lang.psi.elements.impl.MethodReferenceImpl import com.jetbrains.php.lang.psi.stubs.indexes.PhpDepthLimitedRecursiveElementVisitor import com.jetbrains.php.lang.psi.stubs.indexes.PhpInvokeCallsOffsetsIndex.IntArrayExternalizer import it.unimi.dsi.fastutil.ints.IntArrayList import it.unimi.dsi.fastutil.ints.IntList val KEY = ID.create("php.pest.custom_expectations") class CustomExpectationIndex : FileBasedIndexExtension() { override fun getName(): ID { return KEY } override fun getVersion(): Int { return 7 } override fun getIndexer(): DataIndexer { return DataIndexer { inputData -> val file = inputData.psiFile val map: MutableMap = mutableMapOf() if (file is PhpFile) { file.accept(object : PhpDepthLimitedRecursiveElementVisitor() { override fun visitPhpMethodReference(reference: MethodReference) { if (reference is MethodReferenceImpl && reference.isPestExtendReference()) { reference.extendName?.let { if (it !in map) { map[it] = IntArrayList() } map[it]!!.add(reference.parameters[0].textOffset + 1) } } } }) } return@DataIndexer map } } override fun getKeyDescriptor(): KeyDescriptor { return EnumeratorStringDescriptor.INSTANCE } override fun getValueExternalizer(): DataExternalizer { return IntArrayExternalizer.INSTANCE } override fun getInputFilter(): FileBasedIndex.InputFilter { return object : DefaultFileTypeSpecificInputFilter(PhpFileType.INSTANCE) { override fun acceptInput(file: VirtualFile): Boolean { return !isPestStubFile(file) } } } override fun dependsOnFileContent(): Boolean { return true } private fun isPestStubFile(file: VirtualFile): Boolean { val path = file.path return path.contains("vendor") && path.contains("pestphp") && path.contains("stubs") } } ================================================ FILE: src/main/kotlin/com/pestphp/pest/features/customExpectations/CustomExpectationNotifier.kt ================================================ package com.pestphp.pest.features.customExpectations import com.intellij.psi.PsiFile import com.intellij.util.messages.Topic import com.pestphp.pest.features.customExpectations.generators.Method import java.util.EventListener interface CustomExpectationNotifier : EventListener { companion object { @Topic.ProjectLevel val TOPIC = Topic.create("Custom expectation", CustomExpectationNotifier::class.java) } fun changedExpectation(file: PsiFile, customExpectations: List) } ================================================ FILE: src/main/kotlin/com/pestphp/pest/features/customExpectations/CustomExpectationParameterInfoHandler.kt ================================================ @file:Suppress("UnstableApiUsage") package com.pestphp.pest.features.customExpectations import com.intellij.lang.parameterInfo.CreateParameterInfoContext import com.intellij.lang.parameterInfo.ParameterInfoHandlerWithTabActionSupport import com.intellij.lang.parameterInfo.ParameterInfoUIContext import com.intellij.lang.parameterInfo.UpdateParameterInfoContext import com.intellij.model.psi.PsiSymbolReferenceService import com.intellij.psi.PsiElement import com.intellij.psi.tree.IElementType import com.intellij.util.IntPair import com.intellij.util.containers.ContainerUtil import com.jetbrains.php.lang.PhpParameterInfoHandler import com.jetbrains.php.lang.lexer.PhpTokenTypes import com.jetbrains.php.lang.psi.elements.FunctionReference import com.jetbrains.php.lang.psi.elements.PhpPsiElement import com.jetbrains.php.lang.psi.elements.Statement import com.jetbrains.php.lang.psi.elements.impl.MethodReferenceImpl import com.pestphp.pest.features.customExpectations.generators.Parameter import com.pestphp.pest.features.customExpectations.symbols.PestCustomExpectationReference import com.pestphp.pest.features.customExpectations.symbols.PestCustomExpectationSymbol class CustomExpectationParameterInfoHandler : ParameterInfoHandlerWithTabActionSupport, PsiElement> { override fun findElementForParameterInfo(context: CreateParameterInfoContext): FunctionReference? { val methodReference = PhpParameterInfoHandler.findAnchorForParameterInfo(context) as? MethodReferenceImpl ?: return null val references = PsiSymbolReferenceService.getService().getReferences(methodReference) val symbol = references.filterIsInstance().flatMap { it.resolveReference() } .filterIsInstance().firstOrNull() ?: return null context.itemsToShow = arrayOf(symbol.methodDescriptor.parameters) return methodReference } override fun showParameterInfo(element: FunctionReference, context: CreateParameterInfoContext) { context.showHint(element, element.textRange.startOffset, this) } override fun findElementForUpdatingParameterInfo(context: UpdateParameterInfoContext): FunctionReference? { return PhpParameterInfoHandler.findAnchorForParameterInfo(context) as? FunctionReference } override fun updateParameterInfo(reference: FunctionReference, context: UpdateParameterInfoContext) { context.setCurrentParameter( PhpParameterInfoHandler.getCurrentParameterIndex( reference, PhpParameterInfoHandler.getCurrentOffset(context), actualParameterDelimiterType ) ) } override fun updateUI(params: List, context: ParameterInfoUIContext) { if (params.isEmpty()) { context.isUIComponentEnabled = false return } var currentParameter = context.currentParameterIndex if (currentParameter < 0) currentParameter = 0 val buffer = StringBuilder() val highlightRange = PhpParameterInfoHandler.appendParameterInfo(context, buffer, IntPair(-1, -1), currentParameter, params) { p -> if (p.returnType.isEmpty) { p.name } else { "${p.name}: ${p.returnType}" } } context.setupUIComponentPresentation( buffer.toString(), highlightRange.first, highlightRange.second, false, false, false, context.defaultParameterColor ) } override fun getActualParameters(o: FunctionReference): Array { val parameters = o.parameters return parameters.copyOf() } override fun getActualParameterDelimiterType(): IElementType { return PhpTokenTypes.opCOMMA } override fun getActualParametersRBraceType(): IElementType { return PhpTokenTypes.chRPAREN } override fun getArgumentListAllowedParentClasses(): Set> { return ContainerUtil.newHashSet>(PhpPsiElement::class.java) } override fun getArgListStopSearchClasses(): Set> { return ContainerUtil.newHashSet( Statement::class.java ) } override fun getArgumentListClass(): Class { return FunctionReference::class.java } } ================================================ FILE: src/main/kotlin/com/pestphp/pest/features/customExpectations/CustomExpectationRemoveGeneratedFileStartupActivity.kt ================================================ package com.pestphp.pest.features.customExpectations import com.intellij.openapi.application.runWriteAction import com.intellij.openapi.project.Project import com.intellij.openapi.startup.ProjectActivity import com.intellij.openapi.util.Disposer import com.intellij.openapi.vfs.VirtualFile import com.jetbrains.php.composer.ComposerConfigListener import com.jetbrains.php.composer.ComposerDataService import com.jetbrains.php.composer.lib.ComposerLibraryServiceFactory import com.pestphp.pest.PestPluginDisposable class CustomExpectationRemoveGeneratedFileStartupActivity : ProjectActivity { override suspend fun execute(project: Project) { tryDeleteGeneratedExpectationFile(project) val composerDataService = ComposerDataService.getInstance(project) val listener = object : ComposerConfigListener { override fun configPathChanged(oldPath: String?, newPath: String?, isWellConfigured: Boolean) { if (newPath != null) { tryDeleteGeneratedExpectationFile(project) } } } composerDataService.addConfigListener(listener) Disposer.register(PestPluginDisposable.getInstance(project)) { composerDataService.removeConfigListener(listener) } } private fun tryDeleteGeneratedExpectationFile(project: Project) { ComposerLibraryServiceFactory.getInstance(project, null as VirtualFile?).vendorDir?.findChild("Expectation.php")?.let { runWriteAction { it.delete(null) } } } } ================================================ FILE: src/main/kotlin/com/pestphp/pest/features/customExpectations/ListMethodDataExternalizer.kt ================================================ package com.pestphp.pest.features.customExpectations import com.intellij.util.io.DataExternalizer import com.pestphp.pest.features.customExpectations.generators.Method import java.io.DataInput import java.io.DataOutput class ListMethodDataExternalizer : DataExternalizer> { override fun save(out: DataOutput, value: List) { out.writeInt(value.size) val methodExternalizer = MethodDataExternalizer() value.forEach { methodExternalizer.save(out, it) } } override fun read(input: DataInput): List { val size = input.readInt() val methodExternalizer = MethodDataExternalizer() val methods = mutableListOf() repeat(size) { methods.add( methodExternalizer.read(input) ) } return methods } } ================================================ FILE: src/main/kotlin/com/pestphp/pest/features/customExpectations/MethodDataExternalizer.kt ================================================ package com.pestphp.pest.features.customExpectations import com.intellij.util.io.DataExternalizer import com.intellij.util.io.EnumeratorStringDescriptor import com.jetbrains.php.lang.psi.resolve.types.PhpType import com.jetbrains.php.lang.psi.stubs.indexes.StringSetDataExternalizer import com.pestphp.pest.features.customExpectations.generators.Method import com.pestphp.pest.features.customExpectations.generators.Parameter import java.io.DataInput import java.io.DataOutput class MethodDataExternalizer : DataExternalizer { override fun save(out: DataOutput, value: Method) { EnumeratorStringDescriptor.INSTANCE.save(out, value.name) var returnType = value.returnType.toString() if (!value.returnType.isComplete) { returnType = returnType.removeSuffix("|?") } EnumeratorStringDescriptor.INSTANCE.save( out, returnType ) StringSetDataExternalizer.INSTANCE.save( out, value.parameters .map { it.toString() } .toSet() ) } override fun read(input: DataInput): Method { val name = EnumeratorStringDescriptor.INSTANCE.read(input) val returnType = EnumeratorStringDescriptor.INSTANCE.read(input) val parameterString = StringSetDataExternalizer.INSTANCE.read(input) val parameters = parameterString.reversed().map { Parameter( name = Regex("name='(.*?)'") .find(it)!! .groupValues[1], returnType = PhpType.builder() .add( Regex("returnType='(.*?)'") .find(it)!! .groupValues[1] ).build(), defaultValue = Regex("defaultValue='(.*)'") .find(it)!! .groupValues[1] .ifEmpty { null } ) } return Method( name, PhpType().add(returnType), parameters ) } } ================================================ FILE: src/main/kotlin/com/pestphp/pest/features/customExpectations/expectationUtil.kt ================================================ package com.pestphp.pest.features.customExpectations import com.intellij.openapi.project.Project import com.intellij.psi.PsiElement import com.intellij.psi.PsiFile import com.intellij.psi.util.PsiTreeUtil import com.jetbrains.php.PhpIndex import com.jetbrains.php.lang.psi.PhpFile import com.jetbrains.php.lang.psi.elements.Function import com.jetbrains.php.lang.psi.elements.MethodReference import com.jetbrains.php.lang.psi.elements.ParameterList import com.jetbrains.php.lang.psi.elements.PhpExpression import com.jetbrains.php.lang.psi.elements.PhpNamespace import com.jetbrains.php.lang.psi.elements.Statement import com.jetbrains.php.lang.psi.elements.StringLiteralExpression import com.jetbrains.php.lang.psi.elements.Variable import com.jetbrains.php.lang.psi.elements.impl.FunctionReferenceImpl import com.jetbrains.php.lang.psi.elements.impl.MethodReferenceImpl import com.jetbrains.php.lang.psi.resolve.types.PhpType import com.pestphp.pest.features.customExpectations.generators.Method import com.pestphp.pest.features.customExpectations.generators.Parameter val expectationType = PhpType().apply { this.add("\\Pest\\Expectation") } fun PhpType.isExpectation(project: Project): Boolean { val filteredType = this.filterMixed() if (filteredType.isEmpty) { return false } return expectationType.isConvertibleFrom( filteredType, PhpIndex.getInstance(project) ) } fun Statement.isExpectation(): Boolean { return (this.firstPsiChild as? MethodReference)?.isExpectation() == true } fun MethodReference.isExpectation(): Boolean { return this.type.isExpectation(this.project) } fun PsiElement?.isThisVariableInExtend(): Boolean { if ((this as? Variable)?.name != "this") return false val closure = PsiTreeUtil.getParentOfType(this, Function::class.java) if (closure == null || !closure.isClosure) return false val parameterList = closure.parent?.parent as? ParameterList ?: return false return parameterList.parent.isPestExtendReference() } fun PsiElement.isPestExtendReference(): Boolean { if (this !is MethodReferenceImpl) { return false } if (this.canonicalText != "extend") { return false } val expectReference = this.firstChild if (expectReference !is FunctionReferenceImpl) { return false } if (expectReference.canonicalText != "expect") { return false } return true } val MethodReference.extendName: String? get() { val name = this.getParameter(0) ?: return null if (name !is StringLiteralExpression) { return null } return name.contents } val PsiFile.customExpects: List get() { if (this !is PhpFile) return emptyList() val element = this.firstChild return element.children.filterIsInstance() .mapNotNull { it.statements } .getOrElse( 0 ) { element } .children .filterIsInstance() .mapNotNull { it.firstChild } .filterIsInstance() .filter { it.isPestExtendReference() } } fun MethodReference.toMethod(): Method? { val extendName = this.extendName ?: return null // Custom expectations should always have two parameters. if(this.parameters.count() != 2) { return null } val closure = (this.parameters[1] as? PhpExpression)?.firstChild as? Function if (closure === null) { return null } return Method( extendName, closure.type, closure.parameters.map { parameter -> Parameter( parameter.name, parameter.type, parameter.defaultValuePresentation ) } ) } ================================================ FILE: src/main/kotlin/com/pestphp/pest/features/customExpectations/externalizers/ListDataExternalizer.kt ================================================ package com.pestphp.pest.features.customExpectations.externalizers import com.intellij.util.io.DataExternalizer import java.io.DataInput import java.io.DataOutput class ListDataExternalizer(private val dataExternalizer: DataExternalizer) : DataExternalizer> { override fun save(out: DataOutput, value: List) { out.writeInt(value.size) value.forEach { dataExternalizer.save(out, it) } } override fun read(input: DataInput): List { val size = input.readInt() val list = mutableListOf() repeat(size) { list.add( dataExternalizer.read(input) ) } return list } } ================================================ FILE: src/main/kotlin/com/pestphp/pest/features/customExpectations/externalizers/MethodDataExternalizer.kt ================================================ package com.pestphp.pest.features.customExpectations.externalizers import com.intellij.util.io.DataExternalizer import com.intellij.util.io.EnumeratorStringDescriptor import com.pestphp.pest.features.customExpectations.generators.Method import java.io.DataInput import java.io.DataOutput class MethodDataExternalizer : DataExternalizer { companion object { val INSTANCE = MethodDataExternalizer() } override fun save(out: DataOutput, value: Method) { EnumeratorStringDescriptor.INSTANCE.save(out, value.name) PhpTypeDataExternalizer.INSTANCE.save(out, value.returnType) ListDataExternalizer(ParameterDataExternalizer.INSTANCE).save( out, value.parameters ) } override fun read(input: DataInput): Method { val name = EnumeratorStringDescriptor.INSTANCE.read(input) val returnType = PhpTypeDataExternalizer.INSTANCE.read(input) val parameters = ListDataExternalizer(ParameterDataExternalizer.INSTANCE).read(input) return Method( name, returnType, parameters ) } } ================================================ FILE: src/main/kotlin/com/pestphp/pest/features/customExpectations/externalizers/ParameterDataExternalizer.kt ================================================ package com.pestphp.pest.features.customExpectations.externalizers import com.intellij.util.io.DataExternalizer import com.intellij.util.io.EnumeratorStringDescriptor import com.intellij.util.io.NullableDataExternalizer import com.pestphp.pest.features.customExpectations.generators.Parameter import java.io.DataInput import java.io.DataOutput class ParameterDataExternalizer : DataExternalizer { companion object { val INSTANCE = ParameterDataExternalizer() } override fun save(out: DataOutput, value: Parameter) { EnumeratorStringDescriptor.INSTANCE.save(out, value.name) PhpTypeDataExternalizer.INSTANCE.save(out, value.returnType) NullableDataExternalizer(EnumeratorStringDescriptor.INSTANCE).save( out, value.defaultValue ) } override fun read(input: DataInput): Parameter { val name = EnumeratorStringDescriptor.INSTANCE.read(input) val returnType = PhpTypeDataExternalizer.INSTANCE.read(input) val defaultValue = NullableDataExternalizer(EnumeratorStringDescriptor.INSTANCE).read(input) return Parameter( name, returnType, defaultValue ) } } ================================================ FILE: src/main/kotlin/com/pestphp/pest/features/customExpectations/externalizers/PhpTypeDataExternalizer.kt ================================================ package com.pestphp.pest.features.customExpectations.externalizers import com.intellij.util.io.DataExternalizer import com.intellij.util.io.EnumeratorStringDescriptor import com.jetbrains.php.lang.psi.resolve.types.PhpType import java.io.DataInput import java.io.DataOutput class PhpTypeDataExternalizer : DataExternalizer { companion object { val INSTANCE = PhpTypeDataExternalizer() } override fun save(out: DataOutput, value: PhpType) { var endValue = value.toString() if (!value.isComplete) { endValue = endValue.removeSuffix("|?") } EnumeratorStringDescriptor.INSTANCE.save( out, endValue ) } override fun read(input: DataInput): PhpType { val type = EnumeratorStringDescriptor.INSTANCE.read(input) return PhpType().add(type) } } ================================================ FILE: src/main/kotlin/com/pestphp/pest/features/customExpectations/generators/ExpectationGenerator.kt ================================================ package com.pestphp.pest.features.customExpectations.generators import com.intellij.openapi.project.Project import com.intellij.psi.PsiFile import com.intellij.psi.PsiFileFactory import com.jetbrains.php.lang.PhpFileType /** * Generates A class for expectations. */ class ExpectationGenerator { val docMethods: MutableList = mutableListOf() fun generate(project: Project): String { return docMethods .joinToString("\n") { methodString(it, project) } .let { //language=InjectablePHP """ |) { fun parametersAsString(project: Project): List { return parameters.map { var parameterAsString = "${it.returnType.global(project).toStringResolved()} $${it.name}" if (it.defaultValue !== null) { parameterAsString += " = ${it.defaultValue}" } parameterAsString } } override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false other as Method if (name != other.name) return false if (returnType.toString() != other.returnType.toString()) return false if (parameters != other.parameters) return false return true } override fun hashCode(): Int { var result = name.hashCode() result = 31 * result + returnType.toString().hashCode() result = 31 * result + parameters.hashCode() return result } override fun toString(): String { return "Method(name='$name', returnType=$returnType, parameters=$parameters)" } } ================================================ FILE: src/main/kotlin/com/pestphp/pest/features/customExpectations/generators/Parameter.kt ================================================ package com.pestphp.pest.features.customExpectations.generators import com.jetbrains.php.lang.psi.resolve.types.PhpType class Parameter(val name: String, val returnType: PhpType, val defaultValue: String? = null) { override fun toString(): String { val defaultValue = defaultValue ?: "" return "Parameter(name='$name', returnType='$returnType', defaultValue='$defaultValue')" } override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false other as Parameter if (name != other.name) return false if (returnType.toString() != other.returnType.toString()) return false if (defaultValue != other.defaultValue) return false return true } override fun hashCode(): Int { var result = name.hashCode() result = 31 * result + returnType.toString().hashCode() result = 31 * result + (defaultValue?.hashCode() ?: 0) return result } } ================================================ FILE: src/main/kotlin/com/pestphp/pest/features/customExpectations/symbols/PestCustomExpectationReference.kt ================================================ @file:Suppress("UnstableApiUsage") package com.pestphp.pest.features.customExpectations.symbols import com.intellij.model.Symbol import com.intellij.model.psi.PsiSymbolReference import com.intellij.openapi.util.TextRange import com.intellij.psi.search.GlobalSearchScope import com.intellij.util.indexing.FileBasedIndex import com.jetbrains.php.lang.psi.PhpPsiUtil import com.jetbrains.php.lang.psi.elements.MethodReference import com.pestphp.pest.features.customExpectations.KEY import com.pestphp.pest.features.customExpectations.toMethod class PestCustomExpectationReference(private val methodReference: MethodReference) : PsiSymbolReference { override fun getElement(): MethodReference = methodReference override fun getRangeInElement() = methodReference.rangeInElement override fun resolveReference(): Collection { val pestCustomExtensions = mutableListOf() methodReference.name?.let { val extensionName = methodReference.name!! FileBasedIndex.getInstance() .processValues(KEY, extensionName, null, { file, value -> methodReference.manager.findFile(file)?.let { psiFile -> val range = TextRange.from(value.first(), extensionName.length) val element = psiFile.findElementAt(range.startOffset) PhpPsiUtil.getParentOfClass(element, MethodReference::class.java)?.toMethod()?.let { pestCustomExtensions.add(PestCustomExpectationSymbol(extensionName, psiFile, range, it)) } } true }, GlobalSearchScope.allScope(methodReference.project)) } return pestCustomExtensions } } ================================================ FILE: src/main/kotlin/com/pestphp/pest/features/customExpectations/symbols/PestCustomExpectationReferenceProvider.kt ================================================ @file:Suppress("UnstableApiUsage") package com.pestphp.pest.features.customExpectations.symbols import com.intellij.model.Symbol import com.intellij.model.psi.PsiExternalReferenceHost import com.intellij.model.psi.PsiSymbolReference import com.intellij.model.psi.PsiSymbolReferenceHints import com.intellij.model.psi.PsiSymbolReferenceProvider import com.intellij.model.search.SearchRequest import com.intellij.openapi.project.Project import com.jetbrains.php.lang.psi.elements.impl.MethodReferenceImpl import com.jetbrains.php.lang.psi.resolve.types.PhpType import com.pestphp.pest.features.customExpectations.symbols.PestCustomExpectationReferenceProvider.Companion.PEST_EXPECTATION val PEST_EXPECTATION_TYPE: PhpType = PhpType.from(PEST_EXPECTATION) class PestCustomExpectationReferenceProvider : PsiSymbolReferenceProvider { companion object { const val PEST_EXPECTATION: String = "\\Pest\\Expectation" } override fun getReferences( element: PsiExternalReferenceHost, hints: PsiSymbolReferenceHints ): Collection { if (element is MethodReferenceImpl) { val classReference = element.classReference val methodName = element.name if (methodName != null && classReference != null && "extend" != methodName && PhpType.intersectsGlobal(element.project, PEST_EXPECTATION_TYPE, classReference.type) ) { // workaround till `com.intellij.lang.javascript.navigation.JSGotoDeclarationHandler#getGotoDeclarationTargets` is not fixed if (element.multiResolve(false).isEmpty()) { return arrayListOf(PestCustomExpectationReference(element)) } } } return emptyList() } override fun getSearchRequests(project: Project, target: Symbol): Collection { return (target as? PestCustomExpectationSymbol)?.let { listOf(SearchRequest.of(target.expectationName)) } ?: emptyList() } } ================================================ FILE: src/main/kotlin/com/pestphp/pest/features/customExpectations/symbols/PestCustomExpectationRenameUsageSearcher.kt ================================================ @file:Suppress("UnstableApiUsage") package com.pestphp.pest.features.customExpectations.symbols import com.intellij.find.usages.api.PsiUsage import com.intellij.model.Pointer import com.intellij.model.search.LeafOccurrenceMapper import com.intellij.model.search.SearchContext import com.intellij.model.search.SearchService import com.intellij.openapi.application.runReadAction import com.intellij.refactoring.rename.api.PsiModifiableRenameUsage import com.intellij.refactoring.rename.api.RenameUsage import com.intellij.refactoring.rename.api.RenameUsageSearchParameters import com.intellij.refactoring.rename.api.RenameUsageSearcher import com.intellij.util.AbstractQuery import com.intellij.util.Processor import com.intellij.util.Query import com.jetbrains.php.lang.PhpLanguage private class PestCustomExpectationRenameUsageSearcher : RenameUsageSearcher { override fun collectSearchRequests(parameters: RenameUsageSearchParameters): Collection> { val targetSymbol = parameters.target as? PestCustomExpectationSymbol ?: return emptyList() val symbolPointer: Pointer = targetSymbol.createPointer() val usages = SearchService.getInstance() .searchWord(parameters.project, targetSymbol.expectationName) .caseSensitive(true) .inContexts(SearchContext.inCode()) .inFilesWithLanguage(PhpLanguage.INSTANCE) .inScope(parameters.searchScope) .buildQuery(LeafOccurrenceMapper.withPointer(symbolPointer, ::findReferencesToSymbol)) val selfUsage = PestCustomExtensionDeclarationUsageQuery( PsiModifiableRenameUsage.defaultPsiModifiableRenameUsage(targetSymbol.declarationUsage()) ) return listOf(usages.mapping { PsiModifiableRenameUsage.defaultPsiModifiableRenameUsage( PsiUsage.textUsage(it.element.containingFile, it.element.nameNode!!.textRange) ) }, selfUsage) } } internal class PestCustomExtensionDeclarationUsageQuery(private val targetDeclaration: T) : AbstractQuery() { override fun processResults(p0: Processor): Boolean { return runReadAction { p0.process(targetDeclaration) } } } ================================================ FILE: src/main/kotlin/com/pestphp/pest/features/customExpectations/symbols/PestCustomExpectationSymbol.kt ================================================ @file:Suppress("UnstableApiUsage") package com.pestphp.pest.features.customExpectations.symbols import com.intellij.find.usages.api.SearchTarget import com.intellij.find.usages.api.UsageHandler import com.intellij.model.Pointer import com.intellij.model.Symbol import com.intellij.openapi.util.NlsSafe import com.intellij.openapi.util.TextRange import com.intellij.platform.backend.navigation.NavigationRequest import com.intellij.platform.backend.navigation.NavigationTarget import com.intellij.platform.backend.presentation.TargetPresentation import com.intellij.psi.PsiFile import com.intellij.psi.search.GlobalSearchScope import com.intellij.psi.search.SearchScope import com.intellij.refactoring.rename.api.RenameTarget import com.pestphp.pest.features.customExpectations.generators.Method class PestCustomExpectationSymbol( @NlsSafe val expectationName: String, val file: PsiFile, val rangeInFile: TextRange, val methodDescriptor: Method ) : Symbol, NavigationTarget, SearchTarget, RenameTarget { override fun createPointer() = Pointer.fileRangePointer( file, TextRange(rangeInFile.startOffset - 1, rangeInFile.endOffset + 1) ) { restoredFile, restoredRange -> // pointer doesn't survive when element is of zero range PestCustomExpectationSymbol( expectationName, restoredFile, TextRange(restoredRange.startOffset + 1, restoredRange.endOffset - 1), methodDescriptor ) } override val maximalSearchScope: SearchScope get() = GlobalSearchScope.projectScope(file.project) override val targetName = expectationName override fun presentation() = computePresentation() override val usageHandler: UsageHandler get() = UsageHandler.createEmptyUsageHandler(expectationName) override fun equals(other: Any?) = (other as? PestCustomExpectationSymbol)?.let { expectationName == it.expectationName } ?: false override fun hashCode() = expectationName.hashCode() override fun computePresentation() = TargetPresentation.builder(expectationName).presentation() override fun navigationRequest() = NavigationRequest.sourceNavigationRequest(file, rangeInFile) } ================================================ FILE: src/main/kotlin/com/pestphp/pest/features/customExpectations/symbols/PestCustomExpectationSymbolDeclaration.kt ================================================ @file:Suppress("UnstableApiUsage") package com.pestphp.pest.features.customExpectations.symbols import com.intellij.model.psi.PsiSymbolDeclaration import com.intellij.openapi.util.TextRange import com.jetbrains.php.lang.psi.elements.StringLiteralExpression import com.pestphp.pest.features.customExpectations.generators.Method class PestCustomExpectationSymbolDeclaration(private val element: StringLiteralExpression, private val methodDescriptor: Method) : PsiSymbolDeclaration { override fun getDeclaringElement() = element override fun getRangeInDeclaringElement() = element.textRangeInParent override fun getSymbol(): PestCustomExpectationSymbol { val rangeInFile = TextRange.from(element.textRange.startOffset + 1, element.contents.length) return PestCustomExpectationSymbol(element.contents, element.containingFile, rangeInFile, methodDescriptor) } } ================================================ FILE: src/main/kotlin/com/pestphp/pest/features/customExpectations/symbols/PestCustomExpectationSymbolDeclarationProvider.kt ================================================ @file:Suppress("UnstableApiUsage") package com.pestphp.pest.features.customExpectations.symbols import com.intellij.model.psi.PsiSymbolDeclaration import com.intellij.model.psi.PsiSymbolDeclarationProvider import com.intellij.psi.PsiElement import com.jetbrains.php.lang.psi.elements.ParameterList import com.jetbrains.php.lang.psi.elements.StringLiteralExpression import com.jetbrains.php.lang.psi.elements.impl.MethodReferenceImpl import com.jetbrains.php.lang.psi.resolve.types.PhpType import com.pestphp.pest.features.customExpectations.toMethod class PestCustomExpectationSymbolDeclarationProvider : PsiSymbolDeclarationProvider { override fun getDeclarations(element: PsiElement, offsetInElement: Int): Collection { val possibleExtensionName = element as? StringLiteralExpression ?: return emptyList() if (possibleExtensionName.parent !is ParameterList || possibleExtensionName.contents.isEmpty()) return emptyList() val methodReference = possibleExtensionName.parent.parent as? MethodReferenceImpl ?: return emptyList() val methodDescriptor = methodReference.toMethod() ?: return emptyList() if (methodReference.name == "extend" && PhpType.intersectsGlobal(element.project, methodReference.classReference!!.type, PEST_EXPECTATION_TYPE) ) { return listOf(PestCustomExpectationSymbolDeclaration(element, methodDescriptor)) } return emptyList() } } ================================================ FILE: src/main/kotlin/com/pestphp/pest/features/customExpectations/symbols/PestCustomExpectationUsageSearcher.kt ================================================ @file:Suppress("UnstableApiUsage") package com.pestphp.pest.features.customExpectations.symbols import com.intellij.find.usages.api.PsiUsage import com.intellij.find.usages.api.Usage import com.intellij.find.usages.api.UsageSearchParameters import com.intellij.find.usages.api.UsageSearcher import com.intellij.model.Pointer import com.intellij.model.search.LeafOccurrence import com.intellij.model.search.LeafOccurrenceMapper import com.intellij.model.search.SearchContext import com.intellij.model.search.SearchService import com.intellij.openapi.util.TextRange import com.intellij.psi.PsiFile import com.intellij.util.Query import com.jetbrains.php.lang.PhpLanguage import com.jetbrains.php.lang.psi.PhpPsiUtil import com.jetbrains.php.lang.psi.elements.MethodReference class PestCustomExpectationUsageSearcher : UsageSearcher { override fun collectSearchRequests(parameters: UsageSearchParameters): Collection> { val targetSymbol = parameters.target as? PestCustomExpectationSymbol ?: return emptyList() val symbolPointer: Pointer = targetSymbol.createPointer() val usages = SearchService.getInstance() .searchWord(parameters.project, targetSymbol.expectationName).caseSensitive(true) .inContexts(SearchContext.inCode()).inFilesWithLanguage(PhpLanguage.INSTANCE) .inScope(parameters.searchScope) .buildQuery(LeafOccurrenceMapper.withPointer(symbolPointer, ::findReferencesToSymbol)) .mapping { PsiUsage.textUsage(it.element.containingFile, it.element.nameNode!!.textRange) } val selfUsage = PestCustomExtensionDeclarationUsageQuery(targetSymbol.declarationUsage()) return listOf(usages, selfUsage) } } fun PestCustomExpectationSymbol.declarationUsage() = PestCustomDeclarationUsage(file, rangeInFile) class PestCustomDeclarationUsage( override val file: PsiFile, override val range: TextRange ) : PsiUsage { override val declaration: Boolean get() = true override fun createPointer() = PsiUsage.textUsage(file, range).createPointer() } fun findReferencesToSymbol( symbol: PestCustomExpectationSymbol, leafOccurrence: LeafOccurrence ): Collection { val methodReference = PhpPsiUtil.getParentOfClass(leafOccurrence.start, MethodReference::class.java) if (methodReference?.nameNode == null) return emptyList() val possibleReference = PestCustomExpectationReference(methodReference) return if (possibleReference.resolvesTo(symbol)) listOf(possibleReference) else emptyList() } ================================================ FILE: src/main/kotlin/com/pestphp/pest/features/datasets/DatasetIndex.kt ================================================ package com.pestphp.pest.features.datasets import com.intellij.openapi.vfs.VirtualFile import com.intellij.util.indexing.DataIndexer import com.intellij.util.indexing.DefaultFileTypeSpecificInputFilter import com.intellij.util.indexing.FileBasedIndex import com.intellij.util.indexing.FileBasedIndexExtension import com.intellij.util.indexing.FileContent import com.intellij.util.indexing.ID import com.intellij.util.io.DataExternalizer import com.intellij.util.io.EnumeratorStringDescriptor import com.intellij.util.io.KeyDescriptor import com.jetbrains.php.lang.PhpFileType import com.jetbrains.php.lang.psi.PhpFile import com.pestphp.pest.features.customExpectations.externalizers.ListDataExternalizer import com.pestphp.pest.realPath val key = ID.create>("php.pest.datasets") /** * Indexes all pest datasets with the following key value store * `path/datasets/file => ['my-dataset', 'my-other-dataset'] */ class DatasetIndex : FileBasedIndexExtension>() { override fun getName(): ID> { return key } override fun getVersion(): Int { return 3 } override fun getIndexer(): DataIndexer, FileContent> { return DataIndexer { inputData -> val file = inputData.psiFile if (file !is PhpFile) { return@DataIndexer mapOf() } val datasets = file .getDatasets() .mapNotNull { it.getPestDatasetName() } if (datasets.isEmpty()) { return@DataIndexer mapOf() } mapOf( file.realPath to datasets ) } } override fun getKeyDescriptor(): KeyDescriptor { return EnumeratorStringDescriptor.INSTANCE } override fun getValueExternalizer(): DataExternalizer> { return ListDataExternalizer(EnumeratorStringDescriptor.INSTANCE) } override fun getInputFilter(): FileBasedIndex.InputFilter { return object : DefaultFileTypeSpecificInputFilter(PhpFileType.INSTANCE) { override fun acceptInput(file: VirtualFile): Boolean { return super.acceptInput(file) && file.parent.path.endsWith("/Datasets") } } } override fun dependsOnFileContent(): Boolean { return true } } ================================================ FILE: src/main/kotlin/com/pestphp/pest/features/datasets/DatasetReference.kt ================================================ package com.pestphp.pest.features.datasets import com.intellij.psi.PsiElement import com.intellij.psi.PsiElementResolveResult import com.intellij.psi.PsiManager import com.intellij.psi.PsiPolyVariantReference import com.intellij.psi.PsiReferenceBase import com.intellij.psi.ResolveResult import com.intellij.psi.search.GlobalSearchScope import com.intellij.util.indexing.FileBasedIndex import com.jetbrains.php.lang.psi.elements.StringLiteralExpression import com.jetbrains.php.lang.psi.elements.impl.FunctionReferenceImpl /** * Used to make a reference between a string and a dataset function call */ class DatasetReference( element: StringLiteralExpression ) : PsiReferenceBase(element), PsiPolyVariantReference { override fun resolve(): PsiElement? { return multiResolve(false).firstOrNull()?.element } override fun getVariants(): Array { return getAllDatasets() .mapNotNull { it.getPestDatasetName() } .toTypedArray() } override fun multiResolve(incompleteCode: Boolean): Array { val fileBasedIndex = FileBasedIndex.getInstance() val datasetName = element.contents val foundDatasets = mutableListOf() fileBasedIndex.getAllKeys( key, element.project ).forEach { key -> fileBasedIndex.processValues( com.pestphp.pest.features.datasets.key, key, null, { file, datasets -> if (datasetName !in datasets) { return@processValues true } // Add all shared datasets which matches PsiManager.getInstance(element.project).findFile(file)!! .getDatasets() .filter { it.getPestDatasetName() == datasetName } .forEach { foundDatasets.add(it) } true }, GlobalSearchScope.projectScope(element.project) ) } // Add all local datasets which matches element.containingFile .getDatasets() .filter { it.getPestDatasetName() == datasetName } .forEach { foundDatasets.add(it) } return foundDatasets.map { PsiElementResolveResult(it) } .toTypedArray() } private fun getAllDatasets(): MutableList { val fileBasedIndex = FileBasedIndex.getInstance() val foundDatasets = mutableListOf() fileBasedIndex.getAllKeys( key, element.project ).forEach { key -> fileBasedIndex.processValues( com.pestphp.pest.features.datasets.key, key, null, { file, _ -> // Add all datasets PsiManager.getInstance(element.project).findFile(file)!! .getDatasets() .forEach { foundDatasets.add(it) } true }, GlobalSearchScope.projectScope(element.project) ) } // Add all local datasets which matches element.containingFile .getDatasets() .forEach { foundDatasets.add(it) } return foundDatasets } } ================================================ FILE: src/main/kotlin/com/pestphp/pest/features/datasets/DatasetReferenceContributor.kt ================================================ package com.pestphp.pest.features.datasets import com.intellij.patterns.PlatformPatterns import com.intellij.psi.PsiReferenceContributor import com.intellij.psi.PsiReferenceRegistrar import com.jetbrains.php.lang.psi.elements.ParameterList import com.jetbrains.php.lang.psi.elements.StringLiteralExpression import com.jetbrains.php.lang.psi.elements.impl.MethodReferenceImpl /** * Used to register all dataset reference provider */ class DatasetReferenceContributor : PsiReferenceContributor() { override fun registerReferenceProviders(registrar: PsiReferenceRegistrar) { registrar.registerReferenceProvider( PlatformPatterns.psiElement(StringLiteralExpression::class.java) .withParents( ParameterList::class.java, MethodReferenceImpl::class.java, ), DatasetReferenceProvider() ) } } ================================================ FILE: src/main/kotlin/com/pestphp/pest/features/datasets/DatasetReferenceProvider.kt ================================================ package com.pestphp.pest.features.datasets import com.intellij.psi.PsiElement import com.intellij.psi.PsiReference import com.intellij.psi.PsiReferenceProvider import com.intellij.util.ProcessingContext import com.jetbrains.php.lang.psi.elements.MethodReference import com.jetbrains.php.lang.psi.elements.StringLiteralExpression /** * Adds goto and reference support to string literals referring datasets */ class DatasetReferenceProvider : PsiReferenceProvider() { override fun getReferencesByElement( element: PsiElement, context: ProcessingContext ): Array { if (element !is StringLiteralExpression) { return PsiReference.EMPTY_ARRAY } val methodReference = element.parent?.parent as? MethodReference ?: return PsiReference.EMPTY_ARRAY if (!methodReference.isDatasetCall()) return PsiReference.EMPTY_ARRAY return arrayOf( DatasetReference(element) ) } } ================================================ FILE: src/main/kotlin/com/pestphp/pest/features/datasets/DatasetUtil.kt ================================================ package com.pestphp.pest.features.datasets import com.intellij.psi.PsiElement import com.intellij.psi.PsiFile import com.intellij.psi.search.ProjectScope import com.intellij.util.indexing.FileBasedIndex import com.jetbrains.php.lang.psi.elements.MethodReference import com.jetbrains.php.lang.psi.elements.StringLiteralExpression import com.jetbrains.php.lang.psi.elements.impl.FunctionReferenceImpl import com.pestphp.pest.collectFromDescribeBlocks import com.pestphp.pest.getRootPhpPsiElements import com.pestphp.pest.isPestTestReference import com.pestphp.pest.realPath fun PsiFile.isIndexedPestDatasetFile(): Boolean { return FileBasedIndex.getInstance().getValues( key, this.realPath, ProjectScope.getProjectScope(this.project) ).isNotEmpty() } fun PsiElement?.isPestDataset(): Boolean { return when (this) { is FunctionReferenceImpl -> this.isPestDatasetFunction() else -> false } } fun FunctionReferenceImpl.isPestDatasetFunction(): Boolean { return this.canonicalText in setOf("dataset") } fun FunctionReferenceImpl.getPestDatasetName(): String? { return (getParameter(0) as? StringLiteralExpression)?.contents } fun MethodReference.isDatasetCall() : Boolean { if (name != "with") return false return isPestTestReference() } fun PsiFile.getDatasets(): List { return collectFromDescribeBlocks(this.getRootPhpPsiElements()) { element -> (element as? FunctionReferenceImpl)?.takeIf { it.isPestDataset() } } } ================================================ FILE: src/main/kotlin/com/pestphp/pest/features/datasets/InvalidDatasetNameCaseInspection.kt ================================================ package com.pestphp.pest.features.datasets import com.intellij.codeInspection.ProblemsHolder import com.intellij.modcommand.ActionContext import com.intellij.modcommand.ModPsiUpdater import com.intellij.modcommand.PsiUpdateModCommandAction import com.intellij.psi.PsiElementVisitor import com.jetbrains.php.lang.inspections.PhpInspection import com.jetbrains.php.lang.psi.PhpFile import com.jetbrains.php.lang.psi.PhpPsiElementFactory import com.jetbrains.php.lang.psi.elements.FunctionReference import com.jetbrains.php.lang.psi.elements.MethodReference import com.jetbrains.php.lang.psi.elements.StringLiteralExpression import com.jetbrains.php.lang.psi.elements.impl.FunctionReferenceImpl import com.jetbrains.php.lang.psi.visitors.PhpElementVisitor import com.pestphp.pest.PestBundle import com.pestphp.pest.getInitialFunctionReference import com.pestphp.pest.getRootPhpPsiElements import com.pestphp.pest.goto.getDatasetUsages import com.pestphp.pest.inspections.convertTestNameToSentenceCase import com.pestphp.pest.inspections.isInvalidNameCase class InvalidDatasetNameCaseInspection : PhpInspection() { override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor { return object : PhpElementVisitor() { override fun visitPhpFile(file: PhpFile) { val localDatasets = file.getRootPhpPsiElements() .filter { it.isPestDataset() } .filterIsInstance() localDatasets.groupBy { it.getPestDatasetName() } .filterKeys { datasetName -> datasetName != null && isInvalidNameCase(datasetName) } .forEach { declareProblemType(holder, it.value) } } } } private fun declareProblemType(holder: ProblemsHolder, datasets: List) { datasets.mapNotNull { it.getInitialFunctionReference()?.getParameter(0) } .filterIsInstance() .forEach { holder.problem(it, PestBundle.message("INSPECTION_INVALID_DATASET_NAME_CASE")) .fix(ChangeDatasetNameCasingQuickFix(it)) .register() } } private class ChangeDatasetNameCasingQuickFix( datasetDeclarationName: StringLiteralExpression ) : PsiUpdateModCommandAction(datasetDeclarationName) { override fun getFamilyName(): String { return PestBundle.message("QUICK_FIX_CHANGE_DATASET_NAME_CASING") } override fun invoke(context: ActionContext, datasetNamePsiElement: StringLiteralExpression, updater: ModPsiUpdater) { val sentenceCaseDatasetName = convertTestNameToSentenceCase(datasetNamePsiElement.contents) val newNameParameter = PhpPsiElementFactory.createStringLiteralExpression( datasetNamePsiElement.project, sentenceCaseDatasetName, true ) val datasetUsages = getDatasetUsages(datasetNamePsiElement)?.map { updater.getWritable(it) } datasetUsages?.forEach { val testWithDataset = it as? MethodReference ?: return@forEach val nameParameter = testWithDataset.getParameter(0) as? StringLiteralExpression ?: return@forEach nameParameter.replace(newNameParameter.copy()) } datasetNamePsiElement.replace(newNameParameter.copy()) } } } ================================================ FILE: src/main/kotlin/com/pestphp/pest/features/datasets/InvalidDatasetReferenceInspection.kt ================================================ package com.pestphp.pest.features.datasets import com.intellij.codeInspection.LocalQuickFix import com.intellij.codeInspection.ProblemsHolder import com.intellij.psi.PsiElementVisitor import com.intellij.psi.search.GlobalSearchScope import com.intellij.util.indexing.FileBasedIndex import com.jetbrains.php.lang.inspections.PhpInspection import com.jetbrains.php.lang.psi.PhpFile import com.jetbrains.php.lang.psi.elements.StringLiteralExpression import com.jetbrains.php.lang.psi.elements.impl.MethodReferenceImpl import com.jetbrains.php.lang.psi.visitors.PhpElementVisitor import com.pestphp.pest.PestBundle import com.pestphp.pest.getPestTests class InvalidDatasetReferenceInspection : PhpInspection() { override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor { return object : PhpElementVisitor() { override fun visitPhpFile(file: PhpFile) { val localDatasets = file.getDatasets() .mapNotNull { it.getPestDatasetName() } // Get all shared datasets val fileBasedIndex = FileBasedIndex.getInstance() val sharedDatasets = fileBasedIndex.getAllKeys(key, file.project) .map { fileBasedIndex.getValues( key, it, GlobalSearchScope.projectScope(file.project) ) } .flatten() .flatten() file.getPestTests() // Has to be a method reference, as else there is no dataset .asSequence() .filterIsInstance() .filter { it.name == "with" } .mapNotNull { it.parameters.getOrNull(0) } .filterIsInstance() .filter { it.contents !in localDatasets && it.contents !in sharedDatasets } .toList() .forEach { declareProblemType( holder, it ) } } } } @Suppress("SpreadOperator") private fun declareProblemType(holder: ProblemsHolder, datasetName: StringLiteralExpression) { holder.registerProblem( datasetName, PestBundle.message("INSPECTION_INVALID_DATASET_REFERENCE"), *LocalQuickFix.EMPTY_ARRAY ) } } ================================================ FILE: src/main/kotlin/com/pestphp/pest/features/parallel/PestParallelProgramRunner.kt ================================================ package com.pestphp.pest.features.parallel import com.intellij.execution.ExecutionException import com.intellij.execution.configurations.RunConfiguration import com.intellij.execution.configurations.RunProfile import com.intellij.execution.configurations.RunProfileState import com.intellij.execution.configurations.RunnerSettings import com.intellij.execution.process.ProcessEvent import com.intellij.execution.process.ProcessListener import com.intellij.execution.runners.ExecutionEnvironment import com.intellij.execution.runners.GenericProgramRunner import com.intellij.execution.runners.RunContentBuilder import com.intellij.execution.testframework.sm.runner.SMTestProxy.SMRootTestProxy import com.intellij.execution.testframework.sm.runner.ui.SMTRunnerConsoleView import com.intellij.execution.ui.RunContentDescriptor import com.intellij.notification.NotificationType import com.intellij.openapi.fileEditor.FileDocumentManager import com.intellij.openapi.util.NlsSafe import com.jetbrains.php.PhpBundle import com.jetbrains.php.config.commandLine.PhpCommandSettings import com.jetbrains.php.config.commandLine.PhpCommandSettingsBuilder import com.jetbrains.php.phpunit.PhpUnitUtil import com.jetbrains.php.testFramework.PhpTestFrameworkSettingsManager import com.pestphp.pest.PestBundle import com.pestphp.pest.PestFrameworkType import com.pestphp.pest.configuration.PestRerunProfile import com.pestphp.pest.configuration.PestRunConfiguration import com.pestphp.pest.configuration.PestVersionDetector import com.pestphp.pest.statistics.PestUsagesCollector internal val PEST_PARALLEL_ARGUMENTS = mutableListOf("--parallel", "--log-teamcity", "php://stdout") class PestParallelProgramRunner : GenericProgramRunner() { companion object { const val RUNNER_ID: String = "PestParallelRunner" } override fun canRun(executorId: String, profile: RunProfile): Boolean = executorId == PestParallelTestExecutor.EXECUTOR_ID && profile is PestRunConfiguration override fun doExecute(state: RunProfileState, environment: ExecutionEnvironment): RunContentDescriptor? { PestUsagesCollector.logParallelTestExecution(environment.project) val executionResult = if (environment.runProfile is PestRerunProfile) { state.execute(environment.executor, this) } else { val runConfiguration = environment.runProfile as? PestRunConfiguration ?: throw ExecutionException(PestBundle.message("PEST_PARALLEL_IS_NOT_SUPPORTED_FOR_SELECTED_RUN_PROFILE")) val command = createPestParallelCommand(runConfiguration) runConfiguration.checkAndGetState(environment, command)?.execute(environment.executor, this) } if (executionResult == null) throw ExecutionException(PhpBundle.message("execution.result.is.null")) val contentDescriptor = RunContentBuilder(executionResult, environment).showRunContent(environment.contentToReuse) postprocessExecutionResult(contentDescriptor, environment, PestBundle.message("PARALLEL_TESTING_IS_SUPPORTED_FROM_VERSION_2")) return contentDescriptor } override fun getRunnerId(): String = RUNNER_ID fun getArguments(): MutableList = PEST_PARALLEL_ARGUMENTS } internal fun createPestParallelCommand(runConfiguration: PestRunConfiguration): PhpCommandSettings { FileDocumentManager.getInstance().saveAllDocuments() val interpreter = runConfiguration.interpreter ?: throw ExecutionException(PhpCommandSettingsBuilder.getInterpreterNotFoundError()) return runConfiguration.createCommand( interpreter, mutableMapOf(), if (executeInParallel(runConfiguration)) mutableListOf() else PEST_PARALLEL_ARGUMENTS, false ) } fun postprocessExecutionResult( contentDescriptor: RunContentDescriptor, environment: ExecutionEnvironment, @NlsSafe versionRequirement: String, ) { val processHandler = contentDescriptor.processHandler processHandler?.addProcessListener(object : ProcessListener { override fun processTerminated(event: ProcessEvent) { val executionConsole = contentDescriptor.executionConsole as? SMTRunnerConsoleView ?: return val rootProxy = executionConsole.resultsViewer.root as? SMRootTestProxy ?: return if (rootProxy.isEmptySuite && !rootProxy.isTestsReporterAttached) { handleEmptySuite(environment, versionRequirement) } } }) } private fun handleEmptySuite( environment: ExecutionEnvironment, @NlsSafe versionRequirement: String, ) { val profile = environment.runProfile as PestRunConfiguration val project = profile.project val interpreter = profile.interpreter ?: return val config = PhpTestFrameworkSettingsManager.getInstance(project).getOrCreateByInterpreter( PestFrameworkType.instance, interpreter, profile.getBaseFile(null, interpreter), true ) ?: return val version = if (!interpreter.isRemote) { PestVersionDetector.instance.getVersion(project, interpreter, config.executablePath) } else { null } createAndShowNotification(environment, versionRequirement, version) } private fun createAndShowNotification( environment: ExecutionEnvironment, @NlsSafe versionRequirement: String, version: String?, ) { PhpUnitUtil.getNotificationGroup().createNotification( versionRequirement, NotificationType.ERROR ).apply { version?.let { setTitle(PestBundle.message("CURRENT_PEST_VERSION_IS", it)) } setSuggestionType(true) notify(environment.project) } } internal fun executeInParallel(runConfiguration: RunConfiguration): Boolean { return runConfiguration is PestRunConfiguration && runConfiguration.pestSettings.pestRunnerSettings.parallelTestingEnabled } fun addParallelArguments(runConfiguration: PestRunConfiguration, command: PhpCommandSettings) { if (executeInParallel(runConfiguration)) { PestUsagesCollector.logParallelTestExecution(runConfiguration.project) command.addArguments(PEST_PARALLEL_ARGUMENTS) } } ================================================ FILE: src/main/kotlin/com/pestphp/pest/features/parallel/PestParallelSMTEventsAdapter.kt ================================================ package com.pestphp.pest.features.parallel import com.intellij.execution.testframework.sm.runner.SMTRunnerEventsAdapter import com.intellij.execution.testframework.sm.runner.SMTestProxy class PestParallelSMTEventsAdapter : SMTRunnerEventsAdapter() { override fun onSuiteStarted(suite: SMTestProxy) { suite.setPresentableName(convertSuiteNameToClassName(suite.name)) } override fun onTestStarted(test: SMTestProxy) { test.setPresentableName(convertRuntimeTestNameToRealTestName(test.name)) } } private const val PLACEHOLDER = " " internal fun convertRuntimeTestNameToRealTestName(runtimeTestName: String): String = runtimeTestName .removePrefix("__pest_evaluable_") .replace("__→_", " → ") .replace("__", PLACEHOLDER) .replace("_", " ") .replace(PLACEHOLDER, "_") private fun convertSuiteNameToClassName(suiteName: String): String = suiteName.removePrefix("P\\") ================================================ FILE: src/main/kotlin/com/pestphp/pest/features/parallel/PestParallelTestExecutor.kt ================================================ package com.pestphp.pest.features.parallel import com.intellij.execution.Executor import com.intellij.icons.AllIcons import com.intellij.openapi.util.IconLoader.getDisabledIcon import com.intellij.openapi.util.text.StringUtil import com.intellij.openapi.util.text.TextWithMnemonic import com.intellij.openapi.wm.ToolWindowId import com.jetbrains.php.PhpIcons import com.pestphp.pest.PestBundle import org.jetbrains.annotations.Nls import org.jetbrains.annotations.NonNls import javax.swing.Icon class PestParallelTestExecutor : Executor() { companion object { const val EXECUTOR_ID: @NonNls String = "PestParallelTestExecutor" const val CONTEXT_ACTION_ID: @NonNls String = "PestParallelRun" } override fun getToolWindowId(): String = ToolWindowId.RUN override fun getToolWindowIcon(): Icon = AllIcons.Toolwindows.ToolWindowRun override fun getIcon(): Icon = PhpIcons.RUN_PARA_TEST override fun getRerunIcon(): Icon = AllIcons.Actions.Rerun override fun getDisabledIcon(): Icon = getDisabledIcon(icon) override fun getDescription(): String = PestBundle.message("ACTION_RUN_SELECTED_CONFIGURATION_WITH_PARALLEL_DESCRIPTION") override fun getActionName(): String = PestBundle.message("ACTION_PEST_PARALLEL_TEXT") override fun getId(): String = EXECUTOR_ID override fun getStartActionText(): @Nls(capitalization = Nls.Capitalization.Title) String = PestBundle.message("RUN_PEST_WITH_PARALLEL") override fun getStartActionText(configurationName: String): String { val configName = if (StringUtil.isEmpty(configurationName)) "" else " '${shortenNameIfNeeded(configurationName)}'" return TextWithMnemonic.parse(PestBundle.message("RUN_S_WITH_PARALLEL")).replaceFirst("%s", configName).toString() } override fun getContextActionId(): String = CONTEXT_ACTION_ID override fun getHelpId(): String? = null } ================================================ FILE: src/main/kotlin/com/pestphp/pest/features/snapshotTesting/SnapshotLineMarkerProvider.kt ================================================ package com.pestphp.pest.features.snapshotTesting import com.intellij.codeInsight.daemon.RelatedItemLineMarkerInfo import com.intellij.codeInsight.daemon.RelatedItemLineMarkerProvider import com.intellij.codeInsight.navigation.NavigationGutterIconBuilder import com.intellij.icons.AllIcons import com.intellij.psi.PsiElement import com.jetbrains.php.lang.lexer.PhpTokenTypes import com.jetbrains.php.lang.psi.PhpPsiUtil import com.jetbrains.php.lang.psi.elements.PhpUse import com.jetbrains.php.lang.psi.elements.impl.FunctionReferenceImpl import com.pestphp.pest.PestBundle private class SnapshotLineMarkerProvider : RelatedItemLineMarkerProvider() { override fun collectNavigationMarkers( element: PsiElement, result: MutableCollection>, ) { if (!PhpPsiUtil.isOfType(element, PhpTokenTypes.IDENTIFIER)) { return } val functionReference = element.parent as? FunctionReferenceImpl ?: return if (!functionReference.isSnapshotAssertionCall) { return } if (functionReference.parent is PhpUse) { return } val snapshotFiles = functionReference.snapshotFiles val builder = NavigationGutterIconBuilder.create(AllIcons.Nodes.DataSchema) .setTargets(snapshotFiles) .setTooltipText(PestBundle.message("TOOLTIP_NAVIGATE_TO_SNAPSHOT_FILES")) result.add(builder.createLineMarkerInfo(element)) } } ================================================ FILE: src/main/kotlin/com/pestphp/pest/features/snapshotTesting/SnapshotUtil.kt ================================================ package com.pestphp.pest.features.snapshotTesting import com.intellij.openapi.project.guessProjectDir import com.intellij.openapi.roots.ProjectFileIndex import com.intellij.psi.PsiFile import com.intellij.psi.PsiManager import com.intellij.psi.util.parentOfType import com.jetbrains.php.codeInsight.controlFlow.PhpControlFlowUtil import com.jetbrains.php.codeInsight.controlFlow.PhpInstructionProcessor import com.jetbrains.php.codeInsight.controlFlow.instructions.PhpCallInstruction import com.jetbrains.php.lang.psi.elements.impl.FunctionImpl import com.jetbrains.php.lang.psi.elements.impl.FunctionReferenceImpl import com.pestphp.pest.getPestTestName import com.pestphp.pest.isPestTestReference val snapshotAssertionNames = listOf( "assertMatchesSnapshot", "assertMatchesFileHashSnapshot", "assertMatchesFileSnapshot", "assertMatchesHtmlSnapshot", "assertMatchesJsonSnapshot", "assertMatchesObjectSnapshot", "assertMatchesTextSnapshot", "assertMatchesXmlSnapshot", "assertMatchesYamlSnapshot", ) val FunctionReferenceImpl.isSnapshotAssertionCall: Boolean get() { return snapshotAssertionNames.contains(this.name) } val FunctionReferenceImpl.snapshotFiles: List get() { val pestBody = this.parentOfType() ?: return emptyList() // Make sure we are inside a pest test val pestTestReference = pestBody.parent?.parent?.parent ?: return emptyList() if (!pestTestReference.isPestTestReference()) { return emptyList() } val snapshotDirectory = this.project.guessProjectDir() ?.findFileByRelativePath("tests/__snapshots__") ?: return emptyList() val testFileName = this.containingFile.name.removeSuffix(".php") val testName = pestTestReference.getPestTestName() ?: return emptyList() val snapshotFiles = mutableListOf() val snapshotCalls = pestBody.getSnapshotCallNumber(this) ProjectFileIndex.getInstance(this.project) .iterateContentUnderDirectory( snapshotDirectory ) { val psiFile = PsiManager.getInstance(this.project) .findFile(it) ?: return@iterateContentUnderDirectory true if (!psiFile.isSnapshotFile( testName, testFileName, snapshotCalls ) ) { return@iterateContentUnderDirectory true } snapshotFiles.add(psiFile) true } return snapshotFiles } private fun PsiFile.isSnapshotFile(testName: String, testFileName: String, snapshotCall: Int): Boolean { val snapshotFileName = this.virtualFile.nameWithoutExtension this.virtualFile.extension ?: return false val testNameUnderscore = testName.replace(' ', '_') if (!snapshotFileName.startsWith("${testFileName}__$testNameUnderscore")) { return false } if (!snapshotFileName.endsWith("__$snapshotCall")) { return false } return true } private fun FunctionImpl.getSnapshotCallNumber(snapshotFunctionReference: FunctionReferenceImpl): Int { var snapshotCalls = 0 val processor: PhpInstructionProcessor = object : PhpInstructionProcessor() { override fun processPhpCallInstruction(instruction: PhpCallInstruction): Boolean { val functionReference = instruction.functionReference if (functionReference !is FunctionReferenceImpl) { return super.processPhpCallInstruction(instruction) } if (!functionReference.isSnapshotAssertionCall) { return super.processPhpCallInstruction(instruction) } snapshotCalls++ if (PsiManager.getInstance(functionReference.project).areElementsEquivalent( functionReference, snapshotFunctionReference ) ) { return false } return super.processPhpCallInstruction(instruction) } } val flow = controlFlow PhpControlFlowUtil.processSuccessors(flow.entryPoint, false, processor) return snapshotCalls } ================================================ FILE: src/main/kotlin/com/pestphp/pest/goto/PestDatasetUsagesGotoHandler.kt ================================================ package com.pestphp.pest.goto import com.intellij.codeInsight.navigation.actions.GotoDeclarationHandler import com.intellij.openapi.editor.Editor import com.intellij.psi.PsiElement import com.intellij.psi.PsiFile import com.intellij.psi.search.GlobalSearchScope import com.intellij.psi.search.PsiSearchHelper import com.intellij.psi.util.PsiTreeUtil import com.intellij.util.Processor import com.jetbrains.php.lang.psi.elements.MethodReference import com.jetbrains.php.lang.psi.elements.StringLiteralExpression import com.jetbrains.php.lang.psi.elements.impl.FunctionReferenceImpl import com.pestphp.pest.features.datasets.isDatasetCall import com.pestphp.pest.features.datasets.isPestDatasetFunction fun getDatasetUsages(literal: StringLiteralExpression): Array? { val function = literal.parent?.parent as? FunctionReferenceImpl ?: return null if (!function.isPestDatasetFunction()) return null val searchHelper = PsiSearchHelper.getInstance(literal.project) val result = mutableListOf() val datasetName = literal.contents val processor = Processor { psiFile: PsiFile -> result.addAll( PsiTreeUtil.findChildrenOfType(psiFile, StringLiteralExpression::class.java).filter { it.contents == datasetName }.mapNotNull { it.parent?.parent as? MethodReference }.filter { it.isDatasetCall() } ) true } searchHelper.processAllFilesWithWordInLiterals( datasetName, GlobalSearchScope.allScope(literal.project), processor, ) return result.toTypedArray() } class PestDatasetUsagesGotoHandler : GotoDeclarationHandler { override fun getGotoDeclarationTargets( sourceElement: PsiElement?, offset: Int, editor: Editor? ): Array? { val literal = sourceElement?.parent as? StringLiteralExpression ?: return null return getDatasetUsages(literal) } } ================================================ FILE: src/main/kotlin/com/pestphp/pest/goto/PestGotoTargetPresentationProvider.kt ================================================ package com.pestphp.pest.goto import com.intellij.codeInsight.navigation.GotoTargetPresentationProvider import com.intellij.openapi.util.NlsSafe import com.intellij.platform.backend.presentation.TargetPresentation import com.intellij.psi.PsiElement import com.pestphp.pest.PestIcons import com.pestphp.pest.getPestTestName import com.pestphp.pest.isPestTestReference class PestGotoTargetPresentationProvider: GotoTargetPresentationProvider { override fun getTargetPresentation(element: PsiElement, differentNames: Boolean): TargetPresentation? { if (element.isPestTestReference()) { @NlsSafe val pestTestName = element.getPestTestName() return TargetPresentation.builder(pestTestName ?: element.containingFile.name) .containerText(element.containingFile?.presentation?.locationString) .icon(PestIcons.Logo) .presentation() } return null } } ================================================ FILE: src/main/kotlin/com/pestphp/pest/goto/PestTestFinder.kt ================================================ package com.pestphp.pest.goto import com.intellij.openapi.util.Pair import com.intellij.psi.PsiElement import com.intellij.psi.PsiFile import com.intellij.psi.search.GlobalSearchScope import com.intellij.psi.util.PsiTreeUtil import com.intellij.testIntegration.TestFinder import com.intellij.testIntegration.TestFinderHelper import com.intellij.util.indexing.FileBasedIndex import com.jetbrains.php.PhpIndex import com.jetbrains.php.lang.psi.elements.FunctionReference import com.jetbrains.php.lang.psi.elements.Method import com.jetbrains.php.lang.psi.elements.PhpClass import com.pestphp.pest.getPestTestName import com.pestphp.pest.getPestTests import com.pestphp.pest.indexers.key import com.pestphp.pest.inspections.convertTestNameToSentenceCase import com.pestphp.pest.isPestTestFile class PestTestFinder : TestFinder { /** * @return methods if the given element is a psi child of Pest function call, * classes otherwise */ override fun findClassesForTest(element: PsiElement): Collection { val classes = PhpIndex.getInstance(element.project) .getClassesByNameInScope( element.containingFile.name.removeSuffix("Test.php"), GlobalSearchScope.projectScope(element.project) ) val testName = PsiTreeUtil.getNonStrictParentOfType(element, FunctionReference::class.java) ?.getPestTestName() ?.split(" ") ?.joinToString("") ?: return classes val methodsAndProximityScores = classes.flatMap { phpClass -> phpClass.ownMethods.toList() } .filter { method -> testName.contains(method.name, ignoreCase = true) } .map { method -> Pair(method, TestFinderHelper.calcTestNameProximity(method.name, testName)) } return if (!methodsAndProximityScores.isEmpty()) TestFinderHelper.getSortedElements(methodsAndProximityScores, true) else classes } override fun findSourceElement(from: PsiElement): PsiElement? { return from.containingFile } override fun isTest(element: PsiElement): Boolean { if (element is PhpClass) return false return element.containingFile.isPestTestFile() } override fun findTestsForClass(element: PsiElement): Collection { val parent = PsiTreeUtil.getNonStrictParentOfType(element, PhpClass::class.java, Method::class.java) ?: return arrayListOf() return when (parent) { is PhpClass -> findTestFilesForClass(parent) is Method -> findTestsForMethod(parent) else -> arrayListOf() } } private fun findTestsForMethod(method: Method): List { val phpClass = method.containingClass ?: return emptyList() val sentenceCaseMethodName = convertTestNameToSentenceCase(method.name) val testsAndProximityScores = findTestFilesForClass(phpClass) .flatMap { psiFile -> psiFile.getPestTests().mapNotNull { test -> val testName = test.getPestTestName() ?: return@mapNotNull null val sentenceCaseTestName = if (testName.contains(' ')) testName else convertTestNameToSentenceCase(testName) if (sentenceCaseTestName.contains(sentenceCaseMethodName, ignoreCase = true)) { Pair(test, TestFinderHelper.calcTestNameProximity(sentenceCaseMethodName, sentenceCaseTestName)) } else { null } } } return testsAndProximityScores.sortedBy { it.second }.map { it.first } } private fun findTestFilesForClass(phpClass: PhpClass): List { return FileBasedIndex.getInstance().getAllKeys( key, phpClass.project ).filter { testClassName -> testClassName.contains(phpClass.name) } .flatMap { testClassName -> FileBasedIndex.getInstance().getContainingFiles( key, testClassName, GlobalSearchScope.projectScope(phpClass.project) ) } .mapNotNull { testFile -> phpClass.manager.findFile(testFile) } } } ================================================ FILE: src/main/kotlin/com/pestphp/pest/goto/PestTestGoToSymbolContributor.kt ================================================ package com.pestphp.pest.goto import com.intellij.ide.projectView.PresentationData import com.intellij.navigation.ChooseByNameContributor import com.intellij.navigation.ItemPresentation import com.intellij.navigation.NavigationItem import com.intellij.openapi.project.Project import com.intellij.psi.PsiManager import com.intellij.psi.search.ProjectScope import com.intellij.util.indexing.FileBasedIndex import com.jetbrains.php.PhpPresentationUtil import com.jetbrains.php.lang.psi.elements.FunctionReference import com.pestphp.pest.PestIcons import com.pestphp.pest.getPestTestName import com.pestphp.pest.getPestTests import com.pestphp.pest.indexers.key /** * Adds support for navigating to pest tests via the symbol searching */ class PestTestGoToSymbolContributor : ChooseByNameContributor { override fun getNames(project: Project, includeNonProjectItems: Boolean): Array { val index = FileBasedIndex.getInstance() return index .getAllKeys(key, project) .flatMap { index.getValues( key, it, when { includeNonProjectItems -> ProjectScope.getAllScope(project) else -> ProjectScope.getProjectScope(project) } ) } .flatten() .distinct() .toTypedArray() } override fun getItemsByName( name: String, pattern: String, project: Project, includeNonProjectItems: Boolean ): Array { val index = FileBasedIndex.getInstance() val psiManager = PsiManager.getInstance(project) return index .getAllKeys(key, project) .flatMap { fileName -> val hasName = index.getValues( key, fileName, when { includeNonProjectItems -> ProjectScope.getAllScope(project) else -> ProjectScope.getProjectScope(project) } ).flatten() .contains(name) if (!hasName) { return@flatMap emptyList() } index.getContainingFiles( key, fileName, when { includeNonProjectItems -> ProjectScope.getAllScope(project) else -> ProjectScope.getProjectScope(project) } ) }.mapNotNull { psiManager.findFile(it) } .flatMap { it.getPestTests() } .filter { it.getPestTestName().equals(name) } .map { functionReference -> val location = PhpPresentationUtil.getPresentablePathForFile( functionReference.containingFile.virtualFile, functionReference.project ) val presentation = PresentationData( functionReference.getPestTestName(), location, PestIcons.Logo, null, ) PestTestFunctionReference(functionReference, presentation) } .toTypedArray() } class PestTestFunctionReference(private val functionReference: FunctionReference, private val presentation: ItemPresentation) : NavigationItem { override fun getPresentation(): ItemPresentation = presentation override fun navigate(requestFocus: Boolean) = functionReference.navigate(requestFocus) override fun canNavigate(): Boolean = functionReference.canNavigate() override fun canNavigateToSource(): Boolean = canNavigateToSource() override fun getName(): String? { return functionReference.getPestTestName() } } } ================================================ FILE: src/main/kotlin/com/pestphp/pest/indexers/PestTestIndex.kt ================================================ package com.pestphp.pest.indexers import com.intellij.openapi.vfs.VirtualFile import com.intellij.util.indexing.DataIndexer import com.intellij.util.indexing.DefaultFileTypeSpecificInputFilter import com.intellij.util.indexing.FileBasedIndex import com.intellij.util.indexing.FileBasedIndexExtension import com.intellij.util.indexing.FileContent import com.intellij.util.indexing.ID import com.intellij.util.io.DataExternalizer import com.intellij.util.io.EnumeratorStringDescriptor import com.intellij.util.io.KeyDescriptor import com.jetbrains.php.lang.PhpFileType import com.jetbrains.php.lang.psi.stubs.indexes.StringSetDataExternalizer import com.pestphp.pest.getPestTestName import com.pestphp.pest.getPestTests import com.pestphp.pest.isPestTestFile import com.pestphp.pest.realPath val key = ID.create>("php.pest") /** * Indexes all pest test files with the following key-value store * `path/pest-test-file-name => ['it test', 'it should work']` * Note that php files with pest-like named functions are indexed as well */ class PestTestIndex : FileBasedIndexExtension>() { override fun getName(): ID> { return key } override fun getVersion(): Int { return 5 } override fun dependsOnFileContent(): Boolean { return true } override fun getIndexer(): DataIndexer, FileContent> { return DataIndexer { inputData -> val file = inputData.psiFile if (!file.isPestTestFile()) { return@DataIndexer mapOf() } val map = HashMap>() map[file.realPath] = file.getPestTests() .mapNotNull { it.getPestTestName() } .toSet() return@DataIndexer map } } override fun getInputFilter(): FileBasedIndex.InputFilter { return object : DefaultFileTypeSpecificInputFilter(PhpFileType.INSTANCE) { override fun acceptInput(file: VirtualFile): Boolean { return super.acceptInput(file) && file.path.lowercase().contains("test") } } } override fun getKeyDescriptor(): KeyDescriptor { return EnumeratorStringDescriptor.INSTANCE } override fun getValueExternalizer(): DataExternalizer> { return StringSetDataExternalizer.INSTANCE } } ================================================ FILE: src/main/kotlin/com/pestphp/pest/inspections/ChangeMultipleExpectCallsToChainableQuickFix.kt ================================================ package com.pestphp.pest.inspections import com.intellij.codeInspection.LocalQuickFix import com.intellij.codeInspection.ProblemDescriptor import com.intellij.openapi.project.Project import com.jetbrains.php.lang.psi.PhpPsiElementFactory import com.jetbrains.php.lang.psi.elements.MethodReference import com.jetbrains.php.lang.psi.elements.Statement import com.pestphp.pest.PestBundle import com.pestphp.pest.features.customExpectations.isExpectation class ChangeMultipleExpectCallsToChainableQuickFix : LocalQuickFix { override fun getFamilyName(): String { return PestBundle.message("QUICK_FIX_CHANGE_MULTIPLE_EXPECT_INTO_CHAIN") } override fun applyFix(project: Project, descriptor: ProblemDescriptor) { var statement = descriptor.psiElement as? Statement ?: return var expectCall = statement.firstPsiChild as? MethodReference ?: return var replaceExpectCall = expectCall if (!expectCall.isExpectation()) { return } // Find the first expect call in the group while ((statement.prevPsiSibling as? Statement)?.isExpectation() == true) { statement = statement.prevPsiSibling as Statement expectCall = statement.firstPsiChild as MethodReference replaceExpectCall = expectCall } // Loop over all the next statement and merge together to one expect cal.. var nextSibling = statement.nextPsiSibling as? Statement while (nextSibling != null) { val siblingMethodReference = nextSibling.firstPsiChild as? MethodReference ?: break if (!siblingMethodReference.isExpectation()) { break } // Replace expect with and on the next call. replaceExpectCall = PhpPsiElementFactory.createMethodReference( project, replaceExpectCall.text + "\n->" + siblingMethodReference.text.replaceFirst("expect", "and") ) val oldSibling = nextSibling nextSibling = nextSibling.nextPsiSibling as? Statement oldSibling.delete() } expectCall.replace(replaceExpectCall) } } ================================================ FILE: src/main/kotlin/com/pestphp/pest/inspections/ChangeTestNameCasingQuickFix.kt ================================================ package com.pestphp.pest.inspections import com.intellij.codeInspection.LocalQuickFix import com.intellij.codeInspection.ProblemDescriptor import com.intellij.openapi.project.Project import com.intellij.util.text.NameUtilCore import com.jetbrains.php.lang.psi.PhpPsiElementFactory import com.jetbrains.php.lang.psi.elements.StringLiteralExpression import com.pestphp.pest.PestBundle fun convertTestNameToSentenceCase( name: String, shouldLowercaseWords: Boolean = true ) = NameUtilCore.splitNameIntoWordList(name).fold("") { acc, element -> val word = if (shouldLowercaseWords) element.replaceFirstChar(Char::lowercase) else element if (acc.lastOrNull()?.isLetterOrDigit() != true || word.length == 1 && !word[0].isLetterOrDigit()) "$acc$word" else "$acc $word" } fun isInvalidNameCase(name: String) = !name.contains(' ') && convertTestNameToSentenceCase(name, false) != name class ChangeTestNameCasingQuickFix : LocalQuickFix { override fun getFamilyName(): String { return PestBundle.message("QUICK_FIX_CHANGE_TEST_NAME_CASING") } override fun applyFix(project: Project, descriptor: ProblemDescriptor) { val nameParameter = descriptor.psiElement as? StringLiteralExpression ?: return val pestTestName = nameParameter.contents val newNameParameter = PhpPsiElementFactory.createStringLiteralExpression( project, convertTestNameToSentenceCase(pestTestName), true ) nameParameter.replace(newNameParameter) } } ================================================ FILE: src/main/kotlin/com/pestphp/pest/inspections/InvalidTestNameCaseInspection.kt ================================================ package com.pestphp.pest.inspections import com.intellij.codeInspection.ProblemsHolder import com.intellij.psi.PsiElementVisitor import com.jetbrains.php.lang.inspections.PhpInspection import com.jetbrains.php.lang.psi.PhpFile import com.jetbrains.php.lang.psi.elements.FunctionReference import com.jetbrains.php.lang.psi.visitors.PhpElementVisitor import com.pestphp.pest.PestBundle import com.pestphp.pest.getInitialFunctionReference import com.pestphp.pest.getPestTestName import com.pestphp.pest.getPestTests class InvalidTestNameCaseInspection : PhpInspection() { override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor { return object : PhpElementVisitor() { override fun visitPhpFile(file: PhpFile) { file.getPestTests() .groupBy { it.getPestTestName() } .filterKeys { it != null } .filterKeys { // Remove `it ` prefix from test names val testName = if (it!!.startsWith("it ")) it.substring(3) else it isInvalidNameCase(testName) } .forEach { declareProblemType(holder, it.value) } } } } private fun declareProblemType(holder: ProblemsHolder, tests: List) { tests .mapNotNull { it.getInitialFunctionReference()?.getParameter(0) } .forEach { holder.registerProblem( it, PestBundle.message("INSPECTION_INVALID_TEST_NAME_CASE"), ChangeTestNameCasingQuickFix() ) } } } ================================================ FILE: src/main/kotlin/com/pestphp/pest/inspections/MissingScreenshotSnapshotInspection.kt ================================================ package com.pestphp.pest.inspections import com.intellij.codeInspection.ProblemsHolder import com.intellij.openapi.vfs.VfsUtil import com.intellij.openapi.vfs.VirtualFile import com.intellij.psi.PsiElementVisitor import com.intellij.psi.util.findParentOfType import com.jetbrains.php.lang.PhpLangUtil import com.jetbrains.php.lang.inspections.PhpInspection import com.jetbrains.php.lang.psi.elements.MethodReference import com.jetbrains.php.lang.psi.elements.impl.FunctionReferenceImpl import com.jetbrains.php.lang.psi.visitors.PhpElementVisitor import com.pestphp.pest.PestBundle import com.pestphp.pest.getPestTestName import com.pestphp.pest.isPestTestFile class MissingScreenshotSnapshotInspection : PhpInspection() { override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor { return object : PhpElementVisitor() { override fun visitPhpMethodReference(reference: MethodReference) { val methodName = reference.name ?: return if (!PhpLangUtil.equalsMethodNames(methodName, "assertScreenshotMatches")) return if (!reference.containingFile.isPestTestFile()) return val pestCall = reference.findParentOfType() ?: return val testName = pestCall.getPestTestName() ?: return if (!snapshotExists(reference, testName)) { val namePsi = reference.nameNode?.psi ?: reference holder.registerProblem( namePsi, PestBundle.message("INSPECTION_MISSING_SCREENSHOT_SNAPSHOT") ) } } } } private fun snapshotExists(context: MethodReference, testName: String): Boolean { val file = context.containingFile.originalFile.virtualFile ?: return false val testsRoot = getTestsRoot(file) ?: return false val relativePath = VfsUtil.getRelativePath(file.parent, testsRoot) ?: return false val snapshotPath = ".pest/snapshots/$relativePath/${file.nameWithoutExtension}" val expectedDir = testsRoot.findFileByRelativePath(snapshotPath) ?: return false val normalizedTestName = testName.replace("_", "__").replace(Regex("[^a-zA-Z0-9→]"), "_") return expectedDir.children?.any { it.name.matches("${normalizedTestName}(__\\d+)?\\.snap".toRegex()) } == true } private fun getTestsRoot(file: VirtualFile): VirtualFile? { var currentDir = file.parent while (currentDir != null && currentDir.name != "tests") { currentDir = currentDir.parent } return currentDir } } ================================================ FILE: src/main/kotlin/com/pestphp/pest/inspections/MultipleExpectChainableInspection.kt ================================================ package com.pestphp.pest.inspections import com.intellij.codeInspection.ProblemsHolder import com.intellij.psi.PsiElementVisitor import com.jetbrains.php.lang.inspections.PhpInspection import com.jetbrains.php.lang.psi.elements.GroupStatement import com.jetbrains.php.lang.psi.elements.MethodReference import com.jetbrains.php.lang.psi.elements.Statement import com.jetbrains.php.lang.psi.visitors.PhpElementVisitor import com.pestphp.pest.PestBundle import com.pestphp.pest.features.customExpectations.isExpectation class MultipleExpectChainableInspection : PhpInspection() { override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor { return object : PhpElementVisitor() { override fun visitPhpGroupStatement(groupStatement: GroupStatement) { var counter = 1 groupStatement.statements .filterIsInstance(Statement::class.java) .groupBy { val methodReference = it.firstPsiChild as? MethodReference if (methodReference?.text?.startsWith("expect") != true || !methodReference.type.isExpectation(holder.project)) { counter++ return@groupBy 0 } counter } .toMutableMap() // Drop index 0, as that is all non expect calls .also { it.remove(0) } // Filter all expect call groups with only one expect call .filterValues { it.size >= 2 } .forEach { declareProblemType(holder, it.value) } } } } @Suppress("SpreadOperator") private fun declareProblemType(holder: ProblemsHolder, statements: List) { statements .forEach { holder.registerProblem( it, PestBundle.message("INSPECTION_MULTIPLE_CHAINABLE_EXPECT_CALLS"), ChangeMultipleExpectCallsToChainableQuickFix() ) } } } ================================================ FILE: src/main/kotlin/com/pestphp/pest/inspections/PestAssertionCanBeSimplifiedInspection.kt ================================================ package com.pestphp.pest.inspections import com.intellij.codeInspection.ProblemsHolder import com.intellij.modcommand.ActionContext import com.intellij.modcommand.ModPsiUpdater import com.intellij.modcommand.PsiUpdateModCommandAction import com.intellij.openapi.project.Project import com.intellij.openapi.util.TextRange import com.intellij.openapi.util.text.StringUtil.capitalize import com.intellij.openapi.util.text.StringUtil.toLowerCase import com.intellij.psi.PsiElement import com.intellij.psi.PsiElementVisitor import com.intellij.psi.SmartPointerManager import com.intellij.refactoring.suggested.endOffset import com.intellij.refactoring.suggested.startOffset import com.jetbrains.php.lang.PhpLangUtil import com.jetbrains.php.lang.inspections.PhpInspection import com.jetbrains.php.lang.inspections.probablyBug.PhpDivisionByZeroInspection import com.jetbrains.php.lang.lexer.PhpTokenTypes import com.jetbrains.php.lang.parser.PhpElementTypes import com.jetbrains.php.lang.psi.PhpPsiElementFactory import com.jetbrains.php.lang.psi.PhpPsiUtil import com.jetbrains.php.lang.psi.elements.FunctionReference import com.jetbrains.php.lang.psi.elements.MethodReference import com.jetbrains.php.lang.psi.elements.PhpTypedElement import com.jetbrains.php.lang.psi.elements.impl.ParameterListImpl import com.jetbrains.php.lang.psi.resolve.types.PhpType import com.jetbrains.php.lang.psi.visitors.PhpElementVisitor import com.pestphp.pest.PestBundle internal class PestAssertionCanBeSimplifiedInspection : PhpInspection() { override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor { return object : PhpElementVisitor() { override fun visitPhpMethodReference(reference: MethodReference) { val methodNamePsi = reference.nameNode?.psi ?: return getMainParameterFromToBe(reference, methodNamePsi)?.let { mainParameter -> registerProblem(methodNamePsi, mainParameter, "toBe${capitalize(toLowerCase(mainParameter.text))}") } getMainParameterFromToHaveCount(reference, methodNamePsi)?.let { mainParameter -> registerProblem(methodNamePsi, mainParameter, "toBeEmpty") } } private fun getMainParameterFromToBe(reference: MethodReference, methodNameIdentifier: PsiElement): PsiElement? { val parameter = reference.parameterList?.getParameter("count", 0) ?: return null if (PhpLangUtil.equalsMethodNames(methodNameIdentifier.text, "toBe") && (PhpLangUtil.isTrue(parameter) || PhpLangUtil.isFalse(parameter) || PhpLangUtil.isNull(parameter))) { return parameter } return null } private fun getMainParameterFromToHaveCount(reference: MethodReference, methodNameIdentifier: PsiElement): PsiElement? { val parameter = reference.parameterList?.getParameter("expected", 0) ?: return null if (PhpLangUtil.equalsMethodNames(methodNameIdentifier.text, "toHaveCount") && PhpPsiUtil.isOfType(parameter, PhpElementTypes.NUMBER) && PhpDivisionByZeroInspection.isZero(parameter)) { val functionCall = reference.classReference as? FunctionReference if (functionCall == null || functionCall.parameters.size != 1) return null val functionName = functionCall.name val functionParameter = functionCall.parameters.first() if (functionName == "expect" && functionParameter is PhpTypedElement && PhpType.isArray(functionParameter.globalType)) { return parameter } } return null } private fun registerProblem(methodNamePsi: PsiElement, parameterToRemove: PsiElement, newMethodName: String) { holder.problem(methodNamePsi, PestBundle.message("INSPECTION_ASSERTION_CAN_BE_SIMPLIFIED", methodNamePsi.text, newMethodName)) .fix(PestSimplifyAssertionQuickFix(newMethodName, methodNamePsi, parameterToRemove)) .register() } } } private class PestSimplifyAssertionQuickFix( private val newMethodName: String, methodNamePsi: PsiElement, parameterToRemove: PsiElement ) : PsiUpdateModCommandAction(methodNamePsi) { private val parameterToRemovePointer = SmartPointerManager.getInstance(parameterToRemove.getProject()) .createSmartPsiElementPointer(parameterToRemove) override fun getFamilyName() = PestBundle.message("QUICK_FIX_SIMPLIFY_ASSERTION") override fun invoke(context: ActionContext, methodNamePsi: PsiElement, updater: ModPsiUpdater) { val parameterToRemove = updater.getWritable(parameterToRemovePointer.element) ?: return val methodReference = methodNamePsi.parent as? MethodReference if (methodReference == null) return val methodEnd = PhpPsiUtil.findNextSiblingOfAnyType(methodNamePsi, PhpTokenTypes.chRPAREN) ?: return (methodReference.parameterList as? ParameterListImpl)?.removeParameter(parameterToRemove) val newMethodCallText = "$newMethodName(${methodReference.parameterList?.text})" val newMethodReference = insertIntoMethodReference(methodReference, TextRange(methodNamePsi.startOffset, methodEnd.endOffset), newMethodCallText, context.project) methodReference.replace(newMethodReference) } private fun insertIntoMethodReference(methodReference: MethodReference, insertionRange: TextRange, insertionText: String, project: Project): MethodReference { val referenceRange = methodReference.textRange val referenceText = methodReference.text val referenceRelativeStart = insertionRange.startOffset - referenceRange.startOffset val referenceRelativeEnd = insertionRange.endOffset - referenceRange.startOffset val newMethodReferenceText = referenceText.substring(0, referenceRelativeStart) + insertionText + referenceText.substring(referenceRelativeEnd) return PhpPsiElementFactory.createMethodReference(project, newMethodReferenceText) } } } ================================================ FILE: src/main/kotlin/com/pestphp/pest/inspections/PestTestFailedLineInspection.kt ================================================ package com.pestphp.pest.inspections import com.intellij.codeInspection.ProblemsHolder import com.intellij.psi.PsiElementVisitor import com.jetbrains.php.lang.inspections.PhpTestFailedLineInspectionBase import com.jetbrains.php.lang.psi.elements.FunctionReference import com.jetbrains.php.lang.psi.visitors.PhpElementVisitor import com.pestphp.pest.isPestTestReference import com.pestphp.pest.runner.PestFailedLineManager class PestTestFailedLineInspection : PhpTestFailedLineInspectionBase() { override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor { return object : PhpElementVisitor() { override fun visitPhpFunctionCall(functionCall: FunctionReference) { if (!functionCall.isPestTestReference()) return val failedLineManager = holder.project.getService(PestFailedLineManager::class.java) process(holder, functionCall, failedLineManager) } } } } ================================================ FILE: src/main/kotlin/com/pestphp/pest/inspections/SuppressExpressionResultUnusedInspection.kt ================================================ package com.pestphp.pest.inspections import com.intellij.codeInspection.InspectionSuppressor import com.intellij.codeInspection.SuppressQuickFix import com.intellij.psi.PsiElement import com.jetbrains.php.lang.inspections.PhpExpressionResultUnusedInspection import com.jetbrains.php.lang.psi.elements.impl.FunctionReferenceImpl import com.pestphp.pest.isPestTestFunction class SuppressExpressionResultUnusedInspection : InspectionSuppressor { companion object { private val SUPPRESSED_PHP_INSPECTIONS = listOf(PhpExpressionResultUnusedInspection().id) } override fun isSuppressedFor(element: PsiElement, toolId: String): Boolean { if (element !is FunctionReferenceImpl) { return false } if (!element.isPestTestFunction()) { return false } return SUPPRESSED_PHP_INSPECTIONS.contains(toolId) } override fun getSuppressActions(element: PsiElement?, toolId: String): Array { return SuppressQuickFix.EMPTY_ARRAY } } ================================================ FILE: src/main/kotlin/com/pestphp/pest/inspections/SuppressUndefinedPropertyInspection.kt ================================================ package com.pestphp.pest.inspections import com.intellij.codeInspection.InspectionSuppressor import com.intellij.codeInspection.SuppressQuickFix import com.intellij.psi.PsiElement import com.jetbrains.php.lang.inspections.PhpDynamicFieldDeclarationInspection import com.jetbrains.php.lang.inspections.PhpUndefinedFieldInspection import com.jetbrains.php.lang.psi.elements.FieldReference import com.pestphp.pest.isAnyPestFunction import com.pestphp.pest.isThisVariableInPest class SuppressUndefinedPropertyInspection : InspectionSuppressor { private val suppressedPhpInspections = listOf(PhpUndefinedFieldInspection().id, PhpDynamicFieldDeclarationInspection().id) override fun isSuppressedFor(element: PsiElement, toolId: String): Boolean { if (!suppressedPhpInspections.contains(toolId)) { return false } val fieldReference = element.parent as? FieldReference ?: return false if (!fieldReference.classReference.isThisVariableInPest { it.isAnyPestFunction() }) { return false } return suppressedPhpInspections.contains(toolId) } override fun getSuppressActions(element: PsiElement?, toolId: String): Array { return SuppressQuickFix.EMPTY_ARRAY } } ================================================ FILE: src/main/kotlin/com/pestphp/pest/notifications/OutdatedNotification.kt ================================================ package com.pestphp.pest.notifications import com.intellij.notification.Notification import com.intellij.notification.NotificationGroupManager import com.intellij.notification.NotificationType import com.intellij.openapi.project.Project import org.jetbrains.annotations.Nls class OutdatedNotification { private val group = NotificationGroupManager.getInstance() .getNotificationGroup("Outdated Pest") fun notify(project: Project?, @Nls content: String): Notification { val notification: Notification = group.createNotification(content, NotificationType.ERROR) notification.notify(project) return notification } } ================================================ FILE: src/main/kotlin/com/pestphp/pest/parser/PestConfigurationFile.kt ================================================ package com.pestphp.pest.parser import com.jetbrains.php.lang.psi.resolve.types.PhpType data class PestConfigurationFile( val baseTestType: PhpType, val pathsClasses: List> ) ================================================ FILE: src/main/kotlin/com/pestphp/pest/parser/PestConfigurationFileParser.kt ================================================ package com.pestphp.pest.parser import com.intellij.openapi.project.Project import com.intellij.openapi.project.guessProjectDir import com.intellij.openapi.util.Key import com.intellij.openapi.vfs.VirtualFile import com.intellij.openapi.vfs.VirtualFileManager import com.intellij.psi.PsiElement import com.intellij.psi.PsiManager import com.intellij.psi.PsiRecursiveElementWalkingVisitor import com.intellij.psi.util.CachedValue import com.intellij.psi.util.CachedValueProvider import com.intellij.psi.util.CachedValuesManager import com.jetbrains.php.lang.psi.PhpFile import com.jetbrains.php.lang.psi.elements.FunctionReference import com.jetbrains.php.lang.psi.elements.MethodReference import com.jetbrains.php.lang.psi.elements.PhpPsiElement import com.jetbrains.php.lang.psi.elements.impl.FunctionReferenceImpl import com.jetbrains.php.lang.psi.elements.impl.PhpFilePathUtils import com.jetbrains.php.lang.psi.resolve.types.PhpType import com.pestphp.pest.CONFIGURATION_FUNCTIONS import com.pestphp.pest.PestSettings import com.pestphp.pest.features.configuration.getConfigurationFunctionCall import com.pestphp.pest.getBaseDir import com.pestphp.pest.getConfigurationPhpType import com.pestphp.pest.getPestConfigurationPhpType import kotlin.io.path.Path class PestConfigurationFileParser(private val settings: PestSettings) { fun parse(project: Project, virtualFile: VirtualFile? = null): PestConfigurationFile { val projectDir = project.guessProjectDir() ?: return defaultConfig // Use the location of the composer.json file or the project dir val baseDir = getBaseDir(project, virtualFile) ?: return defaultConfig val pestFile = VirtualFileManager.getInstance().findFileByUrl(baseDir.url + "/" + settings.pestFilePath) ?: return defaultConfig val psiFile = PsiManager.getInstance(project).findFile(pestFile) as? PhpFile ?: return defaultConfig return CachedValuesManager.getCachedValue(psiFile, cacheKey) { var baseType = PhpType().add("\\PHPUnit\\Framework\\TestCase") val inPaths = mutableListOf>() val relativePath = Path(projectDir.path).relativize(Path(baseDir.path)).toString().run { if (this.isBlank()) this else "$this/" } val testsPath = relativePath + settings.pestFilePath.replaceAfterLast("/", "", "") psiFile.acceptChildren( Visitor { type, inPath, fullPath -> if (fullPath && inPath != null) { inPaths.add(Pair(inPath.replaceBefore(testsPath, ""), type)) } else if (inPath != null) { inPaths.add(Pair(testsPath + inPath, type)) } else { baseType = type } } ) CachedValueProvider.Result.create(PestConfigurationFile(baseType, inPaths), psiFile) } ?: defaultConfig } private class Visitor(private val collect: (PhpType, String?, Boolean) -> Unit) : PsiRecursiveElementWalkingVisitor() { override fun visitElement(element: PsiElement) { if (element is MethodReference) { if (element.name == "in") { visitInReference(element) } else if (getConfigurationFunctionCall(element)?.name in CONFIGURATION_FUNCTIONS) { collect(element.getPestConfigurationPhpType() ?: return, if (getConfigurationFunctionCall(element)?.name == "pest" && element.containingFile.name == CONFIGURATION_FILE_NAME) DEFAULT_DIRECTORY else null, false) } return } else if (element is FunctionReferenceImpl) { if (element.name == "uses") { collect(element.getConfigurationPhpType() ?: return, null, false) } return } super.visitElement(element) } private fun visitInReference(inReference: MethodReference) { var reference = inReference var usesType: PhpType? = null while (true) { val ref = reference.classReference ?: return if (ref is MethodReference) { reference = ref } else if (ref is FunctionReferenceImpl) { if (ref.name in CONFIGURATION_FUNCTIONS) { usesType = (inReference.classReference as? FunctionReference)?.getPestConfigurationPhpType() } break } else { return } } if (usesType == null) return inReference.parameters .map { PhpFilePathUtils.getStaticPath(it as PhpPsiElement?) } .forEach { collect(usesType, it, false) } } } companion object { private val defaultConfig = PestConfigurationFile( PhpType().add("\\PHPUnit\\Framework\\TestCase"), emptyList() ) private val cacheKey = Key>("com.pestphp.pest_configuration") private const val DEFAULT_DIRECTORY = "" private const val CONFIGURATION_FILE_NAME = "Pest.php" } } ================================================ FILE: src/main/kotlin/com/pestphp/pest/runner/LocationInfo.kt ================================================ package com.pestphp.pest.runner import com.intellij.openapi.vfs.VirtualFile class LocationInfo( val file: VirtualFile, val testName: String? ) ================================================ FILE: src/main/kotlin/com/pestphp/pest/runner/PestConsoleProperties.kt ================================================ package com.pestphp.pest.runner import com.intellij.execution.Executor import com.intellij.execution.configurations.RunConfiguration import com.intellij.execution.impl.ConsoleViewImpl import com.intellij.execution.testframework.Printer import com.intellij.execution.testframework.actions.AbstractRerunFailedTestsAction import com.intellij.execution.testframework.sm.runner.SMTRunnerConsoleProperties import com.intellij.execution.testframework.sm.runner.SMTRunnerEventsListener import com.intellij.execution.testframework.sm.runner.SMTestLocator import com.intellij.execution.testframework.sm.runner.SMTestProxy import com.intellij.execution.testframework.sm.runner.ui.TestStackTraceParser import com.intellij.execution.ui.ConsoleView import com.intellij.openapi.project.Project import com.pestphp.pest.PestBundle import com.pestphp.pest.configuration.PestLocationProvider import com.pestphp.pest.configuration.PestRerunFailedTestsAction import com.pestphp.pest.features.parallel.PestParallelSMTEventsAdapter import com.pestphp.pest.features.parallel.PestParallelTestExecutor import com.pestphp.pest.features.parallel.executeInParallel class PestConsoleProperties( config: RunConfiguration, executor: Executor, private val testLocator: PestLocationProvider ) : SMTRunnerConsoleProperties(config, PestBundle.message("FRAMEWORK_NAME"), executor) { init { if (executor is PestParallelTestExecutor || executeInParallel(config)) { config.project.messageBus.connect(this) .subscribe(SMTRunnerEventsListener.TEST_STATUS, PestParallelSMTEventsAdapter()) } } override fun getTestLocator(): SMTestLocator { return testLocator } override fun createRerunFailedTestsAction(consoleView: ConsoleView?): AbstractRerunFailedTestsAction? { return consoleView?.let { PestRerunFailedTestsAction(it, this) } } override fun isPrintTestingStartedTime(): Boolean { return false } override fun printExpectedActualHeader(printer: Printer, expected: String, actual: String) { super.printExpectedActualHeader(printer, expected, actual) } override fun createConsole(): ConsoleView { return super.createConsole() as ConsoleViewImpl } override fun getTestStackTraceParser(url: String, proxy: SMTestProxy, project: Project): TestStackTraceParser { return parse(url, proxy.stacktrace, proxy.errorMessage, testLocator, project) } @Deprecated("Deprecated in Java", ReplaceWith("true")) override fun serviceMessageHasNewLinePrefix(): Boolean { return true } } ================================================ FILE: src/main/kotlin/com/pestphp/pest/runner/PestFailedLineManager.kt ================================================ package com.pestphp.pest.runner import com.intellij.execution.TestStateStorage import com.intellij.openapi.components.Service import com.intellij.openapi.fileEditor.FileEditorManagerListener import com.intellij.openapi.project.Project import com.intellij.psi.PsiElement import com.intellij.psi.PsiFile import com.jetbrains.php.lang.psi.elements.FunctionReference import com.jetbrains.php.lang.psi.elements.MethodReference import com.jetbrains.php.phpunit.PhpUnitTestRunLineMarkerProvider import com.jetbrains.php.phpunit.PhpUnitTestRunLineMarkerProvider.createPathMapper import com.jetbrains.php.testFramework.PhpTestFrameworkFailedLineManager import com.pestphp.pest.configuration.PestLocationProvider import com.pestphp.pest.features.datasets.isDatasetCall import com.pestphp.pest.getPestTestName import com.pestphp.pest.withoutFirstFileSeparator @Service(Service.Level.PROJECT) class PestFailedLineManager( project: Project ) : PhpTestFrameworkFailedLineManager(project), FileEditorManagerListener { override fun getTestLocationUrl(testElement: PsiElement): String? { if (testElement !is FunctionReference) return null return getLocationUrl(testElement.containingFile, testElement) } override fun getRecordsForTest(testElement: PsiElement): List { val testLocationUrl = getTestLocationUrl(testElement) ?: return emptyList() val testStateRecord = TestStateStorage.getInstance(testElement.project).getState(testLocationUrl) ?: return emptyList() val project = testElement.getProject() val records = mutableListOf(testStateRecord) if (testStateRecord.failedLine == -1 && (testElement.parent as? MethodReference)?.isDatasetCall() == true) { val allRecordLocationUrls = TestStateStorage.getInstance(project).keys val dataSetRecords: List = allRecordLocationUrls .asSequence() .filterNotNull() .filter { recordLocationUrl -> isLocationUrlWithNamedDatasetValue(recordLocationUrl, testLocationUrl) } .map { recordLocationUrl -> TestStateStorage.getInstance(project).getState(recordLocationUrl) } .filterNotNull() .filter { record -> record.failedLine != -1 } .toList() records.addAll(dataSetRecords) } return records } private fun isLocationUrlWithNamedDatasetValue(recordLocationUrl: String, testLocationUrl: String): Boolean = recordLocationUrl.startsWith("$testLocationUrl with data set \"dataset") private fun getLocationUrl(containingFile: PsiFile, functionCall: FunctionReference): String = getLocationUrl(containingFile) + "::" + functionCall.getPestTestName() } internal fun getLocationUrl(psiFile: PsiFile): String { return "${PestLocationProvider.PROTOCOL_ID}://${ PhpUnitTestRunLineMarkerProvider.getFilePathDeploymentAware(psiFile) .removePrefix(getProjectPathDeploymentAware(psiFile.project)).withoutFirstFileSeparator }" } private fun getProjectPathDeploymentAware(project: Project): String { val projectPath = project.basePath ?: return "" val remoteMapper = createPathMapper(project) return if (remoteMapper.canProcess(projectPath)) { remoteMapper.process(projectPath) } else { projectPath } } ================================================ FILE: src/main/kotlin/com/pestphp/pest/runner/PestPressToContinueAction.kt ================================================ package com.pestphp.pest.runner import com.intellij.execution.impl.ConsoleViewImpl import com.intellij.execution.testframework.ui.BaseTestsOutputConsoleView import com.intellij.execution.ui.ConsoleViewContentType import com.intellij.execution.ui.RunContentDescriptor import com.intellij.openapi.actionSystem.ActionUpdateThread import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.LangDataKeys import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.project.DumbAwareAction import com.intellij.openapi.util.TextRange import com.pestphp.pest.PestBundle import com.pestphp.pest.configuration.PestRunConfigurationType import java.io.IOException import java.nio.charset.StandardCharsets class PestPressToContinueAction : DumbAwareAction() { override fun getActionUpdateThread(): ActionUpdateThread { return ActionUpdateThread.BGT } override fun update(e: AnActionEvent) { val descriptor = e.getData(LangDataKeys.RUN_CONTENT_DESCRIPTOR) val processHandler = descriptor?.processHandler e.presentation.setText(PestBundle.messagePointer("action.press.to.continue.text")) e.presentation.isVisible = descriptor?.runConfigurationTypeId == PestRunConfigurationType.instance.id e.presentation.isEnabled = processHandler != null && !processHandler.isProcessTerminated && !processHandler.isProcessTerminating && getInnerConsoleViewImpl(descriptor)?.let { shouldEnableAndPrintHint(it) } == true } override fun actionPerformed(e: AnActionEvent) { val descriptor = e.getData(LangDataKeys.RUN_CONTENT_DESCRIPTOR) ?: return val processHandler = descriptor.processHandler ?: return val processInput = processHandler.processInput ?: return processInput.let { stream -> try { stream.write("\n".toByteArray(StandardCharsets.UTF_8)) stream.flush() } catch (io: IOException) { logger.warn("Failed to write to process stdin for Pest Press to continue", io) } } } private fun getInnerConsoleViewImpl(descriptor: RunContentDescriptor): ConsoleViewImpl? { val baseConsole = descriptor.executionConsole as? BaseTestsOutputConsoleView return baseConsole?.console as? ConsoleViewImpl } private fun readLastNonEmptyLineOrEmpty(view: ConsoleViewImpl): String { val editor = view.editor ?: return "" val doc = editor.document var line = doc.lineCount - 1 while (line >= 0) { val start = doc.getLineStartOffset(line) val end = doc.getLineEndOffset(line) val text = doc.getText(TextRange(start, end)) if (text.any { !it.isWhitespace() }) { return text } line-- } return "" } private fun shouldEnableAndPrintHint(view: ConsoleViewImpl): Boolean { val line = readLastNonEmptyLineOrEmpty(view) if (line.contains(PROMPT)) { view.print("\n $HINT\n", ConsoleViewContentType.SYSTEM_OUTPUT) return true } return line.contains(HINT) } internal companion object { private val logger = Logger.getInstance(PestPressToContinueAction::class.java) internal const val PROMPT: String = "Press any key to continue" internal const val HINT: String = "To continue, click \"Continue Test Run\" on the test results' toolbar." } } ================================================ FILE: src/main/kotlin/com/pestphp/pest/runner/PestPromptConsoleFolding.kt ================================================ package com.pestphp.pest.runner import com.intellij.execution.ConsoleFolding import com.intellij.execution.ui.ConsoleView import com.intellij.ide.DataManager import com.intellij.openapi.actionSystem.LangDataKeys import com.intellij.openapi.project.Project import com.pestphp.pest.configuration.PestRunConfigurationType class PestPromptConsoleFolding : ConsoleFolding() { override fun isEnabledForConsole(consoleView: ConsoleView): Boolean { val context = DataManager.getInstance().getDataContext(consoleView.component) val descriptor = context.getData(LangDataKeys.RUN_CONTENT_DESCRIPTOR) ?: return false return descriptor.runConfigurationTypeId == PestRunConfigurationType.instance.id } override fun shouldFoldLine(project: Project, line: String): Boolean { return line.contains(PestPressToContinueAction.PROMPT) } override fun getPlaceholderText(project: Project, lines: List): String { return "" } override fun shouldBeAttachedToThePreviousLine(): Boolean { return false } } ================================================ FILE: src/main/kotlin/com/pestphp/pest/runner/PestTestStackTraceParser.kt ================================================ package com.pestphp.pest.runner import com.intellij.execution.testframework.sm.runner.ui.TestStackTraceParser import com.intellij.openapi.project.Project import com.intellij.openapi.util.text.StringUtil import com.intellij.psi.PsiDocumentManager import com.intellij.psi.PsiManager import com.intellij.util.DocumentUtil import com.jetbrains.php.util.pathmapper.PhpLocalPathMapper import com.pestphp.pest.configuration.PestLocationProvider fun parse(url: String, stacktrace: String?, errorMessage: String?, locator: PestLocationProvider, project: Project): PestTestStackTraceParser { if (stacktrace == null) return PestTestStackTraceParser(errorMessage) val lines = stacktrace.split("\n") if (lines.isEmpty()) return PestTestStackTraceParser(errorMessage) val realErrorMessage = if (errorMessage.isNullOrEmpty()) lines[0] else errorMessage val path = url.removePrefix("${PestLocationProvider.PROTOCOL_ID}://").substringBefore( "::") val lastLine = lines.last().trim { it <= ' ' }.substringAfter("at ") if (path == url || !lastLine.startsWith(path)) return PestTestStackTraceParser(realErrorMessage) val failedLine = StringUtil.parseInt(lastLine.substring(path.length + 1), -1) val failedLineText = if (failedLine > 0) getLineText(path, failedLine, project, locator) else null return PestTestStackTraceParser(failedLine, failedLineText, realErrorMessage, null) } private fun getLineText( path: String, line: Int, project: Project, locator: PestLocationProvider ): String? { val fileUrl = locator.calculateFileUrl(path) val vFile = locator.pathMapper.getLocalFile(fileUrl) ?: PhpLocalPathMapper(project).getLocalFile(fileUrl) ?: return null val psiFile = PsiManager.getInstance(project).findFile(vFile) ?: return null val document = PsiDocumentManager.getInstance(project).getDocument(psiFile) ?: return null if (line > document.lineCount) return null val range = DocumentUtil.getLineTextRange(document, line - 1) return document.getText(range) } class PestTestStackTraceParser( failedLine: Int, failedMethodName: String?, errorMessageName: String?, topLocationLine: String?, ) : TestStackTraceParser(failedLine, failedMethodName, errorMessageName, topLocationLine) { constructor(errorMessage: String?) : this(-1, null, errorMessage, null) } ================================================ FILE: src/main/kotlin/com/pestphp/pest/statistics/PestUsagesCollector.kt ================================================ package com.pestphp.pest.statistics import com.intellij.internal.statistic.eventLog.EventLogGroup import com.intellij.internal.statistic.service.fus.collectors.CounterUsagesCollector import com.intellij.openapi.project.Project object PestUsagesCollector : CounterUsagesCollector() { private val GROUP = EventLogGroup("pest", 2) private val PEST_MUTATION_TEST_EXECUTED = GROUP.registerEvent("pest.mutation.test.executed") private val PEST_PARALLEL_TEST_EXECUTED = GROUP.registerEvent("pest.parallel.test.executed") override fun getGroup(): EventLogGroup = GROUP fun logMutationTestExecution(project: Project) { PEST_MUTATION_TEST_EXECUTED.log(project) } fun logParallelTestExecution(project: Project) { PEST_PARALLEL_TEST_EXECUTED.log(project) } } ================================================ FILE: src/main/kotlin/com/pestphp/pest/structureView/PestStructureViewElement.kt ================================================ package com.pestphp.pest.structureView import com.intellij.ide.projectView.PresentationData import com.intellij.ide.structureView.StructureViewTreeElement import com.intellij.ide.util.treeView.smartTree.TreeElement import com.intellij.navigation.ItemPresentation import com.intellij.psi.NavigatablePsiElement import com.pestphp.pest.PestIcons import com.pestphp.pest.getPestTestName import com.pestphp.pest.isPestTestReference /** * Defines how the elements in the structure view * should be rendered. */ class PestStructureViewElement(val element: NavigatablePsiElement) : StructureViewTreeElement { override fun getPresentation(): ItemPresentation { if (!element.isPestTestReference()) { return element.presentation ?: PresentationData() } return PresentationData( element.getPestTestName(), null, PestIcons.Logo, null, ) } override fun getChildren(): Array { return arrayOf() } override fun navigate(requestFocus: Boolean) { return element.navigate(requestFocus) } override fun canNavigate(): Boolean { return element.canNavigate() } override fun canNavigateToSource(): Boolean { return element.canNavigateToSource() } override fun getValue(): Any { return element } } ================================================ FILE: src/main/kotlin/com/pestphp/pest/structureView/PestStructureViewExtension.kt ================================================ package com.pestphp.pest.structureView import com.intellij.ide.structureView.StructureViewExtension import com.intellij.ide.structureView.StructureViewTreeElement import com.intellij.openapi.editor.Editor import com.intellij.psi.PsiElement import com.jetbrains.php.lang.psi.PhpFile import com.pestphp.pest.getPestTests /** * Extends the structure view, so we can include all * the pest tests in it. */ class PestStructureViewExtension : StructureViewExtension { override fun getType(): Class { return PhpFile::class.java } override fun getChildren(parent: PsiElement?): Array { if (parent !is PhpFile) { return arrayOf() } return parent.getPestTests() .map { PestStructureViewElement(it) } .toTypedArray() } override fun getCurrentEditorElement(editor: Editor?, parent: PsiElement?): Any? { return null } } ================================================ FILE: src/main/kotlin/com/pestphp/pest/surrounders/ExpectStatementSurrounder.kt ================================================ package com.pestphp.pest.surrounders import com.intellij.lang.surroundWith.Surrounder import com.intellij.openapi.editor.Editor import com.intellij.openapi.project.Project import com.intellij.openapi.util.TextRange import com.intellij.psi.PsiElement import com.jetbrains.php.lang.psi.PhpPsiElementFactory import com.pestphp.pest.PestBundle class ExpectStatementSurrounder : Surrounder { override fun getTemplateDescription(): String { return PestBundle.message("EXPECT_STATEMENT") } override fun isApplicable(elements: Array): Boolean { return true } override fun surroundElements(project: Project, editor: Editor, elements: Array): TextRange? { val template = PhpPsiElementFactory.createStatement( project, "expect(${elements.joinToString("") { it.text }})" ) val lastElement = elements.last() val replaced = lastElement.replace(template) elements.forEach { it.delete() } return replaced.textRange } } ================================================ FILE: src/main/kotlin/com/pestphp/pest/surrounders/StatementSurroundDescriptor.kt ================================================ package com.pestphp.pest.surrounders import com.intellij.lang.surroundWith.SurroundDescriptor import com.intellij.lang.surroundWith.Surrounder import com.intellij.openapi.util.TextRange import com.intellij.psi.PsiElement import com.intellij.psi.PsiFile import com.jetbrains.php.surroundWith.PhpStatementSurroundDescriptor import com.pestphp.pest.getPestTests class StatementSurroundDescriptor : SurroundDescriptor { override fun getElementsToSurround(file: PsiFile, startOffset: Int, endOffset: Int): Array { val range = TextRange(startOffset, endOffset) val insideTest = file.getPestTests() .any { it.textRange.contains(range) } if (!insideTest) { return arrayOf() } return PhpStatementSurroundDescriptor().getElementsToSurround( file, startOffset, endOffset ) } override fun getSurrounders(): Array { return arrayOf(ExpectStatementSurrounder()) } override fun isExclusive(): Boolean { return false } } ================================================ FILE: src/main/kotlin/com/pestphp/pest/templates/PestConfigNewDatasetFileAction.kt ================================================ package com.pestphp.pest.templates import com.intellij.ide.actions.CreateFileFromTemplateDialog import com.intellij.ide.fileTemplates.FileTemplate import com.intellij.openapi.project.Project import com.intellij.psi.PsiDirectory import com.intellij.psi.PsiFile import com.pestphp.pest.PestBundle import com.pestphp.pest.PestIcons /** * Shows the "Create Pest Dataset File" action in the context menu when creating a new file. * * This will only show if the file is being created in a directory named "tests". */ private class PestConfigNewDatasetFileAction : PestConfigNewFileAction() { companion object { const val PEST_SHARED_DATASET_TEMPLATE = "Pest Shared Dataset" const val PEST_SCOPED_DATASET_TEMPLATE = "Pest Scoped Dataset" } override fun buildDialog(project: Project, directory: PsiDirectory, builder: CreateFileFromTemplateDialog.Builder) { builder .setTitle(PestBundle.message("CREATE_NEW_PEST_DATASET_DIALOG_TITLE")) .addKind(PestBundle.message("CREATE_NEW_PEST_SHARED_DATASET"), PestIcons.Dataset, PEST_SHARED_DATASET_TEMPLATE) .addKind(PestBundle.message("CREATE_NEW_PEST_SCOPED_DATASET"), PestIcons.Dataset, PEST_SCOPED_DATASET_TEMPLATE) } override fun getActionName(directory: PsiDirectory?, newName: String, templateName: String?): String { return PestBundle.message("action.Pest.New.Dataset.text") } override fun createFileFromTemplate(name: String?, template: FileTemplate, dir: PsiDirectory): PsiFile { if (template.name == PEST_SHARED_DATASET_TEMPLATE) { // find parent directory named "tests" var parentDir = dir while (parentDir.name != "tests") { parentDir = parentDir.parentDirectory ?: break } val datasetDir = parentDir.findSubdirectory("Datasets") ?: parentDir.createSubdirectory("Datasets") // Check if first character is lowercase in name var newName = name if (name!![0].isLowerCase()) { newName = name.replaceFirstChar { it.uppercase() } } return createFileFromTemplate( newName, template, datasetDir, defaultTemplateProperty, true, mapOf("DATASET_NAME" to name.replaceFirstChar { it.lowercase() }) )!! } return createFileFromTemplate( "Datasets", template, dir, defaultTemplateProperty, true, mapOf("DATASET_NAME" to name!!.replaceFirstChar { it.lowercase() }) )!! } } ================================================ FILE: src/main/kotlin/com/pestphp/pest/templates/PestConfigNewFileAction.kt ================================================ package com.pestphp.pest.templates import com.intellij.ide.actions.CreateFileFromTemplateAction import com.intellij.ide.actions.CreateFileFromTemplateDialog import com.intellij.ide.fileTemplates.FileTemplate import com.intellij.openapi.actionSystem.DataContext import com.intellij.openapi.actionSystem.LangDataKeys import com.intellij.openapi.project.Project import com.intellij.psi.PsiDirectory import com.intellij.psi.PsiFile import com.pestphp.pest.PestBundle import com.pestphp.pest.PestIcons import com.pestphp.pest.PestSettings import com.pestphp.pest.PestSettings.TestFlavor /** * Shows the "Create Pest Test File" action in the context menu when creating a new file. * * This will only show if the file is being created in a directory named "tests". */ open class PestConfigNewFileAction : CreateFileFromTemplateAction() { companion object { const val PEST_IT_TEMPLATE = "Pest It" const val PEST_TEST_TEMPLATE = "Pest Test" } override fun isAvailable(dataContext: DataContext): Boolean { val view = LangDataKeys.IDE_VIEW.getData(dataContext) var psiDir: PsiDirectory? = null if (view != null) { val directories = view.directories if (directories.size == 1) { psiDir = directories[0] } } if (psiDir == null || !psiDir.isValid) { return false } val virtualDir = psiDir.virtualFile if (!virtualDir.isValid || !virtualDir.isDirectory) { return false } return virtualDir.path.contains("tests") } override fun buildDialog(project: Project, directory: PsiDirectory, builder: CreateFileFromTemplateDialog.Builder) { builder .setTitle(PestBundle.message("CREATE_NEW_PEST_TEST_DIALOG_TITLE")) .addKind(PestBundle.message("CREATE_NEW_PEST_IT_FLAVOR"), PestIcons.File, PEST_IT_TEMPLATE) .addKind(PestBundle.message("CREATE_NEW_PEST_TEST_FLAVOR"), PestIcons.File, PEST_TEST_TEMPLATE) } override fun getActionName(directory: PsiDirectory?, newName: String, templateName: String?): String { return PestBundle.message("action.Pest.New.File.text") } override fun createFileFromTemplate(name: String?, template: FileTemplate, dir: PsiDirectory): PsiFile { PestSettings.getInstance(dir.project).preferredTestFlavor = if (template.name == PEST_IT_TEMPLATE) TestFlavor.IT else TestFlavor.TEST var testName = name if (!name!!.endsWith("test", true)) { testName = "${name}Test" } return super.createFileFromTemplate(testName, template, dir) } override fun getDefaultTemplateName(dir: PsiDirectory): String { return if (PestSettings.getInstance(dir.project).preferredTestFlavor == TestFlavor.IT) { PEST_IT_TEMPLATE } else { PEST_TEST_TEMPLATE } } } ================================================ FILE: src/main/kotlin/com/pestphp/pest/templates/PestDescribePostfixTemplate.kt ================================================ package com.pestphp.pest.templates import com.intellij.openapi.editor.Document import com.intellij.psi.PsiElement import com.jetbrains.php.lang.psi.elements.StringLiteralExpression import com.jetbrains.php.postfixCompletion.PhpPostfixUtils import com.jetbrains.php.postfixCompletion.PhpStringBasedPostfixTemplate import com.pestphp.pest.isPestTestFile class PestDescribePostfixTemplate : PhpStringBasedPostfixTemplate( "describe", "describe($EXPR, function...)", PhpPostfixUtils.selectorTopmost() ) { override fun isApplicable(context: PsiElement, copyDocument: Document, newOffset: Int): Boolean { return context.parent is StringLiteralExpression && context.containingFile.isPestTestFile() } override fun getTemplateString(element: PsiElement): String { val dollar = "$" return """ describe(${dollar}$EXPR${dollar}, function() { ${dollar}END${dollar} }); """.trimIndent() } } ================================================ FILE: src/main/kotlin/com/pestphp/pest/templates/PestItPostfixTemplate.kt ================================================ package com.pestphp.pest.templates import com.intellij.openapi.editor.Document import com.intellij.psi.PsiElement import com.jetbrains.php.lang.psi.elements.StringLiteralExpression import com.jetbrains.php.postfixCompletion.PhpPostfixUtils import com.jetbrains.php.postfixCompletion.PhpStringBasedPostfixTemplate import com.pestphp.pest.isPestTestFile /** * Adds a postfix template for `it` tests. */ class PestItPostfixTemplate : PhpStringBasedPostfixTemplate( "it", "it(${EXPR}, function...)", PhpPostfixUtils.selectorTopmost() ) { override fun isApplicable(context: PsiElement, copyDocument: Document, newOffset: Int): Boolean { return context.parent is StringLiteralExpression && context.containingFile.isPestTestFile() } override fun getTemplateString(element: PsiElement): String { val dollar = "$"; return """ it(${dollar}${EXPR}${dollar}, function() { ${dollar}END${dollar} }); """.trimIndent() } } ================================================ FILE: src/main/kotlin/com/pestphp/pest/templates/PestPostfixTemplateProvider.kt ================================================ package com.pestphp.pest.templates import com.intellij.codeInsight.template.postfix.templates.PostfixTemplate import com.intellij.codeInsight.template.postfix.templates.PostfixTemplateProvider import com.intellij.openapi.editor.Editor import com.intellij.psi.PsiFile /** * Register postfix templates */ class PestPostfixTemplateProvider : PostfixTemplateProvider { override fun getTemplates(): MutableSet { return mutableSetOf(PestItPostfixTemplate(), PestDescribePostfixTemplate()) } override fun isTerminalSymbol(currentChar: Char): Boolean { return currentChar == '.' } override fun preExpand(file: PsiFile, editor: Editor) = Unit override fun afterExpand(file: PsiFile, editor: Editor) = Unit override fun preCheck(copyFile: PsiFile, realEditor: Editor, currentOffset: Int): PsiFile { return copyFile } } ================================================ FILE: src/main/kotlin/com/pestphp/pest/templates/PestRootTemplateContextType.kt ================================================ package com.pestphp.pest.templates import com.intellij.codeInsight.template.TemplateActionContext import com.intellij.codeInsight.template.TemplateContextType import com.jetbrains.php.lang.psi.PhpFile import com.jetbrains.php.lang.psi.elements.PhpNamespace import com.jetbrains.php.lang.psi.elements.StringLiteralExpression import com.pestphp.pest.PestBundle import com.pestphp.pest.isPestTestFile /** * Adds a template context to be used in live templates * * This Pest root template checks if the context is the root of a * pest test file. */ class PestRootTemplateContextType : TemplateContextType(PestBundle.message("LIVE_TEMPLATE_PEST_ROOT")) { override fun isInContext(templateActionContext: TemplateActionContext): Boolean { if (!templateActionContext.file.isPestTestFile()) { return false } val element = templateActionContext.file.findElementAt(templateActionContext.startOffset) if (element?.parent is StringLiteralExpression) { return false } // Get root element val root = element?.parent?.parent?.parent?.parent // Check if in root is namespace or file return root is PhpFile || root is PhpNamespace } } ================================================ FILE: src/main/kotlin/com/pestphp/pest/types/HigherOrderExtendTypeProvider.kt ================================================ package com.pestphp.pest.types import com.intellij.openapi.project.DumbService import com.intellij.openapi.project.Project import com.intellij.psi.PsiElement import com.jetbrains.php.lang.psi.elements.FieldReference import com.jetbrains.php.lang.psi.elements.MemberReference import com.jetbrains.php.lang.psi.elements.MethodReference import com.jetbrains.php.lang.psi.elements.PhpNamedElement import com.jetbrains.php.lang.psi.elements.PhpTypedElement import com.jetbrains.php.lang.psi.elements.impl.FunctionReferenceImpl import com.jetbrains.php.lang.psi.resolve.types.PhpType import com.jetbrains.php.lang.psi.resolve.types.PhpTypeProvider4 import com.pestphp.pest.features.customExpectations.expectationType class HigherOrderExtendTypeProvider : PhpTypeProvider4 { override fun getKey(): Char { return '\u0224' // Ȥ } override fun getType(psiElement: PsiElement): PhpType? { if (DumbService.isDumb(psiElement.project)) return null val reference = psiElement as? MemberReference ?: return null if (reference !is FieldReference && reference !is MethodReference) return null val expectCall = getExpectCall(reference) ?: return null if (expectCall.parameters.isEmpty()) return null val firstParameterType = (expectCall.parameters[0] as? PhpTypedElement)?.type ?: return null return PhpType().add(firstParameterType).add(expectationType) } private fun getExpectCall(reference: MemberReference, depth: Int = 50): FunctionReferenceImpl? { if (depth <= 0) return null return when (val classReference = reference.classReference) { is FunctionReferenceImpl -> if (classReference.name == "expect") classReference else null is MemberReference -> getExpectCall(classReference, depth - 1) else -> null } } override fun complete(s: String, project: Project): PhpType? { return null } override fun getBySignature(s: String, set: Set, i: Int, project: Project): Collection { return emptyList() } } ================================================ FILE: src/main/kotlin/com/pestphp/pest/types/InnerTestTypeProvider.kt ================================================ package com.pestphp.pest.types import com.intellij.openapi.project.DumbService import com.intellij.psi.PsiElement import com.jetbrains.php.lang.psi.resolve.types.PhpType import com.pestphp.pest.isAnyPestFunction import com.pestphp.pest.isTestAsThisVariableInPest /** * Extend `test()` type inside a test closure with types from `uses`. * Both `uses` from the same file, the pest config file * and `uses` with paths from pest config file. */ class InnerTestTypeProvider: ThisTypeProvider() { override fun getKey(): Char { return '\u0226' // Ȧ } override fun getType(psiElement: PsiElement): PhpType? { if (DumbService.isDumb(psiElement.project)) return null if (!psiElement.isTestAsThisVariableInPest { it.isAnyPestFunction() }) return null return getPestType(psiElement) } } ================================================ FILE: src/main/kotlin/com/pestphp/pest/types/ThisExtendTypeProvider.kt ================================================ package com.pestphp.pest.types import com.intellij.openapi.project.DumbService import com.intellij.openapi.project.Project import com.intellij.psi.PsiElement import com.jetbrains.php.lang.psi.elements.PhpNamedElement import com.jetbrains.php.lang.psi.resolve.types.PhpType import com.jetbrains.php.lang.psi.resolve.types.PhpTypeProvider4 import com.pestphp.pest.features.customExpectations.expectationType import com.pestphp.pest.features.customExpectations.isThisVariableInExtend /** * Adds autocompletion for `$this` variable in `expect->extend`. */ class ThisExtendTypeProvider : PhpTypeProvider4 { override fun getKey(): Char { return '\u0223' // ȣ } override fun getType(psiElement: PsiElement): PhpType? { if (DumbService.isDumb(psiElement.project)) return null if (!psiElement.isThisVariableInExtend()) return null return expectationType } override fun complete(s: String, project: Project): PhpType? { return null } override fun getBySignature(s: String, set: Set, i: Int, project: Project): Collection { return emptyList() } } ================================================ FILE: src/main/kotlin/com/pestphp/pest/types/ThisFieldTypeProvider.kt ================================================ package com.pestphp.pest.types import com.intellij.openapi.project.DumbService import com.intellij.openapi.project.Project import com.intellij.psi.PsiElement import com.jetbrains.php.lang.psi.elements.FieldReference import com.jetbrains.php.lang.psi.elements.PhpNamedElement import com.jetbrains.php.lang.psi.elements.PhpTypedElement import com.jetbrains.php.lang.psi.elements.impl.FunctionReferenceImpl import com.jetbrains.php.lang.psi.resolve.types.PhpType import com.jetbrains.php.lang.psi.resolve.types.PhpTypeProvider4 import com.pestphp.pest.getAllBeforeThisAssignments import com.pestphp.pest.isPestAfterFunction import com.pestphp.pest.isPestTestFunction import com.pestphp.pest.isThisVariableInPest /** * Adds type for fields registered in `beforeEach`. */ class ThisFieldTypeProvider : PhpTypeProvider4 { override fun getKey(): Char { return '\u0225' // ȥ } override fun getType(psiElement: PsiElement): PhpType? { if (DumbService.isDumb(psiElement.project)) return null val fieldReference = psiElement as? FieldReference ?: return null if (!fieldReference.classReference.isThisVariableInPest { check(it) }) return null val fieldName = fieldReference.name ?: return null return (psiElement.containingFile ?: return null).getAllBeforeThisAssignments() .filter { (it.variable as? FieldReference)?.name == fieldName } .mapNotNull { it.value } .filterIsInstance() .firstOrNull()?.type } private fun check(it: FunctionReferenceImpl) = it.isPestTestFunction() || it.isPestAfterFunction() override fun complete(s: String, project: Project): PhpType? { return null } override fun getBySignature(s: String, set: Set, i: Int, project: Project): Collection { return emptyList() } } ================================================ FILE: src/main/kotlin/com/pestphp/pest/types/ThisTypeProvider.kt ================================================ package com.pestphp.pest.types import com.intellij.openapi.project.DumbService import com.intellij.openapi.project.Project import com.intellij.openapi.project.guessProjectDir import com.intellij.openapi.util.io.FileUtil import com.intellij.openapi.vfs.VfsUtil import com.intellij.psi.PsiElement import com.jetbrains.php.lang.psi.elements.FunctionReference import com.jetbrains.php.lang.psi.elements.PhpNamedElement import com.jetbrains.php.lang.psi.elements.impl.FunctionReferenceImpl import com.jetbrains.php.lang.psi.resolve.types.PhpType import com.jetbrains.php.lang.psi.resolve.types.PhpTypeProvider4 import com.pestphp.pest.PestSettings import com.pestphp.pest.getPestConfigurationPhpType import com.pestphp.pest.getRoot import com.pestphp.pest.isAnyPestFunction import com.pestphp.pest.isThisVariableInPest import java.nio.file.FileSystems import kotlin.io.path.Path /** * Extend `$this` type with types from `uses`. * Both `uses` from the same file, the pest config file * and `uses` with paths from pest config file. */ open class ThisTypeProvider : PhpTypeProvider4 { override fun getKey(): Char { return '\u0221' // ȡ } override fun getType(psiElement: PsiElement): PhpType? { if (DumbService.isDumb(psiElement.project)) return null if ( ((psiElement as? FunctionReferenceImpl)?.isAnyPestFunction() != true) && !psiElement.isThisVariableInPest { it.isAnyPestFunction() } ) return null return getPestType(psiElement) } protected fun getPestType(psiElement: PsiElement): PhpType? { val virtualFile = psiElement.containingFile?.originalFile?.virtualFile ?: return null val config = PestSettings.getInstance(psiElement.project).getPestConfiguration(psiElement.project, virtualFile) val baseDir = (psiElement.project.guessProjectDir() ?: return config.baseTestType) val relativePath = VfsUtil.getRelativePath(virtualFile, baseDir) ?: return config.baseTestType val result = PhpType().add(config.baseTestType) val defaultFileSystem = FileSystems.getDefault() config.pathsClasses.forEach { (path, type) -> FileUtil.toCanonicalPath(path)?.let { normalizedPathForMatching -> if (defaultFileSystem.getPathMatcher("glob:$normalizedPathForMatching**").matches(Path(relativePath))) { result.add(type) } } } psiElement.containingFile.getRoot() .filterIsInstance() .mapNotNull { it.getPestConfigurationPhpType() } .forEach { result.add(it) } return result } override fun complete(s: String, project: Project): PhpType? { return null } override fun getBySignature(s: String, set: Set, i: Int, project: Project): Collection { return emptyList() } } ================================================ FILE: src/main/resources/META-INF/plugin.xml ================================================ com.pestphp.pest-intellij Pest JetBrains Plugin provides Pest PHP test framework support messages.pestBundle Test Tools ================================================ FILE: src/main/resources/fileTemplates/internal/Pest It.php.ft ================================================ Reports invalid names for pest dataset cases. ================================================ FILE: src/main/resources/inspectionDescriptions/InvalidDatasetReferenceInspection.html ================================================ Reports a non-existent Pest dataset. ================================================ FILE: src/main/resources/inspectionDescriptions/InvalidTestNameCaseInspection.html ================================================ Reports invalid names for pest test cases. ================================================ FILE: src/main/resources/inspectionDescriptions/MissingScreenshotSnapshotInspection.html ================================================ Reports missing visual regression snapshot files for $var->assertScreenshotMatches() calls in Pest tests. ================================================ FILE: src/main/resources/inspectionDescriptions/MultipleExpectChainableInspection.html ================================================ Reports multiple expectations that can be converted to chainable calls. ================================================ FILE: src/main/resources/inspectionDescriptions/PestAssertionCanBeSimplifiedInspection.html ================================================ Reports assertion call to be replaced with more specific analogue ================================================ FILE: src/main/resources/inspectionDescriptions/PestTestFailedLineInspection.html ================================================

Reports failed function calls or assertions in Pest tests. It helps detect the failed line in code faster and start debugging it immediately.

Example:


  test("1 is 2", function() {
      expect(1)->toBe(2); //highlighted
  }
================================================ FILE: src/main/resources/liveTemplates/PestPHP.xml ================================================ ================================================ FILE: src/main/resources/log4j.properties ================================================ log4j.rootLogger=INFO,stdout log4j.appender.stdout=org.apache.log4j.ConsoleAppender log4j.appender.stdout.layout=org.apache.log4j.PatternLayout ================================================ FILE: src/main/resources/messages/pestBundle.properties ================================================ CREATE_NEW_PEST_TEST=Create New Pest Test\u2026 CREATE_NEW_PEST_TEST_DESCRIPTION=Creates a new Pest test based on a file FRAMEWORK_NAME=Pest NO_FAILED_TESTS_FOUND=Failed finding any tests to rerun
Rerunning support was first added in Pest v0.3.5 PEST_CONFIGURATION_UI_CAN_NOT_PARSE_VERSION=Cannot parse version command output.\n{0} GETTING_PEST_VERSION=Getting Pest version\u2026 CREATE_NEW_PEST_IT_FLAVOR=Test file - it(...) CREATE_NEW_PEST_TEST_FLAVOR=Test file - test(...) CREATE_NEW_PEST_TEST_DIALOG_TITLE=Create Pest Test File CREATE_NEW_PEST_DATASET_DIALOG_TITLE=Create Pest Dataset File CREATE_NEW_PEST_SHARED_DATASET=Shared dataset CREATE_NEW_PEST_SCOPED_DATASET=Scoped dataset LIVE_TEMPLATE_PEST_ROOT=Pest root EXPECT_STATEMENT=Expect RUNTIME_CONFIGURATION_EXCEPTION_MESSAGE=''{0}'' for ''{1}'' run configuration CANNOT_RUN_PEST_WITH_TYPE_MESSAGE=Cannot run Pest with type COVERAGE_ENGINE_LABEL_TEXT=Preferred Coverage engine: ENABLE_PARALLEL_TESTING_LABEL_TEXT=Enable parallel testing: NOTIFICATION_GROUP_OUTDATED_PEST=Outdated Pest DIALOG_MESSAGE_COULD_NOT_FIND_PHP_INTERPRETER=Could not find PHP interpreter TOOLTIP_NAVIGATE_TO_SNAPSHOT_FILES=Navigate to snapshot files action.Pest.New.File.text=Pest Test action.Pest.New.Dataset.text=Pest Dataset ACTIONS_NEW_PEST_TEST_ACTION_DESCRIPTION=Creates new Pest test INSPECTION_PHP_GROUP_PEST=Pest INSPECTION_MULTIPLE_CHAINABLE_EXPECT_CALLS=Multiple expectations can be chained together INSPECTION_INVALID_TEST_NAME_CASE=Words in Pest test names should be separated by spaces INSPECTION_INVALID_DATASET_NAME_CASE=Words in Pest dataset names should be separated by spaces INSPECTION_DUPLICATE_TEST_NAME=Pest test names must be unique within a file INSPECTION_DUPLICATE_CUSTOM_EXPECTATION=Pest custom expectation names must be unique INSPECTION_INVALID_DATASET_REFERENCE=The dataset does not exist INSPECTION_ASSERTION_CAN_BE_SIMPLIFIED_NAME=Assertion can be simplified INSPECTION_ASSERTION_CAN_BE_SIMPLIFIED=Assertion ''{0}'' can be simplified to ''{1}'' INSPECTION_PEST_FAILED_LINE=Failed line in test INSPECTION_MISSING_SCREENSHOT_SNAPSHOT_NAME=Missing screenshot snapshot INSPECTION_MISSING_SCREENSHOT_SNAPSHOT=Missing screenshot snapshot, run the test to create it QUICK_FIX_CHANGE_TEST_NAME_CASING=Convert test name to sentence case QUICK_FIX_CHANGE_DATASET_NAME_CASING=Convert dataset name to sentence case QUICK_FIX_CHANGE_MULTIPLE_EXPECT_INTO_CHAIN=Convert multiple expectations into chain QUICK_FIX_SIMPLIFY_ASSERTION=Simplify assertion QUICK_FIX_DELETE_TEST=Delete test ''{0}'' QUICK_FIX_DELETE_CUSTOM_EXPECTATION=Delete expectation ''{0}'' INTENTION_NAVIGATE_TO_DUPLICATE_TEST_NAME=Navigate to duplicate test name INTENTION_NAVIGATE_TO_DUPLICATE_CUSTOM_EXPECTATION=Navigate to duplicate custom expectation ACTION_RUN_SELECTED_CONFIGURATION_WITH_MUTATE_DESCRIPTION=Run selected configuration with --mutate ACTION_PEST_MUTATE_TEXT=Pest Mutation RUN_PEST_WITH_MUTATE=Run Pest with Mutation RUN_S_WITH_MUTATE=Run%s with Mutation ACTION_RUN_SELECTED_CONFIGURATION_WITH_PARALLEL_DESCRIPTION=Run selected configuration with --parallel ACTION_PEST_PARALLEL_TEXT=Pest Parallel RUN_PEST_WITH_PARALLEL=Run Pest in Parallel RUN_S_WITH_PARALLEL=Run%s in Parallel PEST_PARALLEL_IS_NOT_SUPPORTED_FOR_SELECTED_RUN_PROFILE=Pest --parallel is not compatible with selected run profile PEST_VERSION_IS_NOT_SUPPORTED_FOR_REMOTE_INTERPRETER=Pest version is not supported for remote interpreter MUTATION_TESTING_IS_AVAILABLE_FROM_VERSION_3=Mutation testing is available in Pest starting from version 3.0 PARALLEL_TESTING_IS_SUPPORTED_FROM_VERSION_2=Parallel testing is supported in PhpStorm starting from Pest version 2.0 CURRENT_PEST_VERSION_IS=Current Pest version is {0} action.press.to.continue.text=Continue Test Run ================================================ FILE: src/main/resources/postfixTemplates/PestDescribePostfixTemplate/after.php.template ================================================ "can describe this"$key ================================================ FILE: src/main/resources/postfixTemplates/PestDescribePostfixTemplate/description.html ================================================

Wraps a string into a pest describe block.

================================================ FILE: src/main/resources/postfixTemplates/PestItPostfixTemplate/after.php.template ================================================ "can test this"$key ================================================ FILE: src/main/resources/postfixTemplates/PestItPostfixTemplate/description.html ================================================

Wraps a string into a pest it test.

================================================ FILE: src/test/kotlin/com/pestphp/pest/PestIconProviderTest.kt ================================================ package com.pestphp.pest import com.intellij.openapi.util.Iconable.ICON_FLAG_VISIBILITY import com.intellij.psi.PsiManager class PestIconProviderTest : PestLightCodeFixture() { override fun setUp() { super.setUp() myFixture.copyFileToProject("utilTests/SimpleTest.php", "tests/SimpleTest.php") } fun testCanGetPestIconForPestFile() { val file = myFixture.configureByFile("tests/SimpleTest.php") assertEquals( PestIcons.File, PestIconProvider().getIcon(file.virtualFile, ICON_FLAG_VISIBILITY, project), ) } fun testCanGetOtherIconForNonPestFile() { val file = myFixture.configureByFile("SimpleScript.php") assertNull( PestIconProvider().getIcon(file.virtualFile, ICON_FLAG_VISIBILITY, project) ) } fun testCanGetPestIconForDatasetFile() { myFixture.copyFileToProject("Dataset.php", "/tests/Datasets/Dataset.php") val file = myFixture.configureByFile("tests/Datasets/Dataset.php") assertEquals( PestIcons.Dataset, PestIconProvider().getIcon(file.virtualFile, ICON_FLAG_VISIBILITY, project) ) } fun testCanGetPestIconForDatasetFileWithTests() { myFixture.copyFileToProject("TestWithDataset.php", "/tests/Datasets/TestWithDataset.php") val file = myFixture.configureByFile("tests/Datasets/TestWithDataset.php") assertEquals( PestIcons.File, PestIconProvider().getIcon(file.virtualFile, ICON_FLAG_VISIBILITY, project) ) } fun testCanGetPestIconForPestBaseFile() { val virtualFile = myFixture.copyFileToProject("Pest.php", "tests/Pest.php") val file = PsiManager.getInstance(project).findFile(virtualFile)!! assertEquals( PestIcons.Logo, PestIconProvider().getIcon(file.virtualFile, ICON_FLAG_VISIBILITY, project) ) } fun testCanGetPestIconForPestFileWithPropertyCall() { val virtualFile = myFixture.copyFileToProject("SimpleHigherOrderNotTest.php", "tests/SimpleHigherOrderNotTest.php") val file = PsiManager.getInstance(project).findFile(virtualFile)!! assertEquals( PestIcons.File, PestIconProvider().getIcon(file.virtualFile, ICON_FLAG_VISIBILITY, project) ) } } ================================================ FILE: src/test/kotlin/com/pestphp/pest/PestLightCodeFixture.kt ================================================ package com.pestphp.pest import com.intellij.openapi.util.io.FileUtil import com.intellij.psi.PsiFile import com.intellij.testFramework.TestDataPath import com.intellij.testFramework.fixtures.BasePlatformTestCase import com.intellij.testFramework.fixtures.IdeaTestExecutionPolicy import com.jetbrains.php.config.interpreters.PhpInterpreter import com.jetbrains.php.lang.PhpFileType import com.jetbrains.php.testFramework.PhpTestFrameworkConfiguration import com.jetbrains.php.testFramework.PhpTestFrameworkSettingsManager import io.mockk.clearAllMocks import io.mockk.unmockkAll import java.io.File import java.nio.file.Files import kotlin.io.path.Path @Suppress("UnnecessaryAbstractClass") @TestDataPath("\$CONTENT_ROOT/../resources/com/pestphp/pest") abstract class PestLightCodeFixture : BasePlatformTestCase() { companion object { private const val TEST_DATA_REL_PATH = "src/test/resources/com/pestphp/pest" private const val MONOREPO_PREFIX = "phpstorm/pest" private val baseTestDataPath: String by lazy { val homePath = IdeaTestExecutionPolicy.getHomePathWithPolicy() if (Files.exists(Path(homePath, MONOREPO_PREFIX, TEST_DATA_REL_PATH))) { "/$MONOREPO_PREFIX/$TEST_DATA_REL_PATH" } else { "/$TEST_DATA_REL_PATH" } } } override fun getBasePath() = baseTestDataPath override fun setUp() { super.setUp() val pestStubsFile = File("${IdeaTestExecutionPolicy.getHomePathWithPolicy()}$baseTestDataPath/stubs.php") if (pestStubsFile.exists()) { myFixture.copyFileToProject(pestStubsFile.absolutePath, "stubs.php") } } override fun tearDown() { try { clearAllMocks() unmockkAll() } catch (e: Throwable) { addSuppressedException(e) } finally { super.tearDown() } } private val testNameSeparator = '#' protected fun assertCompletion(vararg shouldContain: String) { myFixture.completeBasic() val strings = myFixture.lookupElementStrings ?: return fail("empty completion result") assertContainsElements(strings, shouldContain.asList()) } protected fun assertAllCompletion(vararg shouldContain: String) { myFixture.completeBasic() val strings = myFixture.lookupElementStrings ?: return fail("empty completion result") assertEquals(shouldContain.toList(), strings) } protected fun assertNoCompletion() { myFixture.completeBasic() val strings = myFixture.lookupElementStrings assertNullOrEmpty(strings) } protected fun createPestFrameworkConfiguration(): PhpTestFrameworkConfiguration? { val configuration = PhpTestFrameworkSettingsManager .getInstance(myFixture.project) .getOrCreateByInterpreter(PestFrameworkType.instance, PhpInterpreter(), null, false) configuration?.executablePath = "randomPath" return configuration } protected fun configureByPhpCode(code: String) { myFixture.configureByText(PhpFileType.INSTANCE, " val relativePath = FileUtil.toSystemIndependentName(file.path).removePrefix("$testDataPath/") myFixture.copyFileToProject(relativePath) } } } protected fun configureByFile(): PsiFile { val path = getFileNameBeforeRelativePath() configureByDirectory(path) return myFixture.configureByFile(path) } private fun getFullPath(basePath: String, relativePath: String): String { return "$basePath/$relativePath" } protected fun getFileNameBeforeRelativePath(): String { return getTestName(false).let { testPath -> val phpTestPath = "$testPath.php" if (phpTestPath.contains(testNameSeparator)) { phpTestPath.replace(testNameSeparator, File.separatorChar) } else { phpTestPath } } } protected fun getFileBeforeFullPath(): String { return getFullPath(testDataPath, getFileNameBeforeRelativePath()) } } ================================================ FILE: src/test/kotlin/com/pestphp/pest/PestTestRunLineMarkerProviderTest.kt ================================================ package com.pestphp.pest import com.intellij.codeInsight.daemon.impl.DaemonCodeAnalyzerImpl import com.intellij.execution.actions.ConfigurationContext import com.intellij.testFramework.TestDataPath import com.jetbrains.php.testFramework.PhpTestFrameworkConfiguration import com.jetbrains.php.testFramework.PhpTestFrameworkSettingsManager import com.pestphp.pest.configuration.PestRunConfigurationType @TestDataPath("\$CONTENT_ROOT/../resources/com/pestphp/pest/PestTestRunLineMarkerProviderTest") class PestTestRunLineMarkerProviderTest : PestLightCodeFixture() { private lateinit var pestConfigurations: List override fun getBasePath(): String = "${super.getBasePath()}/PestTestRunLineMarkerProviderTest" override fun setUp() { super.setUp() pestConfigurations = PhpTestFrameworkSettingsManager.getInstance(project).getConfigurations(PestFrameworkType.instance) } override fun tearDown() { try { PhpTestFrameworkSettingsManager.getInstance(project).setConfigurations(PestFrameworkType.instance, pestConfigurations) } catch (e: Throwable) { addSuppressedException(e) } finally { super.tearDown() } } private fun doTest(vararg expectedMarkerLines: Int) { myFixture.doHighlighting() val editor = myFixture.editor val markerList = DaemonCodeAnalyzerImpl.getLineMarkers(editor.document, project) val actualMarkerLines = markerList.map { marker -> editor.offsetToLogicalPosition(marker.startOffset).line } assertSameElements(actualMarkerLines, expectedMarkerLines.toList()) } private fun initConfiguration() { val configuration = createPestFrameworkConfiguration() PhpTestFrameworkSettingsManager.getInstance(project).setConfigurations(PestFrameworkType.instance, listOf(configuration)) } fun testMethodCallNamedItAndVariableTestIsNotPestTest() { myFixture.configureByFile("MethodCallNamedItAndVariableTest.php") doTest() } fun testFunctionCallNamedItWithDescriptionAndClosure() { myFixture.configureByFile("PestItFunctionCallWithDescriptionAndClosure.php") doTest(0, 2) } fun testFunctionCallNamedItRedefinition() { myFixture.configureByFile("PestItFunctionCallWithRedefinition.php") doTest() } fun testFunctionCallNamedTestWithoutPest() { myFixture.configureByFile("FunctionCallNamedTestWithoutPest.php") doTest() } fun testAssignmentFunctionCallNamedTestWithoutPest() { myFixture.configureByFile("AssignmentFunctionCallNamedTestWithoutPest.php") doTest() } fun testAssignmentFunctionCallNamedTest() { myFixture.configureByFile("AssignmentFunctionCallNamedTest.php") doTest() } fun testFunctionCallNamedTestAsArgument() { myFixture.configureByFile("FunctionCallNamedTestAsArgument.php") doTest() } fun testFunctionCallNamedTestInsideDescribeBlock() { myFixture.configureByFile("FunctionCallNamedTestInsideDescribeBlock.php") doTest(0, 2, 3) } fun testFunctionCallNamedTestInsideTest() { myFixture.configureByFile("FunctionCallNamedTestInsideTest.php") doTest(0, 2) } fun testDataSetsAreNotYetMarkedAsRunnable() { myFixture.configureByFile("NamedDataSets.php") doTest(0, 2, 3, 9) } fun testRunContextFromTestDirectory() { initConfiguration() val rootDirectory = myFixture.copyDirectoryToProject("contextProject", ".") val context = myFixture.psiManager.findDirectory(rootDirectory.findChild("tests")!!)!! val configurationsFromContext = ConfigurationContext(context).configurationsFromContext!! assertSize(2, configurationsFromContext) } fun testRunContextFromPestTestFile() { initConfiguration() myFixture.copyDirectoryToProject("contextProject", ".") val context = myFixture.configureByFile("contextProject/tests/Test.php") val configurationsFromContext = ConfigurationContext(context).configurationsFromContext!! assertSize(1, configurationsFromContext) assertEquals(PestRunConfigurationType.instance, configurationsFromContext[0].configurationType) } } ================================================ FILE: src/test/kotlin/com/pestphp/pest/annotator/PestAnnotatorTest.kt ================================================ package com.pestphp.pest.annotator import com.pestphp.pest.PestBundle import com.pestphp.pest.PestLightCodeFixture class PestAnnotatorTest: PestLightCodeFixture() { override fun getBasePath(): String = "${super.getBasePath()}/annotator" override fun setUp() { super.setUp() myFixture.copyDirectoryToProject("stub", ".") } fun testHasDuplicateCustomExpectation() { myFixture.configureByFile("DuplicateCustomExpectation.php") myFixture.checkHighlighting() } fun testDeleteDuplicateCustomExpectation() { myFixture.configureByFile("DuplicateCustomExpectation.php") myFixture.checkHighlighting() myFixture.getAllQuickFixes().first { it.familyName == PestBundle.message("QUICK_FIX_DELETE_CUSTOM_EXPECTATION", "toBeOne") } .run { myFixture.launchAction(this) } myFixture.checkResultByFile("DuplicateCustomExpectation.afterDelete.php") } fun testNavigateToNextDuplicateCustomExpectation() { myFixture.configureByFile("DuplicateCustomExpectation.php") myFixture.checkHighlighting() myFixture.getAllQuickFixes().last { it.familyName == PestBundle.message("INTENTION_NAVIGATE_TO_DUPLICATE_CUSTOM_EXPECTATION") } .run { myFixture.launchAction(this) } myFixture.checkResultByFile("DuplicateCustomExpectation.afterNavigate.php") } fun testNoDuplicateCustomExpectation() { myFixture.configureByFile("NoDuplicateCustomExpectation.php") myFixture.checkHighlighting() } fun testHasDuplicateTest() { myFixture.configureByFile("DuplicateTestName.php") myFixture.checkHighlighting() } fun testDeleteDuplicateTest() { myFixture.configureByFile("DuplicateTestName.php") myFixture.checkHighlighting() myFixture.getAllQuickFixes().first { it.familyName == PestBundle.message("QUICK_FIX_DELETE_TEST", "basic") } .run { myFixture.launchAction(this) } myFixture.checkResultByFile("DuplicateTestName.afterDelete.php") } fun testNavigateToDuplicateTest() { myFixture.configureByFile("DuplicateTestName.php") myFixture.checkHighlighting() myFixture.getAllQuickFixes().last { it.familyName == PestBundle.message("INTENTION_NAVIGATE_TO_DUPLICATE_TEST_NAME") } .run { myFixture.launchAction(this) } myFixture.checkResultByFile("DuplicateTestName.afterNavigate.php") } fun testNoDuplicateTest() { myFixture.configureByFile("NoDuplicateTestName.php") myFixture.checkHighlighting() } fun testHasDuplicateTestInDescribeBlock() { myFixture.configureByFile("DuplicateTestNameInDescribeBlock.php") myFixture.checkHighlighting() } } ================================================ FILE: src/test/kotlin/com/pestphp/pest/codeInsight/typeInference/PestTypeInferenceTest.kt ================================================ package com.pestphp.pest.codeInsight.typeInference import com.intellij.openapi.application.WriteAction import com.intellij.openapi.util.Segment import com.intellij.openapi.util.TextRange import com.intellij.openapi.util.text.StringUtil import com.intellij.psi.PsiDocumentManager import com.intellij.psi.PsiFile import com.jetbrains.php.lang.PhpLangUtil import com.jetbrains.php.lang.psi.PhpPsiUtil import com.jetbrains.php.lang.psi.elements.PhpPsiElement import com.jetbrains.php.lang.psi.elements.PhpTypedElement import com.jetbrains.php.lang.psi.resolve.types.PhpType import com.pestphp.pest.PestLightCodeFixture import com.pestphp.pest.PestSettings import java.util.regex.Pattern class PestTypeInferenceTest : PestLightCodeFixture() { private lateinit var pestFilePathBackup: String override fun getBasePath(): String = "${super.getBasePath()}/codeInsight/typeInference" private fun doTest(block: () -> PsiFile) { PestSettings.getInstance(project).pestFilePath = "Pest.php" val pestPhpFile = block() myFixture.openFileInEditor(pestPhpFile.virtualFile) doTypeTest() } private fun doTypeTest() { val file = configureByFile() val originalFileText = file.text val fileTextWithExtractedTypes = StringBuilder() val rangesToTypes = extractRangesToTypes(originalFileText, fileTextWithExtractedTypes) val fileWithExtractedTypes = replaceContentOfConfiguredFile(fileTextWithExtractedTypes) val text = fileWithExtractedTypes.text var lastOffset = 0 val contentWithActualTypes = StringBuilder() for ((segment, stringTypes) in rangesToTypes.entries) { val actualType = getType(segment.startOffset, segment.endOffset) if (actualType.hasUnknown()) { actualType.add(PhpType.MIXED) } val diffType = constructDiffType(actualType, stringTypes) contentWithActualTypes.append(text, lastOffset, segment.startOffset) appendActualType(text, contentWithActualTypes, segment, diffType) lastOffset = segment.endOffset } contentWithActualTypes.append(text.substring(lastOffset)) assertEquals("Types are not matched", originalFileText, contentWithActualTypes.toString()) } private fun replaceContentOfConfiguredFile(fileTextWithExtractedTypes: StringBuilder): PsiFile { WriteAction.run { myFixture.editor.getDocument().setText(fileTextWithExtractedTypes.toString()) } PsiDocumentManager.getInstance(project).commitAllDocuments() return myFixture.file } private fun getType(start: Int, end: Int): PhpType { val expression = PhpPsiUtil.findElementOfClassAtRange( myFixture.file, start, end, PhpPsiElement::class.java, true, true ) assertNotNull(expression) assertTrue(expression!!.text, expression is PhpTypedElement) return (expression as PhpTypedElement?)!!.globalType } fun testThisInInnerClosure() = doTest { myFixture.addFileToProject("Pest.php", "in("./"); """.trimIndent() ) } override fun setUp() { super.setUp() pestFilePathBackup = PestSettings.getInstance(project).pestFilePath } override fun tearDown() { try { PestSettings.getInstance(project).pestFilePath = pestFilePathBackup } catch (e: Throwable) { addSuppressedException(e) } finally { super.tearDown() } } } private const val CLOSING_TYPE_TAG = "" private val PATTERN = Pattern.compile("") private fun extractRangesToTypes(text: String, contentWithExtractedTypes: java.lang.StringBuilder): LinkedHashMap> { val res = LinkedHashMap>() var lastOffset = 0 val matcher = PATTERN.matcher(text) while (matcher.find()) { contentWithExtractedTypes.append(text, lastOffset, matcher.start()) val closingTagIndexOf = text.indexOf(CLOSING_TYPE_TAG, lastOffset) val start = contentWithExtractedTypes.length contentWithExtractedTypes.append(text, matcher.end(), closingTagIndexOf) val end = contentWithExtractedTypes.length res[TextRange.create(start, end)] = StringUtil.split(matcher.group(1), "|") lastOffset = closingTagIndexOf + CLOSING_TYPE_TAG.length } contentWithExtractedTypes.append(text.substring(lastOffset)) return res } private fun constructDiffType(type: PhpType, strings: List): String { return type.types.asSequence() .filter { t -> !t.startsWith("?") } .filter { qualifiedName -> PhpLangUtil.isFqn(qualifiedName!!) } .map { fqn -> toPresentableFQN(fqn) } .sortedBy { fqn -> strings.indexOf(fqn) } .joinToString("|") } private fun toPresentableFQN(fqn: String): String { return if (PhpLangUtil.isGlobalNamespaceFQN(PhpLangUtil.getParentNamespaceFQN(fqn))) PhpLangUtil.toPresentableFQN(fqn) else fqn } private fun appendActualType(text: String, contentWithActualTypes: java.lang.StringBuilder, segment: Segment, diffType: String) { contentWithActualTypes.append("") contentWithActualTypes.append(text, segment.startOffset, segment.endOffset) contentWithActualTypes.append(CLOSING_TYPE_TAG) } ================================================ FILE: src/test/kotlin/com/pestphp/pest/configuration/PestLocationProviderTest.kt ================================================ package com.pestphp.pest.configuration import com.intellij.psi.search.GlobalSearchScope import com.intellij.testFramework.PlatformTestUtil import com.jetbrains.php.lang.psi.elements.FunctionReference import com.jetbrains.php.phpunit.PhpPsiLocationWithDataSet import com.jetbrains.php.util.pathmapper.PhpPathMapper import com.pestphp.pest.PestLightCodeFixture class PestLocationProviderTest : PestLightCodeFixture() { override fun getBasePath(): String = "${super.getBasePath()}/configuration/locationProvider" fun testSubprojectFor2xVersion() { val testName = "test" doTestGetLocation( "Test.php::$testName", testName, "subdir" ) } fun testSubprojectFor1xVersion() { val testName = "test" doTestGetLocation( "Test.php::$testName", testName, "subdir", true ) } fun testDescribeBlock() { doTestGetLocation( "Test.php::`sum` → check true", "test", "subdir" ) } fun testDescribeBlockIt() { doTestGetLocation( "Test.php::`sum` → it check valid", "it", "subdir" ) } private fun doTestGetLocation( pathSuffix: String, expectedTestName: String, configurationFileRelativePath: String? = null, isAbsolutePath: Boolean = false, ) { val testDir = PlatformTestUtil.getTestName(name, false) myFixture.copyDirectoryToProject(testDir, ".") val configurationFileRootPath = "${myFixture.testDataPath}/${testDir}${configurationFileRelativePath?.let { "/$it" } ?: ""}" val locationProvider = PestLocationProvider(PhpPathMapper.create(project), project, configurationFileRootPath) val path = if (isAbsolutePath) "$configurationFileRootPath/$pathSuffix" else pathSuffix val resolvedLocation = locationProvider .getLocation("pest_qn", path, project, GlobalSearchScope.allScope(project)).firstOrNull() assertInstanceOf(resolvedLocation, PhpPsiLocationWithDataSet::class.java) assertInstanceOf(resolvedLocation?.psiElement, FunctionReference::class.java) assertEquals((resolvedLocation?.getPsiElement() as? FunctionReference)?.name, expectedTestName) } } ================================================ FILE: src/test/kotlin/com/pestphp/pest/configuration/PestRunConfigurationTest.kt ================================================ package com.pestphp.pest.configuration import com.intellij.execution.PsiLocation import com.intellij.execution.actions.ConfigurationContext import com.jetbrains.php.config.commandLine.PhpCommandSettings import com.jetbrains.php.config.interpreters.PhpInterpreter import com.jetbrains.php.config.interpreters.PhpInterpretersManagerImpl import com.jetbrains.php.phpunit.coverage.PhpUnitCoverageEngine import com.jetbrains.php.testFramework.PhpTestFrameworkConfiguration import com.jetbrains.php.testFramework.PhpTestFrameworkSettingsManager import com.pestphp.pest.PestFrameworkType import com.pestphp.pest.PestLightCodeFixture class PestRunConfigurationTest : PestLightCodeFixture() { private lateinit var configurationsBackup: List override fun getBasePath(): String = "${super.getBasePath()}/configuration" fun testRunConfigurationRunnerSettingsIsPestRunnerSettings() { val configuration = createConfiguration() assertNotNull(configuration) assertInstanceOf(configuration.settings, PestRunConfigurationSettings::class.java) assertInstanceOf(configuration.settings.runnerSettings, PestRunnerSettings::class.java) } fun testRunConfigurationClone() { val configuration = createConfiguration() configuration.pestSettings.pestRunnerSettings.coverageEngine = PhpUnitCoverageEngine.CoverageEngine.PCOV configuration.pestSettings.pestRunnerSettings.parallelTestingEnabled = true val clone = configuration.clone() as PestRunConfiguration assertEquals(PhpUnitCoverageEngine.CoverageEngine.PCOV, clone.pestSettings.pestRunnerSettings.coverageEngine) assertEquals(true, clone.pestSettings.pestRunnerSettings.parallelTestingEnabled) } fun testRunConfigurationCommand() { val command = createCommand() val expectedCommand = "randomPath --teamcity /src/FileWithPestTest.php" assertEquals(expectedCommand, command.createGeneralCommandLine().parametersList.list.joinToString(" ")) } fun testRunConfigurationCommandWithEnabledParallelTesting() { val configuration = createConfiguration() configuration.pestSettings.pestRunnerSettings.parallelTestingEnabled = true val command = configuration.createCommand(configuration.interpreter!!, mutableMapOf(), mutableListOf(), false) val expectedCommand = "randomPath --teamcity /src/FileWithPestTest.php --parallel --log-teamcity php://stdout" assertEquals(expectedCommand, command.createGeneralCommandLine().parametersList.list.joinToString(" ")) } private fun createCommand(): PhpCommandSettings { val configuration = createConfiguration() return configuration.createCommand(configuration.interpreter!!, mutableMapOf(), mutableListOf(), false) } private fun createConfiguration(): PestRunConfiguration { val file = myFixture.configureByFile("FileWithPestTest.php") val elementAtCaret = file.findElementAt(myFixture.editor.caretModel.offset) createPestFrameworkConfiguration() val configuration = PestRunConfigurationProducer().createConfigurationFromContext( ConfigurationContext.createEmptyContextForLocation( PsiLocation.fromPsiElement(elementAtCaret) ) )?.configuration as PestRunConfiguration configuration.settings.commandLineSettings.interpreterSettings.interpreterName = getTestName(false) return configuration } override fun setUp() { super.setUp() val interpreter = PhpInterpreter().apply { name = getTestName(false) homePath = "$testDataPath/php" } PhpInterpretersManagerImpl.getInstance(project).addInterpreter(interpreter) configurationsBackup = PhpTestFrameworkSettingsManager.getInstance(project).getConfigurations(PestFrameworkType.instance) } override fun tearDown() { try { PhpTestFrameworkSettingsManager.getInstance(project).setConfigurations(PestFrameworkType.instance, configurationsBackup) PhpInterpretersManagerImpl.getInstance(project).interpreters = emptyList() } catch (e: Throwable) { addSuppressedException(e) } finally { super.tearDown() } } } ================================================ FILE: src/test/kotlin/com/pestphp/pest/configuration/PestVersionDetectorTest.kt ================================================ package com.pestphp.pest.configuration import com.intellij.execution.ExecutionException import com.jetbrains.php.config.interpreters.PhpInterpreter import com.pestphp.pest.PestBundle import com.pestphp.pest.PestLightCodeFixture import io.mockk.every import io.mockk.mockk class PestVersionDetectorTest : PestLightCodeFixture() { fun testThrowsOnRemoteInterpreter() { val versionDetector = PestVersionDetector() val mockInterpreter = mockk() every { mockInterpreter.isRemote } returns true assertThrows( ExecutionException::class.java, PestBundle.message("PEST_VERSION_IS_NOT_SUPPORTED_FOR_REMOTE_INTERPRETER") ) { versionDetector.getVersion(project, mockInterpreter, null) } } } ================================================ FILE: src/test/kotlin/com/pestphp/pest/configuration/PestVersionParserTest.kt ================================================ package com.pestphp.pest.configuration import com.intellij.execution.ExecutionException import com.pestphp.pest.PestBundle import com.pestphp.pest.PestLightCodeFixture class PestVersionParserTest : PestLightCodeFixture() { private fun doTest(output: String, expected: String) { val version = PestVersionDetector.instance.parse(output) assertNotNull(version) assertEquals(version, expected) } private fun doFailedTest(output: String) { assertThrows(ExecutionException::class.java, PestBundle.message("PEST_CONFIGURATION_UI_CAN_NOT_PARSE_VERSION", output)) { PestVersionDetector.instance.parse(output) } } fun testPestOutputBeforeV2() { doTest(""" Pest 1.21.0 PHPUnit 9.6.15 """.trimIndent(), "1.21.0") } fun testPestOutputAfterV2() { doTest(""" Pest Testing Framework 2.21.0. """.trimIndent(), "2.21.0") } fun testIncorrectFormatBeforeV2() { doFailedTest("Some text 1.21.0") } fun testIncorrectFormatAfterV2() { doFailedTest(""" Pest 2.21.0. """.trimIndent()) } fun testIncorrectVersionFormat() { doFailedTest("Pest 1.21") } } ================================================ FILE: src/test/kotlin/com/pestphp/pest/configuration/pest/PestConfigurationFileTest.kt ================================================ package com.pestphp.pest.configuration.pest import com.pestphp.pest.PestLightCodeFixture class PestConfigurationFileTest : PestLightCodeFixture() { override fun setUp() { super.setUp() myFixture.copyDirectoryToProject(".", ".") } override fun getBasePath(): String = "${super.getBasePath()}/configuration/pest" fun testUnit() { myFixture.configureByFile("tests/Unit/UnitTest.php") assertCompletion("baseTestFunc") } fun testPestExtendUsesUnit() { myFixture.configureByFile("tests/Unit/PestExtendUnitTest.php") assertCompletion("baseTestFunc", "traitFunc") } fun testFeature() { myFixture.configureByFile("tests/Feature/FeatureTest.php") assertCompletion("baseTestFunc", "featureTestFunc") } fun testGroupedFeature() { myFixture.configureByFile("tests/GroupedFeature/GroupedFeatureTest.php") assertCompletion("baseTestFunc", "featureTestFunc", "someBaseTraitFunc") } fun testDIRFeature() { myFixture.configureByFile("tests/DIRFeature/FeatureTest.php") assertCompletion("baseTestFunc", "featureTestFunc") } fun testDynamicFolderFeature() { myFixture.configureByFile("tests/DynamicFeature/FeatureTest.php") assertCompletion("baseTestFunc", "featureTestFunc") } fun testUnitOutsideOfPestTestDir() { myFixture.configureByFile("tests2/Unit/UnitTest.php") assertCompletion() } fun testGlobPatternDirectory() { myFixture.configureByFile("tests/GlobPattern/DirectoryTest.php") assertCompletion("baseTestFunc", "featureTestFunc") } fun testGlobPatternFile() { myFixture.configureByFile("tests/GlobPattern/FileTest.php") assertCompletion("baseTestFunc", "featureTestFunc") } fun testGlobPatternFileWithRelativePath() { myFixture.configureByFile("tests/GlobPattern/FileWithRelativePathTest.php") assertCompletion("baseTestFunc", "featureTestFunc") } } ================================================ FILE: src/test/kotlin/com/pestphp/pest/configuration/uses/PestConfigurationFileTest.kt ================================================ package com.pestphp.pest.configuration.uses import com.pestphp.pest.PestLightCodeFixture class PestConfigurationFileTest : PestLightCodeFixture() { override fun setUp() { super.setUp() myFixture.copyDirectoryToProject(".", "tests") } override fun getBasePath(): String = "${super.getBasePath()}/configuration/uses" fun testUnit() { myFixture.configureByFile("tests/Unit/UnitTest.php") assertCompletion("baseTestFunc") } fun testUsesUnit() { myFixture.configureByFile("tests/Unit/UsesUnitTest.php") assertCompletion("baseTestFunc", "traitFunc") } fun testFeature() { myFixture.configureByFile("tests/Feature/FeatureTest.php") assertCompletion("baseTestFunc", "featureTestFunc") } fun testGroupedFeature() { myFixture.configureByFile("tests/GroupedFeature/GroupedFeatureTest.php") assertCompletion("baseTestFunc", "featureTestFunc", "someBaseTraitFunc") } fun testDIRFeature() { myFixture.configureByFile("tests/DIRFeature/FeatureTest.php") assertCompletion("baseTestFunc", "featureTestFunc") } fun testDynamicFolderFeature() { myFixture.configureByFile("tests/DynamicFeature/FeatureTest.php") assertCompletion("baseTestFunc", "featureTestFunc") } fun testGlobPatternDirectory() { myFixture.configureByFile("tests/GlobPattern/DirectoryTest.php") assertCompletion("baseTestFunc", "featureTestFunc") } fun testGlobPatternFile() { myFixture.configureByFile("tests/GlobPattern/FileTest.php") assertCompletion("baseTestFunc", "featureTestFunc") } fun testGlobPatternFileWithRelativePath() { myFixture.configureByFile("tests/GlobPattern/FileWithRelativePathTest.php") assertCompletion("baseTestFunc", "featureTestFunc") } } ================================================ FILE: src/test/kotlin/com/pestphp/pest/customExpectations/ListMethodDataExternalizerTest.kt ================================================ package com.pestphp.pest.customExpectations import com.intellij.util.io.DataOutputStream import com.jetbrains.php.lang.psi.resolve.types.PhpType import com.pestphp.pest.PestLightCodeFixture import com.pestphp.pest.features.customExpectations.externalizers.ListDataExternalizer import com.pestphp.pest.features.customExpectations.externalizers.MethodDataExternalizer import com.pestphp.pest.features.customExpectations.generators.Method import com.pestphp.pest.features.customExpectations.generators.Parameter import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream import java.io.DataInputStream class ListMethodDataExternalizerTest : PestLightCodeFixture() { private fun saveAndRead(method: List): List { val dataOutput = ByteArrayOutputStream() val externalizer = ListDataExternalizer(MethodDataExternalizer.INSTANCE) externalizer.save( DataOutputStream(dataOutput), method ) val dataInput = DataInputStream( ByteArrayInputStream(dataOutput.toByteArray()) ) return externalizer.read(dataInput) } fun testCanExternalizeMethod() { val methods = listOf( Method( "works", PhpType.BOOLEAN, emptyList() ) ) val deserializedMethods = saveAndRead(methods) assertEquals( methods, deserializedMethods ) } fun testCanExternalizeMultipleMethods() { val methods = listOf( Method( "works", PhpType.BOOLEAN, emptyList() ), Method( "alsoWorks", PhpType.BOOLEAN, emptyList() ) ) val deserializedMethods = saveAndRead(methods) assertEquals( methods, deserializedMethods ) } fun testCanExternalizeMethodsWithMultipleTypes() { val methods = listOf( Method( "works", PhpType.builder().add("random").add(PhpType.EXCEPTION).build(), emptyList() ), Method( "valid", PhpType.builder().add("some").add(PhpType.STRING).build(), emptyList() ) ) val deserializedMethods = saveAndRead(methods) assertEquals( methods, deserializedMethods ) } fun testCanExternalizeMethodsWithParameter() { val methods = listOf( Method( "getName", PhpType.BOOLEAN, listOf( Parameter( "name", PhpType.STRING, ) ) ), Method( "somethingElse", PhpType.BOOLEAN, listOf() ), Method( "alsoWorks", PhpType.BOOLEAN, listOf( Parameter( "value", PhpType.BOOLEAN, ) ) ), ) val deserializedMethods = saveAndRead(methods) assertEquals( methods, deserializedMethods ) } fun testCanExternalizeMethodWithParameterWithDefaultValue() { val methods = listOf( Method( "getName", PhpType.BOOLEAN, listOf( Parameter( "name", PhpType.STRING, "'Oliver'" ) ) ), Method( "otherMethod", PhpType.BOOLEAN, listOf( Parameter( "name", PhpType.STRING, "true" ) ) ) ) val deserializedMethods = saveAndRead(methods) assertEquals( methods, deserializedMethods ) } fun testCanExternalizeMethodWithMultipleParameters() { val methods = listOf( Method( "someMethod", PhpType.BOOLEAN, listOf( Parameter( "parameter1", PhpType.STRING, ), Parameter( "otherParameter", PhpType.STRING, "'someValue'" ) ) ), Method( "andAnotherOne", PhpType.EXCEPTION, listOf( Parameter( "parameter3", PhpType.STRING, ), Parameter( "otherParameter2", PhpType.STRING, "false" ) ) ) ) val deserializedMethods = saveAndRead(methods) assertEquals( methods, deserializedMethods ) } } ================================================ FILE: src/test/kotlin/com/pestphp/pest/customExpectations/MethodDataExternalizerTest.kt ================================================ package com.pestphp.pest.customExpectations import com.intellij.util.io.DataOutputStream import com.jetbrains.php.lang.psi.resolve.types.PhpType import com.pestphp.pest.PestLightCodeFixture import com.pestphp.pest.features.customExpectations.externalizers.MethodDataExternalizer import com.pestphp.pest.features.customExpectations.generators.Method import com.pestphp.pest.features.customExpectations.generators.Parameter import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream import java.io.DataInputStream class MethodDataExternalizerTest : PestLightCodeFixture() { private fun saveAndRead(method: Method): Method { val dataOutput = ByteArrayOutputStream() val externalizer = MethodDataExternalizer() externalizer.save( DataOutputStream(dataOutput), method ) val dataInput = DataInputStream( ByteArrayInputStream(dataOutput.toByteArray()) ) return externalizer.read(dataInput) } fun testCanExternalizeMethod() { val method = Method( "works", PhpType.BOOLEAN, emptyList() ) val deserializedMethod = saveAndRead(method) assertEquals( method, deserializedMethod ) } fun testCanExternalizeMethodWithMultipleTypes() { val method = Method( "works", PhpType.builder().add("random").add(PhpType.EXCEPTION).build(), emptyList() ) val deserializedMethod = saveAndRead(method) assertEquals( method, deserializedMethod ) } fun testCanExternalizeMethodWithParameter() { val method = Method( "getName", PhpType.BOOLEAN, listOf( Parameter( "name", PhpType.STRING, ) ) ) val deserializedMethod = saveAndRead(method) assertEquals( method, deserializedMethod ) } fun testCanExternalizeMethodWithParameterWithDefaultValue() { val method = Method( "getName", PhpType.BOOLEAN, listOf( Parameter( "name", PhpType.STRING, "'Oliver'" ) ) ) val deserializedMethod = saveAndRead(method) assertEquals( method, deserializedMethod ) } fun testCanExternalizeMethodWithMultipleParameters() { val method = Method( "someMethod", PhpType.BOOLEAN, listOf( Parameter( "parameter1", PhpType.STRING, ), Parameter( "otherParameter", PhpType.STRING, "'someValue'" ) ) ) val deserializedMethod = saveAndRead(method) assertEquals( method, deserializedMethod ) } } ================================================ FILE: src/test/kotlin/com/pestphp/pest/customExpectations/generators/ExpectationGeneratorTest.kt ================================================ package com.pestphp.pest.customExpectations.generators import com.jetbrains.php.lang.psi.resolve.types.PhpType import com.pestphp.pest.PestLightCodeFixture import com.pestphp.pest.features.customExpectations.generators.ExpectationGenerator import com.pestphp.pest.features.customExpectations.generators.Method class ExpectationGeneratorTest : PestLightCodeFixture() { fun testCanGenerateFileWithMethod() { val generator = ExpectationGenerator() generator.docMethods.add( Method( "works", PhpType.BOOLEAN, emptyList() ) ) val result = generator.generate(project) val expectedClass = ExpectationGeneratorTest::class.java .getResource("/com/pestphp/pest/customExpectations/generators/ExpectationGenerator/GeneratedWithMethod.php") ?.readText() ?: fail("File not found.") assertEquals( result, expectedClass ) } } ================================================ FILE: src/test/kotlin/com/pestphp/pest/features/configuration/PestCompletionTest.kt ================================================ package com.pestphp.pest.features.configuration import com.intellij.testFramework.TestDataPath import com.pestphp.pest.PestLightCodeFixture @TestDataPath("\$CONTENT_ROOT/../resources/com/pestphp/pest/features/configuration/pest") class PestCompletionTest : PestLightCodeFixture() { override fun getBasePath(): String = "${super.getBasePath()}/features/configuration/pest" fun testCanCompleteSubFolder() { myFixture.copyFileToProject("Test.php", "myFolder/Test.php") myFixture.copyFileToProject("Test.php", "other/Test.php") myFixture.configureByFile("CompleteInFolder.php") assertCompletion("myFolder", "other") } fun testDoesNotCompleteInNotPestInStatement() { myFixture.copyFileToProject("Test.php", "myFolder/Test.php") myFixture.copyFileToProject("Test.php", "other/Test.php") myFixture.configureByFile("CompleteFakePestInFolder.php") assertNoCompletion() } fun testOnlyCompletesFoldersUnderFile() { myFixture.copyFileToProject("Test.php", "myFolder/Test.php") myFixture.copyFileToProject("Test.php", "tests/Feature/Test.php") myFixture.copyFileToProject("Test.php", "tests/Unit/Test.php") val virtualFile = myFixture.copyFileToProject("CompleteInFolder.php", "tests/Pest.php") myFixture.configureFromExistingVirtualFile(virtualFile) assertAllCompletion("Feature", "Unit") } } ================================================ FILE: src/test/kotlin/com/pestphp/pest/features/configuration/UsesCompletionTest.kt ================================================ package com.pestphp.pest.features.configuration import com.intellij.testFramework.TestDataPath import com.pestphp.pest.PestLightCodeFixture @TestDataPath("\$CONTENT_ROOT/../resources/com/pestphp/pest/features/configuration/uses") class UsesCompletionTest : PestLightCodeFixture() { override fun getBasePath(): String = "${super.getBasePath()}/features/configuration/uses" fun testCanCompleteSubFolder() { myFixture.copyFileToProject("Test.php", "myFolder/Test.php") myFixture.copyFileToProject("Test.php", "other/Test.php") myFixture.configureByFile("CompleteInFolder.php") assertCompletion("myFolder", "other") } fun testDoesNotCompleteInNotUsesInStatement() { myFixture.copyFileToProject("Test.php", "myFolder/Test.php") myFixture.copyFileToProject("Test.php", "other/Test.php") myFixture.configureByFile("CompleteFakeInFolder.php") assertNoCompletion() } fun testOnlyCompletesFoldersUnderFile() { myFixture.copyFileToProject("Test.php", "myFolder/Test.php") myFixture.copyFileToProject("Test.php", "tests/Feature/Test.php") myFixture.copyFileToProject("Test.php", "tests/Unit/Test.php") val virtualFile = myFixture.copyFileToProject("CompleteInFolder.php", "tests/Pest.php") myFixture.configureFromExistingVirtualFile(virtualFile) assertAllCompletion("Feature", "Unit") } } ================================================ FILE: src/test/kotlin/com/pestphp/pest/features/datasets/DatasetCompletionTest.kt ================================================ package com.pestphp.pest.features.datasets import com.intellij.testFramework.TestDataPath import com.pestphp.pest.PestLightCodeFixture @TestDataPath("\$CONTENT_ROOT/../resources/com/pestphp/pest/features/datasets") class DatasetCompletionTest : PestLightCodeFixture() { override fun getBasePath(): String = "${super.getBasePath()}/features/datasets" fun testCanCompleteDatasetInSameFile() { myFixture.configureByFile("AutocompleteDatasetTest.php") assertCompletion("dojos") } fun testCanCompleteDatasetInOtherFile() { myFixture.copyFileToProject("Datasets.php", "tests/Datasets/stances.php") myFixture.configureByFile("AutocompleteDatasetTest.php") assertCompletion("dojos", "stances") } fun testCannotCompleteDatasetOnNonPestTest() { myFixture.copyFileToProject("Datasets.php", "tests/Datasets/stances.php") myFixture.configureByFile("DatasetOnNonPestTestCompletion.php") assertNoCompletion() } fun testCanCompleteDatasetInDescribeBlock() { myFixture.configureByFile("DatasetInDescribeBlockCompletion.php") assertCompletion("some_numbers") } } ================================================ FILE: src/test/kotlin/com/pestphp/pest/features/datasets/DatasetGoToTest.kt ================================================ package com.pestphp.pest.features.datasets import com.intellij.codeInsight.navigation.actions.GotoDeclarationAction import com.intellij.testFramework.TestDataPath import com.jetbrains.php.lang.psi.elements.impl.FunctionReferenceImpl import com.pestphp.pest.PestLightCodeFixture @TestDataPath("\$CONTENT_ROOT/../resources/com/pestphp/pest/features/datasets") class DatasetGoToTest : PestLightCodeFixture() { override fun getBasePath(): String = "${super.getBasePath()}/features/datasets" fun testCanGoToDatasetInSameFile() { myFixture.configureByFile("DatasetReference.php") val declarationElement = GotoDeclarationAction.findTargetElement( project, myFixture.editor, myFixture.caretOffset ) as? FunctionReferenceImpl assertNotNull(declarationElement) assertTrue(declarationElement.isPestDataset()) assertEquals("dojos", declarationElement!!.getPestDatasetName()) } fun testCanGoToDatasetInOtherFile() { myFixture.copyFileToProject("Datasets.php", "tests/Datasets/stances.php") myFixture.configureByFile("SharedDatasetReference.php") val declarationElement = GotoDeclarationAction.findTargetElement( project, myFixture.editor, myFixture.caretOffset ) as? FunctionReferenceImpl assertNotNull(declarationElement) assertTrue(declarationElement.isPestDataset()) assertEquals("stances", declarationElement!!.getPestDatasetName()) } fun testCannotGoToDatasetInNonPestTest() { myFixture.copyFileToProject("Datasets.php", "tests/Datasets/stances.php") myFixture.configureByFile("DatasetOnNonPestTest.php") val declarationElement = GotoDeclarationAction.findTargetElement( project, myFixture.editor, myFixture.caretOffset ) assertNull(declarationElement) } fun testCanGoToDatasetInDescribeBlock() { myFixture.configureByFile("DatasetInDescribeBlockReference.php") val declarationElement = GotoDeclarationAction.findTargetElement( project, myFixture.editor, myFixture.caretOffset ) as? FunctionReferenceImpl assertNotNull(declarationElement) assertTrue(declarationElement.isPestDataset()) assertEquals("some_numbers", declarationElement!!.getPestDatasetName()) } } ================================================ FILE: src/test/kotlin/com/pestphp/pest/features/datasets/DatasetIndexTest.kt ================================================ package com.pestphp.pest.features.datasets import com.intellij.psi.search.GlobalSearchScope import com.intellij.testFramework.TestDataPath import com.intellij.util.indexing.FileBasedIndex import com.pestphp.pest.PestLightCodeFixture import junit.framework.TestCase @TestDataPath("\$CONTENT_ROOT/../resources/com/pestphp/pest/features/datasets") class DatasetIndexTest : PestLightCodeFixture() { override fun getBasePath(): String = "${super.getBasePath()}/features/datasets" fun testDatasetIsIndexed() { myFixture.copyFileToProject( "Datasets.php", "/tests/Datasets/Datasets.php" ) val fileBasedIndex = FileBasedIndex.getInstance() val values = fileBasedIndex.getValues( key, "/src/tests/Datasets/Datasets.php", GlobalSearchScope.projectScope(project) ).flatten() assertSize(1, values) TestCase.assertEquals( listOf("stances"), values ) } fun testDatasetIsNotIndexedWhenOutsideDatasetFolder() { val virtualFile = myFixture.copyFileToProject( "Datasets.php", "/tests/Datasets.php" ) val fileBasedIndex = FileBasedIndex.getInstance() val indexData = fileBasedIndex.getFileData( key, virtualFile, project ) assertEquals(0, indexData.count()) } fun testDatasetInsideDescribeBlockIsIndexed() { myFixture.copyFileToProject( "DatasetInDescribeBlock.php", "/tests/Datasets/DatasetInDescribeBlock.php" ) val fileBasedIndex = FileBasedIndex.getInstance() val values = fileBasedIndex.getValues( key, "/src/tests/Datasets/DatasetInDescribeBlock.php", GlobalSearchScope.projectScope(project) ).flatten() assertSize(2, values) assertContainsElements(values, "top_level", "inside_describe") } } ================================================ FILE: src/test/kotlin/com/pestphp/pest/features/datasets/DatasetReferenceTest.kt ================================================ package com.pestphp.pest.features.datasets import com.intellij.testFramework.TestDataPath import com.jetbrains.php.lang.psi.elements.impl.FunctionReferenceImpl import com.pestphp.pest.PestLightCodeFixture @TestDataPath("\$CONTENT_ROOT/../resources/com/pestphp/pest/features/datasets") class DatasetReferenceTest : PestLightCodeFixture() { override fun getBasePath(): String = "${super.getBasePath()}/features/datasets" fun testReferenceToDatasetInSameFile() { val file = myFixture.configureByFile("DatasetReference.php") val caretElement = file.findElementAt(myFixture.caretOffset) ?: return fail("No element") val datasetReference = caretElement.parent.references.filterIsInstance().firstOrNull() ?: return fail("No reference") val resolved = datasetReference.resolve() as? FunctionReferenceImpl ?: return fail("No function") assertTrue(resolved.isPestDataset()) assertEquals("dojos", resolved.getPestDatasetName()) } fun testReferenceDatasetInOtherFile() { myFixture.copyFileToProject("Datasets.php", "tests/Datasets/stances.php") val file = myFixture.configureByFile("SharedDatasetReference.php") val caretElement = file.findElementAt(myFixture.caretOffset) ?: return fail("No element") val datasetReference = caretElement.parent.references.filterIsInstance().firstOrNull() ?: return fail("No reference") val resolved = datasetReference.resolve() as? FunctionReferenceImpl ?: return fail("No function") assertTrue(resolved.isPestDataset()) assertEquals("stances", resolved.getPestDatasetName()) } fun testDoubleWith() { myFixture.copyFileToProject("Datasets.php", "tests/Datasets/stances.php") myFixture.configureByFile("DoubleWithDatasetReference.php") assertCompletion("stances") } fun testNotDataset() { myFixture.copyFileToProject("Datasets.php", "tests/Datasets/stances.php") myFixture.configureByFile("NotDatasetReference.php") assertNoCompletion() } fun testCannotGoToDatasetInNonPestTest() { myFixture.copyFileToProject("Datasets.php", "tests/Datasets/stances.php") val file = myFixture.configureByFile("DatasetOnNonPestTest.php") val caretElement = file.findElementAt(myFixture.caretOffset) ?: return fail("No element") val datasetReference = caretElement.parent.references.filterIsInstance() assertSize(0, datasetReference) } fun testReferenceToDatasetInDescribeBlock() { val file = myFixture.configureByFile("DatasetInDescribeBlockReference.php") val caretElement = file.findElementAt(myFixture.caretOffset) ?: return fail("No element") val datasetReference = caretElement.parent.references.filterIsInstance().firstOrNull() ?: return fail("No reference") val resolved = datasetReference.resolve() as? FunctionReferenceImpl ?: return fail("No function") assertTrue(resolved.isPestDataset()) assertEquals("some_numbers", resolved.getPestDatasetName()) } } ================================================ FILE: src/test/kotlin/com/pestphp/pest/features/datasets/DatasetUsagesTest.kt ================================================ package com.pestphp.pest.features.datasets import com.intellij.testFramework.TestDataPath import com.jetbrains.php.lang.psi.elements.MethodReference import com.pestphp.pest.PestLightCodeFixture import com.pestphp.pest.goto.PestDatasetUsagesGotoHandler import junit.framework.TestCase @TestDataPath("\$CONTENT_ROOT/../resources/com/pestphp/pest/goto/datasetUsages") class DatasetUsagesTest : PestLightCodeFixture() { override fun getBasePath(): String = "${super.getBasePath()}/goto/datasetUsages" fun testGotoUsages() { myFixture.copyFileToProject("DatasetUsage.php", "tests/usages.php") myFixture.copyFileToProject("DatasetDeclaration.php", "tests/Datasets/declaration.php") val file = myFixture.configureFromTempProjectFile("tests/Datasets/declaration.php") val element = file.findElementAt(myFixture.caretOffset) ?: return fail("No element") val usages = PestDatasetUsagesGotoHandler().getGotoDeclarationTargets(element, 0, null) ?: return fail("No usages") assertEquals(2, usages.size) TestCase.assertTrue(usages.any { it is MethodReference && it.containingFile?.name == "usages.php" }) TestCase.assertTrue(usages.any { it is MethodReference && it.containingFile?.name == "declaration.php" }) } } ================================================ FILE: src/test/kotlin/com/pestphp/pest/features/datasets/InvalidDatasetNameCaseInspectionTest.kt ================================================ package com.pestphp.pest.features.datasets import com.intellij.testFramework.TestDataPath import com.pestphp.pest.PestLightCodeFixture @TestDataPath("\$CONTENT_ROOT/../resources/com/pestphp/pest/features/datasets") class InvalidDatasetNameCaseInspectionTest : PestLightCodeFixture() { override fun getBasePath(): String = "${super.getBasePath()}/features/datasets" override fun setUp() { super.setUp() myFixture.enableInspections(InvalidDatasetNameCaseInspection::class.java) } fun testInvalidDatasetNameCase() { myFixture.configureByFile("InvalidDatasetNameCase.php") myFixture.checkHighlighting() myFixture.getAllQuickFixes().forEach { myFixture.launchAction(it) } myFixture.checkResultByFile("InvalidDatasetNameCase.after.php") } fun testValidDatasetNameCase() { myFixture.configureByFile("ValidDatasetNameCase.php") myFixture.checkHighlighting() } } ================================================ FILE: src/test/kotlin/com/pestphp/pest/features/datasets/InvalidDatasetReferenceInspectionTest.kt ================================================ package com.pestphp.pest.features.datasets import com.intellij.testFramework.TestDataPath import com.pestphp.pest.PestLightCodeFixture @TestDataPath("\$CONTENT_ROOT/../resources/com/pestphp/pest/features/datasets") class InvalidDatasetReferenceInspectionTest : PestLightCodeFixture() { override fun getBasePath(): String = "${super.getBasePath()}/features/datasets" override fun setUp() { super.setUp() myFixture.enableInspections(InvalidDatasetReferenceInspection::class.java) } fun testHasInvalidDatasetName() { myFixture.configureByFile("InvalidDatasetTest.php") myFixture.checkHighlighting() } fun testHasValidDataset() { myFixture.configureByFile("DatasetTest.php") myFixture.checkHighlighting() } fun testHasWithStatementWithNoArgs() { myFixture.configureByFile("DatasetNoArgsTest.php") myFixture.checkHighlighting() } fun testHasInvalidDatasetInDescribeBlock() { myFixture.configureByFile("InvalidDatasetInDescribeBlockTest.php") myFixture.checkHighlighting() } fun testDatasetInsideDescribeBlock() { myFixture.configureByFile("DatasetInsideDescribeBlockTest.php") myFixture.checkHighlighting() } } ================================================ FILE: src/test/kotlin/com/pestphp/pest/features/parallel/PestParallelProgramRunnerTest.kt ================================================ package com.pestphp.pest.features.parallel import com.intellij.execution.PsiLocation import com.intellij.execution.actions.ConfigurationContext import com.intellij.psi.PsiElement import com.intellij.testFramework.TestDataPath import com.jetbrains.php.config.interpreters.PhpInterpreter import com.jetbrains.php.config.interpreters.PhpInterpretersManagerImpl import com.jetbrains.php.testFramework.PhpTestFrameworkConfiguration import com.jetbrains.php.testFramework.PhpTestFrameworkSettingsManager import com.pestphp.pest.PestFrameworkType import com.pestphp.pest.PestLightCodeFixture import com.pestphp.pest.configuration.PestRunConfiguration import com.pestphp.pest.configuration.PestRunConfigurationProducer @TestDataPath("\$CONTENT_ROOT/../resources/com/pestphp/pest/features/parallel") class PestParallelProgramRunnerTest : PestLightCodeFixture() { private lateinit var configurationsBackup: List override fun getBasePath(): String = "${super.getBasePath()}/features/parallel" fun testCannotRunWrongExecutorId() = doTest { val configuration = createConfiguration(myFixture.file) assertFalse(PestParallelProgramRunner().canRun(PestParallelTestExecutor.EXECUTOR_ID + "1", configuration)) } fun testCanRunFile() = doTest { val configuration = createConfiguration(myFixture.file) assertTrue(PestParallelProgramRunner().canRun(PestParallelTestExecutor.EXECUTOR_ID, configuration)) } fun testCanRunFunction() = doTest { val testElement = myFixture.file?.firstChild?.lastChild?.firstChild ?: return@doTest val configuration = createConfiguration(testElement) assertTrue(PestParallelProgramRunner().canRun(PestParallelTestExecutor.EXECUTOR_ID, configuration)) } fun testCanRunDirectory() = doTest { val testElement = myFixture.file?.containingDirectory ?: return@doTest val configuration = createConfiguration(testElement) assertTrue(PestParallelProgramRunner().canRun(PestParallelTestExecutor.EXECUTOR_ID, configuration)) } fun testBuildFile() = doTest { val configuration = createConfiguration(myFixture.file) val pestParallelCommandSettings = createPestParallelCommand(configuration) val expected = "randomPath --teamcity --parallel --log-teamcity php://stdout /src/ATest.php" assertEquals(expected, pestParallelCommandSettings.createGeneralCommandLine().parametersList.list.joinToString(" ")) } fun testBuildFunction() = doTest { val testElement = myFixture.file?.firstChild?.lastChild?.firstChild ?: return@doTest val configuration = createConfiguration(testElement) val pestParallelCommandSettings = createPestParallelCommand(configuration) val expected = "randomPath --teamcity --parallel --log-teamcity php://stdout /src/ATest.php" assertEquals( expected, pestParallelCommandSettings.createGeneralCommandLine().parametersList.list .joinToString(" ") .substringBefore(" --filter") ) } fun testBuildDirectory() = doTest { val testElement = myFixture.file?.containingDirectory ?: return@doTest val configuration = createConfiguration(testElement) val pestParallelCommandSettings = createPestParallelCommand(configuration) val expected = "randomPath --teamcity --parallel --log-teamcity php://stdout /src" assertEquals(expected, pestParallelCommandSettings.createGeneralCommandLine().parametersList.list.joinToString(" ")) } private fun createConfiguration(psiElement: PsiElement): PestRunConfiguration { createPestFrameworkConfiguration() val context = ConfigurationContext.createEmptyContextForLocation(PsiLocation.fromPsiElement(psiElement)) val runConfiguration = PestRunConfigurationProducer().createConfigurationFromContext(context)?.configuration as? PestRunConfiguration runConfiguration!!.settings.commandLineSettings.interpreterSettings.interpreterName = getTestName(false) return runConfiguration } private fun doTest(block: () -> Unit) { myFixture.configureByFile("ATest.php") block() } override fun setUp() { super.setUp() val interpreter = PhpInterpreter().apply { name = getTestName(false) homePath = "$testDataPath/php" } PhpInterpretersManagerImpl.getInstance(project).addInterpreter(interpreter) configurationsBackup = PhpTestFrameworkSettingsManager.getInstance(project).getConfigurations(PestFrameworkType.instance) } override fun tearDown() { try { PhpTestFrameworkSettingsManager.getInstance(project).setConfigurations(PestFrameworkType.instance, configurationsBackup) PhpInterpretersManagerImpl.getInstance(project).interpreters = emptyList() } catch (e: Throwable) { addSuppressedException(e) } finally { super.tearDown() } } } ================================================ FILE: src/test/kotlin/com/pestphp/pest/features/parallel/PestParallelSMTEventsAdapterTest.kt ================================================ package com.pestphp.pest.features.parallel import com.intellij.execution.PsiLocation import com.intellij.execution.actions.ConfigurationContext import com.intellij.execution.process.ProcessOutputTypes import com.intellij.execution.testframework.sm.runner.GeneralToSMTRunnerEventsConvertor import com.intellij.execution.testframework.sm.runner.OutputToGeneralTestEventsConverter import com.intellij.execution.testframework.sm.runner.SMTRunnerEventsListener import com.intellij.execution.testframework.sm.runner.SMTestProxy.SMRootTestProxy import com.intellij.openapi.util.text.StringUtil import com.intellij.psi.PsiElement import com.intellij.testFramework.TestDataPath import com.pestphp.pest.PestLightCodeFixture import com.pestphp.pest.configuration.PestRunConfiguration import com.pestphp.pest.configuration.PestRunConfigurationProducer @TestDataPath("\$CONTENT_ROOT/../resources/com/pestphp/pest/features/parallel") class PestParallelSMTEventsAdapterTest : PestLightCodeFixture() { override fun getBasePath(): String = "${super.getBasePath()}/features/parallel" fun testSuitePresentableName() { val testsRoot = SMRootTestProxy() processTestOutput( testsRoot, """ ##teamcity[testCount count='1' flowId='6630'] ##teamcity[testSuiteStarted name='P\ATest' locationHint='php_qn:///Users/me/my_project/vendor/pestphp/pest/src/Factories/TestCaseFactory.php(169) : eval()|'d code::\P\ATest' flowId='6630'] ##teamcity[testStarted name='__pest_evaluable_foo' locationHint='php_qn:///Users/me/my_project/vendor/pestphp/pest/src/Factories/TestCaseFactory.php(169) : eval()|'d code::\P\ATest::__pest_evaluable_foo' flowId='6630'] ##teamcity[testFailed name='__pest_evaluable_foo' message='Failed asserting that 1 is identical to 2.' details='/Users/me/my_project/tests/kek/MyThirdTest.php:4|n' duration='80' flowId='6630'] ##teamcity[testFinished name='__pest_evaluable_foo' duration='82' flowId='6630'] ##teamcity[testSuiteFinished name='P\ATest' flowId='6630']""".trimIndent() ) val suite = testsRoot.children.first() assertEquals("P\\ATest", suite.name) assertEquals("ATest", suite.presentableName) } fun testTestPresentableName() { val testsRoot = SMRootTestProxy() processTestOutput( testsRoot, """ ##teamcity[testCount count='1' flowId='6630'] ##teamcity[testSuiteStarted name='P\ATest' locationHint='php_qn:///Users/me/my_project/vendor/pestphp/pest/src/Factories/TestCaseFactory.php(169) : eval()|'d code::\P\ATest' flowId='6630'] ##teamcity[testStarted name='__pest_evaluable_foo' locationHint='php_qn:///Users/me/my_project/vendor/pestphp/pest/src/Factories/TestCaseFactory.php(169) : eval()|'d code::\P\ATest::__pest_evaluable_foo' flowId='6630'] ##teamcity[testFailed name='__pest_evaluable_foo' message='Failed asserting that 1 is identical to 2.' details='/Users/me/my_project/tests/kek/MyThirdTest.php:4|n' duration='80' flowId='6630'] ##teamcity[testFinished name='__pest_evaluable_foo' duration='82' flowId='6630'] ##teamcity[testSuiteFinished name='P\ATest' flowId='6630']""".trimIndent() ) val test = testsRoot.children.first().children.first() assertEquals("__pest_evaluable_foo", test.name) assertEquals("foo", test.presentableName) } fun testArchTestPresentableName() { val testsRoot = SMRootTestProxy() processTestOutput( testsRoot, """ ##teamcity[testCount count='1' flowId='6630'] ##teamcity[testSuiteStarted name='P\ATest' locationHint='php_qn:///Users/me/my_project/vendor/pestphp/pest/src/Factories/TestCaseFactory.php(169) : eval()|'d code::\P\ATest' flowId='6630'] ##teamcity[testStarted name='__pest_evaluable_preset__→_php_' locationHint='php_qn:///Users/me/my_project/vendor/pestphp/pest/src/Factories/TestCaseFactory.php(169) : eval()|'d code::\P\ATest::__pest_evaluable_preset__→_php_' flowId='6630'] ##teamcity[testFailed name='__pest_evaluable_preset__→_php_' message='Failed asserting that 1 is identical to 2.' details='/Users/me/my_project/tests/kek/MyThirdTest.php:4|n' duration='80' flowId='6630'] ##teamcity[testFinished name='__pest_evaluable_preset__→_php_' duration='82' flowId='6630'] ##teamcity[testSuiteFinished name='P\ATest' flowId='6630']""".trimIndent() ) val test = testsRoot.children.first().children.first() assertEquals("__pest_evaluable_preset__→_php_", test.name) assertEquals("preset → php", test.presentableName) } private fun processTestOutput(testsRoot: SMRootTestProxy, output: String) { val file = myFixture.configureByFile("ATest.php") project.messageBus.connect(testRootDisposable).subscribe(SMTRunnerEventsListener.TEST_STATUS, PestParallelSMTEventsAdapter()) createPestFrameworkConfiguration() val context = ConfigurationContext.createEmptyContextForLocation(PsiLocation.fromPsiElement(file)) val configuration = PestRunConfigurationProducer().createConfigurationFromContext(context)?.configuration as? PestRunConfiguration val consoleProperties = configuration?.createTestConsoleProperties(PestParallelTestExecutor()) ?: return disposeOnTearDown(consoleProperties) val converter = OutputToGeneralTestEventsConverter("Pest", consoleProperties) converter.setProcessor(GeneralToSMTRunnerEventsConvertor(project, testsRoot, "Pest")) StringUtil.splitByLinesKeepSeparators(output).forEach { line -> converter.process(line, ProcessOutputTypes.STDOUT) } } } ================================================ FILE: src/test/kotlin/com/pestphp/pest/features/snapshotTesting/SnapshotLineMarkerProviderTest.kt ================================================ package com.pestphp.pest.features.snapshotTesting import com.pestphp.pest.PestLightCodeFixture class SnapshotLineMarkerProviderTest: PestLightCodeFixture() { override fun getBasePath(): String = "${super.getBasePath()}/snapshotTesting" fun testCanProvideIconForSnapshotAssertion() { val file = myFixture.configureByFile("allSnapshotAssertions.php") val identifiers = file.firstChild.children .map { it.firstChild.firstChild } assertNotEmpty(identifiers) assertSize(identifiers.count(), this.myFixture.findAllGutters()) } fun testDoNotShowIconForSnapshotUse() { myFixture.configureByFile("snapshotAssertionUseStatement.php") assertSize(0, this.myFixture.findAllGutters()) } } ================================================ FILE: src/test/kotlin/com/pestphp/pest/features/snapshotTesting/SnapshotUtilTest.kt ================================================ package com.pestphp.pest.features.snapshotTesting import com.jetbrains.php.lang.psi.elements.impl.FunctionImpl import com.jetbrains.php.lang.psi.elements.impl.FunctionReferenceImpl import com.pestphp.pest.PestLightCodeFixture import junit.framework.TestCase class SnapshotUtilTest : PestLightCodeFixture() { override fun getBasePath(): String = "${super.getBasePath()}/snapshotTesting" fun testIsSnapshotAssertion() { val file = myFixture.configureByFile("allSnapshotAssertions.php") val statements = file.firstChild.children .map { it.firstChild } assertNotEmpty(statements) statements.forEach { assertInstanceOf(it, FunctionReferenceImpl::class.java) } statements .filterIsInstance() .forEach { TestCase.assertTrue(it.isSnapshotAssertionCall) } } fun testIsNotSnapshotAssertion() { val file = myFixture.configureByFile("nonSnapshotAssertions.php") val functionReference = file.firstChild.children[0].firstChild as FunctionReferenceImpl assertFalse(functionReference.isSnapshotAssertionCall) } fun testCanFindSnapshotFiles() { myFixture.copyFileToProject( "tests/__snapshots__/snapshotTest__it_renders_correctly__1.txt", "tests/__snapshots__/snapshotTest__it_renders_correctly__1.txt" ) val file = myFixture.configureByFile("snapshotTest.php") val pestTest = file.firstChild.children[0].firstChild as FunctionReferenceImpl val pestTestBody = pestTest.parameters[1].firstChild as FunctionImpl val snapshotReference = pestTestBody.children[1].children[1].firstChild as FunctionReferenceImpl assertSize(1, snapshotReference.snapshotFiles) } } ================================================ FILE: src/test/kotlin/com/pestphp/pest/generateTest/PestNewTestFromClassActionTest.kt ================================================ package com.pestphp.pest.generateTest import com.intellij.openapi.project.Project import com.intellij.openapi.util.text.StringUtil import com.intellij.openapi.vfs.VirtualFile import com.intellij.openapi.vfs.ex.temp.TempFileSystem import com.intellij.psi.PsiFile import com.intellij.testFramework.PlatformTestUtil import com.intellij.testFramework.utils.vfs.getPsiFile import com.intellij.util.PathUtil import com.jetbrains.php.lang.psi.PhpFile import com.jetbrains.php.lang.psi.PhpPsiUtil import com.jetbrains.php.lang.psi.elements.Method import com.jetbrains.php.phpunit.codeGeneration.PhpNewTestDialog import com.jetbrains.php.roots.ui.PhpPsrDirectoryComboBox import com.jetbrains.php.templates.PhpCreateFileFromTemplateDataProvider import com.pestphp.pest.PestLightCodeFixture import com.pestphp.pest.PestNewTestFromClassAction import com.pestphp.pest.PestTestCreateInfo class PestNewTestFromClassActionTest : PestLightCodeFixture() { override fun getBasePath(): String = "${super.getBasePath()}/generateTest" private fun createTestFile(file: PsiFile, namespace: String): PsiFile { val dialog = getDialog(file, namespace) try { val testFile = MockPestNewTestFromClassAction.publicCreateFile(project, dialog) assertNotNull(testFile) return testFile!! } finally { dialog.disposeIfNeeded() } } private object MockPestNewTestFromClassAction : PestNewTestFromClassAction() { fun publicCreateFile(project: Project, dataProvider: PhpCreateFileFromTemplateDataProvider): PsiFile? { return super.createFile(project, dataProvider) } } private fun getDialog(file: PsiFile, namespace: String): PhpNewTestDialog { val phpClass = PhpPsiUtil.findClass(file as PhpFile) { _ -> true } return object : PhpNewTestDialog(project, file.containingDirectory, file, PestTestCreateInfo, phpClass) { override fun createDirectoryCombobox(): PhpPsrDirectoryComboBox { return object : PhpPsrDirectoryComboBox(project, testDirectoryProvider) { override fun init(baseDir: VirtualFile, namespace: String) { super.init(baseDir, namespace) updateSuggestions(getNamespace()) } override fun getExistingParent(): VirtualFile { return findExistingParent(selectedPath) ?: PlatformTestUtil.getOrCreateProjectBaseDir(project) } private fun findExistingParent(path: String): VirtualFile? { if (StringUtil.isEmpty(path)) { return null } val directory = TempFileSystem.getInstance().findFileByPath(path) return directory ?: findExistingParent(PathUtil.getParentPath(path)) } override fun getBaseDirectory(): VirtualFile { return myDirectoryCombobox.existingParent ?: super.getBaseDirectory() } } } override fun getNamespace(): String { return namespace } override fun getSelectedClassMembers(): MutableSet { return phpClass?.methods?.toMutableSet() ?: mutableSetOf() } } } fun testPestWithNamespace() { val fileWithClass = myFixture.copyFileToProject("testWithNamespace.php") val testFile = createTestFile(fileWithClass.getPsiFile(project), "a") myFixture.configureByText(testFile.getName(), testFile.getText()) myFixture.checkResultByFile("testWithNamespace.after.php") } } ================================================ FILE: src/test/kotlin/com/pestphp/pest/goto/PestTestFinderTest.kt ================================================ package com.pestphp.pest.goto import com.intellij.testFramework.UsefulTestCase import com.jetbrains.php.lang.psi.PhpFile import com.jetbrains.php.lang.psi.PhpPsiUtil import com.pestphp.pest.PestLightCodeFixture import com.pestphp.pest.getPestTestName import junit.framework.TestCase class PestTestFinderTest : PestLightCodeFixture() { override fun getBasePath(): String = "${super.getBasePath()}/goto/PestTestFinder" fun testPestTestIsTest() { val file = myFixture.configureByFile("test/App/UserTest.php") val testElement = file.firstChild.lastChild.firstChild assertTrue(PestTestFinder().isTest(testElement)) } fun testFileIsTest() { val file = myFixture.configureByFile("test/App/UserTest.php") assertTrue(PestTestFinder().isTest(file)) } fun testRandomElementIsTest() { val file = myFixture.configureByFile("test/App/UserTest.php") assertTrue(PestTestFinder().isTest(file.firstChild.children.random())) } fun testClassIsNotTest() { val file = myFixture.configureByFile("test/App/MockTest.php") myFixture.testDataPath assertFalse(PestTestFinder().isTest(file.firstChild.lastChild.firstChild)) } fun testCanFindSourceElement() { val file = myFixture.configureByFile("App/User.php") TestCase.assertSame( file, PestTestFinder().findSourceElement( PhpPsiUtil.findAllClasses(file as PhpFile).first().methods.first() ) ) } fun testFindTestFileForClass() { val file = myFixture.configureByFile("App/User.php") val testFile = myFixture.configureByFile("test/App/UserTest.php") TestCase.assertSame( testFile, PestTestFinder().findTestsForClass(PhpPsiUtil.findAllClasses(file as PhpFile).first()).first(), ) } fun testFindClassForTestFile() { val file = myFixture.configureByFile("App/User.php") val testFile = myFixture.configureByFile("test/App/UserTest.php") TestCase.assertSame( PhpPsiUtil.findAllClasses(file as PhpFile).first(), PestTestFinder().findClassesForTest(testFile.firstChild).first() ) } fun testFindTestsForMethod() { val file = myFixture.configureByFile("App/User.php") val testFile = myFixture.configureByFile("test/App/UserTest.php") val method = PhpPsiUtil.findAllClasses(file as PhpFile).first().findMethodByName("isPestDeveloper") val tests = testFile.firstChild.children.map { it.firstChild }.filter { it.getPestTestName()?.contains("is pest developer") == true } UsefulTestCase.assertSameElements( PestTestFinder().findTestsForClass(method!!), tests ) } fun testFindMethodsForTest() { val file = myFixture.configureByFile("App/User.php") val testFile = myFixture.configureByFile("test/App/UserTest.php") val test = testFile.firstChild.children.map { it.firstChild }.first { it.getPestTestName() == "is pest developer" } val methods = PhpPsiUtil.findAllClasses(file as PhpFile).first().methods.filter { it.name.contains("is") } UsefulTestCase.assertSameElements( PestTestFinder().findClassesForTest(test), methods ) } fun testFindTestsForMethodInDescribeBlock() { val file = myFixture.configureByFile("App/User.php") myFixture.configureByFile("test/App/UserDescribeTest.php") val method = PhpPsiUtil.findAllClasses(file as PhpFile).first().findMethodByName("isPestDeveloper") val tests = PestTestFinder().findTestsForClass(method!!) assertTrue(tests.any { it.getPestTestName()?.contains("is pest developer in describe") == true }) } } ================================================ FILE: src/test/kotlin/com/pestphp/pest/higherOrderExpectations/HigherOrderExpectationAssertionCompletionTest.kt ================================================ package com.pestphp.pest.higherOrderExpectations import com.pestphp.pest.PestLightCodeFixture class HigherOrderExpectationAssertionCompletionTest: PestLightCodeFixture() { override fun getBasePath(): String = "${super.getBasePath()}/higherOrderExpectations" override fun setUp() { super.setUp() myFixture.copyFileToProject("stubs.php") } fun testFieldCompletion() { myFixture.configureByFile( "ExpectPropertyAssertionCompletion.php" ) assertCompletion("toBeTrue", "toEqual") } fun testMethodCompletion() { myFixture.configureByFile( "ExpectMethodAssertionCompletion.php" ) assertCompletion("toBeTrue", "toEqual") } fun testMethodCompletionChained() { myFixture.configureByFile( "ExpectMethodAssertionCompletionChained.php" ) assertCompletion("toBeInt") } fun testFieldCompletionChained() { myFixture.configureByFile( "ExpectPropertyAssertionCompletionChained.php" ) assertCompletion("toBeInt") } fun testMethodCompletionChainedAssertions() { myFixture.configureByFile( "ExpectMethodAssertionCompletionChainedAssertions.php" ) assertCompletion("toBeInt") } fun testFieldCompletionChainedAssertions() { myFixture.configureByFile( "ExpectPropertyAssertionCompletionChainedAssertions.php" ) assertCompletion("toBeInt") } } ================================================ FILE: src/test/kotlin/com/pestphp/pest/higherOrderExpectations/HigherOrderExpectationCompletionTest.kt ================================================ package com.pestphp.pest.higherOrderExpectations import com.pestphp.pest.PestLightCodeFixture class HigherOrderExpectationCompletionTest: PestLightCodeFixture() { override fun getBasePath(): String = "${super.getBasePath()}/higherOrderExpectations" override fun setUp() { super.setUp() myFixture.copyFileToProject(".phpstorm.meta.php") } fun testFieldCompletion() { myFixture.configureByFile( "ExpectPropertyCompletion.php" ) assertCompletion("otherExample", "test") } fun testMethodCompletion() { myFixture.configureByFile( "ExpectMethodCompletion.php" ) assertCompletion("getOtherExample", "getTest") } fun testMethodCompletionChained() { myFixture.configureByFile( "ExpectMethodCompletionChained.php" ) assertCompletion("getTimestamp"); } fun testFieldCompletionChained() { myFixture.configureByFile( "ExpectPropertyCompletionChained.php" ) assertCompletion("getTimestamp") } fun testChainedAssertionsFieldCompletion() { myFixture.configureByFile( "ExpectPropertyCompletionChainedAssertions.php" ) assertCompletion("otherExample", "test") } fun testChainedAssertionsMethodCompletion() { myFixture.configureByFile( "ExpectMethodCompletionChainedAssertions.php" ) assertCompletion("getOtherExample", "getTest") } } ================================================ FILE: src/test/kotlin/com/pestphp/pest/indexers/PestTestIndexTest.kt ================================================ package com.pestphp.pest.indexers import com.intellij.psi.search.GlobalSearchScope import com.intellij.testFramework.TestDataPath import com.intellij.util.indexing.FileBasedIndex import com.pestphp.pest.PestLightCodeFixture @TestDataPath("\$CONTENT_ROOT/../resources/com/pestphp/pest/indexers/PestTestIndexTest") class PestTestIndexTest : PestLightCodeFixture() { override fun getBasePath(): String = "${super.getBasePath()}/indexers/PestTestIndexTest" fun testPestTestFileIsIndexed() { val virtualFile = myFixture.copyFileToProject("FileWithPestTest.php", "tests/FileWithPestTest.php") myFixture.configureFromExistingVirtualFile(virtualFile) val fileBasedIndex = FileBasedIndex.getInstance() val indexKeys = fileBasedIndex.getAllKeys(key, project).filter { fileBasedIndex.getContainingFiles(key, it, GlobalSearchScope.allScope(project)).isNotEmpty() } assertContainsElements(indexKeys, "/src/tests/FileWithPestTest.php") } fun testPhpFileIsNotIndexed() { myFixture.copyFileToProject("FileWithoutPestTest.php", "tests/FileWithoutPestTest.php") val fileBasedIndex = FileBasedIndex.getInstance() val indexKeys = fileBasedIndex.getAllKeys(key, project).filter { fileBasedIndex.getContainingFiles(key, it, GlobalSearchScope.allScope(project)).isNotEmpty() } assertDoesntContain(indexKeys, "/src/tests/FileWithoutPestTest.php") } fun testPestTestFileWithTodoIsIndexed() { val virtualFile = myFixture.copyFileToProject("FileWithPestTodosTest.php", "tests/FileWithPestTodosTest.php") myFixture.configureFromExistingVirtualFile(virtualFile) val fileBasedIndex = FileBasedIndex.getInstance() val indexKeys = fileBasedIndex.getAllKeys(key, project).filter { fileBasedIndex.getContainingFiles(key, it, GlobalSearchScope.allScope(project)).isNotEmpty() } assertContainsElements(indexKeys, "/src/tests/FileWithPestTodosTest.php") } fun testPestTestFileOutsideTestDirectoryIsIndexed() { val virtualFile = myFixture.copyFileToProject("FileWithPestTest.php", "anywhere-but-not-te-st/FileWithPestTest.php") myFixture.configureFromExistingVirtualFile(virtualFile) val fileBasedIndex = FileBasedIndex.getInstance() val indexKeys = fileBasedIndex.getAllKeys(key, project).filter { fileBasedIndex.getContainingFiles(key, it, GlobalSearchScope.allScope(project)).isNotEmpty() } assertContainsElements(indexKeys, "/src/anywhere-but-not-te-st/FileWithPestTest.php") } fun testDescribeBlockTestsAreIndexed() { val virtualFile = myFixture.copyFileToProject("FileWithDescribeBlockTest.php", "tests/FileWithDescribeBlockTest.php") myFixture.configureFromExistingVirtualFile(virtualFile) val fileBasedIndex = FileBasedIndex.getInstance() val testNames = fileBasedIndex.getValues(key, "/src/tests/FileWithDescribeBlockTest.php", GlobalSearchScope.allScope(project)) .flatten() assertContainsElements(testNames, "`sum` → ", "`sum` → adds numbers", "`sum` → it handles zero") } } ================================================ FILE: src/test/kotlin/com/pestphp/pest/inspections/InvalidTestNameCaseInspectionTest.kt ================================================ package com.pestphp.pest.inspections import com.intellij.testFramework.TestDataPath import com.pestphp.pest.PestLightCodeFixture @TestDataPath("\$CONTENT_ROOT/../resources/com/pestphp/pest/inspections") class InvalidTestNameCaseInspectionTest : PestLightCodeFixture() { override fun getBasePath(): String = "${super.getBasePath()}/inspections" override fun setUp() { super.setUp() myFixture.enableInspections(InvalidTestNameCaseInspection::class.java) } fun testInvalidTestNameCase() { myFixture.configureByFile("InvalidTestNameCase.php") myFixture.checkHighlighting() } fun testValidTestNameCase() { myFixture.configureByFile("ValidTestNameCase.php") myFixture.checkHighlighting() } fun testReplacesInvalidTestNameCase() { myFixture.configureByFile("InvalidTestNameCase.php") myFixture.checkHighlighting() myFixture.getAllQuickFixes().forEach { myFixture.launchAction(it) } myFixture.checkResultByFile("InvalidTestNameCase.after.php") } fun testValidTestNameWhenWrongCasingOnSomeWords() { myFixture.configureByFile("ValidTestNameWhenWrongCasingOnOneWord.php") myFixture.checkHighlighting() } fun testValidTestNameCaseInHigherOrderTest() { myFixture.configureByFile("ValidHigherOrderTestNameCase.php") myFixture.checkHighlighting() } fun testValidItTestNameWithoutSpaces() { myFixture.configureByFile("ValidItTestNameWithoutSpaces.php") myFixture.checkHighlighting() } fun testInvalidTestNameAndDatasetName() { myFixture.configureByFile("InvalidTestNameAndDatasetName.php") myFixture.checkHighlighting() myFixture.getAllQuickFixes().forEach { myFixture.launchAction(it) } myFixture.checkResultByFile("InvalidTestNameAndDatasetName.after.php") } } ================================================ FILE: src/test/kotlin/com/pestphp/pest/inspections/MissingScreenshotSnapshotInspectionTest.kt ================================================ package com.pestphp.pest.inspections import com.intellij.testFramework.TestDataPath import com.pestphp.pest.PestLightCodeFixture @TestDataPath("\$CONTENT_ROOT/../resources/com/pestphp/pest/inspections") class MissingScreenshotSnapshotInspectionTest : PestLightCodeFixture() { override fun getBasePath(): String = "${super.getBasePath()}/inspections" override fun setUp() { super.setUp() myFixture.enableInspections(MissingScreenshotSnapshotInspection::class.java) } fun testSimple() { myFixture.copyDirectoryToProject("screenshotProject", ".") myFixture.configureFromTempProjectFile("tests/Feature/MissingScreenshotSnapshot.php") myFixture.checkHighlighting() } fun testSimpleExists() { myFixture.copyDirectoryToProject("screenshotProject", ".") myFixture.configureFromTempProjectFile("tests/Feature/ScreenshotSnapshot.php") myFixture.checkHighlighting() } fun testMultiple() { myFixture.copyDirectoryToProject("screenshotProject", ".") myFixture.configureFromTempProjectFile("tests/Feature/MissingScreenshotSnapshotMultiple.php") myFixture.checkHighlighting() } fun testMultipleExists() { myFixture.copyDirectoryToProject("screenshotProject", ".") myFixture.configureFromTempProjectFile("tests/Feature/ScreenshotSnapshotMultiple.php") myFixture.checkHighlighting() } fun testComplexName() { myFixture.copyDirectoryToProject("screenshotProject", ".") myFixture.configureFromTempProjectFile("tests/Feature/MissingScreenshotSnapshotComplexName.php") myFixture.checkHighlighting() } fun testComplexNameExists() { myFixture.copyDirectoryToProject("screenshotProject", ".") myFixture.configureFromTempProjectFile("tests/Feature/ScreenshotSnapshotComplexName.php") myFixture.checkHighlighting() } fun testNestedDir() { myFixture.copyDirectoryToProject("screenshotProject", ".") myFixture.configureFromTempProjectFile("tests/Feature/nested/MissingScreenshotSnapshotNested.php") myFixture.checkHighlighting() } fun testNestedDirExists() { myFixture.copyDirectoryToProject("screenshotProject", ".") myFixture.configureFromTempProjectFile("tests/Feature/nested/ScreenshotSnapshotNested.php") myFixture.checkHighlighting() } } ================================================ FILE: src/test/kotlin/com/pestphp/pest/inspections/MultipleExpectChainableInspectionTest.kt ================================================ package com.pestphp.pest.inspections import com.intellij.testFramework.TestDataPath import com.pestphp.pest.PestLightCodeFixture @TestDataPath("\$CONTENT_ROOT/../resources/com/pestphp/pest/inspections") class MultipleExpectChainableInspectionTest : PestLightCodeFixture() { override fun getBasePath(): String = "${super.getBasePath()}/inspections" override fun setUp() { super.setUp() myFixture.enableInspections(MultipleExpectChainableInspection::class.java) } fun testHasMultipleExpectCall() { myFixture.configureByFile("MultipleExpectCall.php") myFixture.checkHighlighting() } fun testSingleExpectCall() { myFixture.configureByFile("SingleExpectCall.php") myFixture.checkHighlighting() } fun testReplacesMultipleExpectCallToChain() { myFixture.configureByFile("MultipleExpectCall.php") myFixture.checkHighlighting() myFixture.getAllQuickFixes().first().run { myFixture.launchAction(this) } myFixture.checkResultByFile("MultipleExpectCall.after.php") } fun testHasExpectCallsWithOtherStatementsBetween() { myFixture.configureByFile("ExpectCallsWithOtherStatementsBetween.php") myFixture.checkHighlighting() } fun testHasMultipleExpectCallsWithOtherStatementsBetween() { myFixture.configureByFile("MultipleExpectCallsWithOtherStatementsBetween.php") myFixture.checkHighlighting() } fun testReplaceMultipleExpectCalls() { myFixture.configureByFile("MultipleExpectCallsWithOtherStatementsBetween.php") myFixture.checkHighlighting() myFixture.getAllQuickFixes().first().run { myFixture.launchAction(this) } myFixture.getAllQuickFixes().last().run { myFixture.launchAction(this) } myFixture.checkResultByFile("MultipleExpectCallsWithOtherStatementsBetween.after.php") } fun testReplaceCanBeCalledOnFirstStatement() { myFixture.configureByFile("MultipleExpectCall.php") myFixture.checkHighlighting() myFixture.getAllQuickFixes().first().run { myFixture.launchAction(this) } myFixture.checkResultByFile("MultipleExpectCall.after.php") } fun testReplaceCanBeCalledOnLastStatement() { myFixture.configureByFile("MultipleExpectCall.php") myFixture.checkHighlighting() myFixture.getAllQuickFixes().last().run { myFixture.launchAction(this) } myFixture.checkResultByFile("MultipleExpectCall.after.php") } fun testReplaceCanBeCalledOnSecondaryStatement() { myFixture.configureByFile("ManyExpectCall.php") myFixture.checkHighlighting() myFixture.getAllQuickFixes()[3].run { myFixture.launchAction(this) } myFixture.checkResultByFile("ManyExpectCall.after.php") } } ================================================ FILE: src/test/kotlin/com/pestphp/pest/inspections/PestAssertionCanBeSimplifiedInspectionTest.kt ================================================ package com.pestphp.pest.inspections import com.pestphp.pest.PestLightCodeFixture class PestAssertionCanBeSimplifiedInspectionTest: PestLightCodeFixture() { override fun getBasePath(): String = "${super.getBasePath()}/inspections/assertionCanBeSimplified" override fun setUp() { super.setUp() myFixture.enableInspections(PestAssertionCanBeSimplifiedInspection::class.java) } fun testToBeWithTrue() { myFixture.configureByFile("ToBeWithTrue.php") myFixture.checkHighlighting() myFixture.getAllQuickFixes().forEach { myFixture.launchAction(it) } myFixture.checkResultByFile("ToBeWithTrue.after.php") } fun testToBeWithFalse() { myFixture.configureByFile("ToBeWithFalse.php") myFixture.checkHighlighting() myFixture.getAllQuickFixes().forEach { myFixture.launchAction(it) } myFixture.checkResultByFile("ToBeWithFalse.after.php") } fun testToBeWithNull() { myFixture.configureByFile("ToBeWithNull.php") myFixture.checkHighlighting() myFixture.getAllQuickFixes().forEach { myFixture.launchAction(it) } myFixture.checkResultByFile("ToBeWithNull.after.php") } fun testToHaveCountWithZero() { myFixture.configureByFile("ToHaveCountWithZero.php") myFixture.checkHighlighting() myFixture.getAllQuickFixes().forEach { myFixture.launchAction(it) } myFixture.checkResultByFile("ToHaveCountWithZero.after.php") } } ================================================ FILE: src/test/kotlin/com/pestphp/pest/inspections/PestTestFailedLineInspectionTest.kt ================================================ package com.pestphp.pest.inspections import com.intellij.execution.TestStateStorage import com.intellij.lang.annotation.HighlightSeverity import com.intellij.testFramework.TestDataPath import com.pestphp.pest.PestLightCodeFixture import java.util.Date @TestDataPath("\$CONTENT_ROOT/../resources/com/pestphp/pest/inspections/pestTestFailedLine") class PestTestFailedLineInspectionTest : PestLightCodeFixture() { override fun getBasePath(): String = "${super.getBasePath()}/inspections/pestTestFailedLine" override fun setUp() { super.setUp() myFixture.enableInspections(PestTestFailedLineInspection::class.java) } private fun writeTestState(record: TestStateStorage.Record) { TestStateStorage.getInstance(project).writeState("pest_qn://src/${getFileNameBeforeRelativePath()}::myTest", record) } private fun writeTestStateWithDataset(record: TestStateStorage.Record, dataset: String) { TestStateStorage.getInstance(project).writeState("pest_qn://src/${getFileNameBeforeRelativePath()}::myTest with data set $dataset", record) } private fun doTest(record: TestStateStorage.Record) { writeTestState(record) configureByFile() myFixture.checkHighlighting() } fun testFailedOneLine() { val record = TestStateStorage.Record( 6, Date(), 0, 5, " expect(1)->toBe(2);", "Failed asserting that 1 is identical to 2.", null ) doTest(record) } fun testMismatchLine() { val record = TestStateStorage.Record( 6, Date(), 0, 5, " expect(1)->toBe(3);", "Failed asserting that 1 is identical to 2.", null ) doTest(record) } fun testOutRange() { val record = TestStateStorage.Record( 6, Date(), 0, 12, " expect(1)->toBe(2);", "Failed asserting that 1 is identical to 2.", null ) doTest(record) } fun testTypeBefore() { val record = TestStateStorage.Record( 6, Date(), 0, 5, " expect(1)->toBe(2);", "Failed asserting that 1 is identical to 2.", null ) doTest(record) myFixture.type("\n echo 1;") val highlightInfos = myFixture.doHighlighting(HighlightSeverity.WARNING) assertEquals(1, highlightInfos.size) assertEquals(76, highlightInfos[0].startOffset) assertEquals(95, highlightInfos[0].endOffset) } fun testTypeInside() { val record = TestStateStorage.Record( 6, Date(), 0, 5, " expect(1)->toBe(2);", "Failed asserting that 1 is identical to 2.", null ) doTest(record) myFixture.type("Not") val highlightInfos = myFixture.doHighlighting(HighlightSeverity.WARNING) assertEquals(1, highlightInfos.size) assertEquals(63, highlightInfos[0].startOffset) assertEquals(85, highlightInfos[0].endOffset) } fun testAnonymousFunction() { val record = TestStateStorage.Record( 6, Date(), 0, 7, " });", "Failed asserting that 1 is identical to 2.", null ) doTest(record) } fun testLambdaFunction() { val record = TestStateStorage.Record( 6, Date(), 0, 6, " );", "Failed asserting that 1 is identical to 2.", null ) doTest(record) } fun testSingleLeafElementReported() { val record = TestStateStorage.Record( 6, Date(), 0, 8, " );", "Failed asserting that 1 is identical to 2.", null ) doTest(record) } fun testMultipleStatementsInOneLine() { val record = TestStateStorage.Record( 6, Date(), 0, 4, " expect(1)->toBe(1); expect(1)->toBe(2);", "Failed asserting that 1 is identical to 2.", null ) doTest(record) } fun testWithDataSetAndKeys() { val secondDatasetRecord = TestStateStorage.Record( 6, Date(), 0, 4, " expect(1)->toBe(\$a);", "Failed asserting that 1 is identical to 2.", null ) writeTestStateWithDataset(secondDatasetRecord, "\"dataset \"\"second dataset\"") val thirdDatasetRecord = TestStateStorage.Record( 6, Date(), 0, 4, " expect(1)->toBe(\$a);", "Failed asserting that 1 is identical to 3.", null ) writeTestStateWithDataset(thirdDatasetRecord, "\"dataset \"\"third dataset\"") val methodRecord = TestStateStorage.Record(6, Date(), 0, -1, "", "", null) doTest(methodRecord) } fun testWithDataSet() { val secondDatasetRecord = TestStateStorage.Record( 6, Date(), 0, 4, " expect(1)->toBe(\$a);", "Failed asserting that 1 is identical to 2.", null ) writeTestStateWithDataset(secondDatasetRecord, "(2)") val thirdDatasetRecord = TestStateStorage.Record( 6, Date(), 0, 4, " expect(1)->toBe(\$a);", "Failed asserting that 1 is identical to 3.", null ) writeTestStateWithDataset(thirdDatasetRecord, "(3)") val methodRecord = TestStateStorage.Record(6, Date(), 0, -1, "", "", null) doTest(methodRecord) } fun testWithNamedDataSet() { val secondDatasetRecord = TestStateStorage.Record( 6, Date(), 0, 4, " expect(1)->toBe(\$a);", "Failed asserting that 1 is identical to 2.", null ) writeTestStateWithDataset(secondDatasetRecord, "second dataset") val thirdDatasetRecord = TestStateStorage.Record( 6, Date(), 0, 4, " expect(1)->toBe(\$a);", "Failed asserting that 1 is identical to 3.", null ) writeTestStateWithDataset(thirdDatasetRecord, "third dataset") val methodRecord = TestStateStorage.Record(6, Date(), 0, -1, "", "", null) doTest(methodRecord) } fun testWithDataSetAndSeveralFails() { val record = TestStateStorage.Record( 6, Date(), 0, 4, " expect(1)->toBe(\$a);", "Failed asserting that 1 is identical to 2.", null ) writeTestStateWithDataset(record, "\"dataset \"\"second dataset\"") val record2 = TestStateStorage.Record( 6, Date(), 0, 5, " expect(2)->toBe(\$a);", "Failed asserting that 2 is identical to 1.", null ) writeTestStateWithDataset(record2, "\"dataset \"\"first dataset\"") val methodRecord = TestStateStorage.Record(6, Date(), 0, -1, "", "", null) doTest(methodRecord) } } ================================================ FILE: src/test/kotlin/com/pestphp/pest/inspections/PhpStormInspectionsTest.kt ================================================ package com.pestphp.pest.inspections import com.intellij.testFramework.PsiTestUtil import com.intellij.testFramework.TestDataPath import com.jetbrains.php.lang.inspections.PhpInspection import com.jetbrains.php.lang.inspections.psr0.PhpMultipleClassesDeclarationsInOneFile import com.pestphp.pest.PestLightCodeFixture import org.jetbrains.jps.model.java.JavaSourceRootType @TestDataPath("\$CONTENT_ROOT/../resources/com/pestphp/pest/inspections/phpstorm") class PhpStormInspectionsTest: PestLightCodeFixture() { override fun getBasePath(): String = "${super.getBasePath()}/inspections/phpstorm" private fun doTest(inspectionClass: Class, testFilePath: String) { myFixture.enableInspections(inspectionClass) myFixture.configureByFile(testFilePath) myFixture.checkHighlighting() } fun testMultipleClassesDeclarationsInPestFile() { val testSrcDir = myFixture.tempDirFixture.findOrCreateDir("tests") PsiTestUtil.addSourceRoot(module, testSrcDir, JavaSourceRootType.TEST_SOURCE) myFixture.copyFileToProject("MultipleClassesDeclarationsInPestFileTest.php", "${testSrcDir.name}/MultipleClassesDeclarationsInPestFileTest.php") doTest(PhpMultipleClassesDeclarationsInOneFile::class.java, "${testSrcDir.name}/MultipleClassesDeclarationsInPestFileTest.php") } } ================================================ FILE: src/test/kotlin/com/pestphp/pest/runner/PestPressToContinueActionTest.kt ================================================ package com.pestphp.pest.runner import com.intellij.execution.configurations.RunConfiguration import com.intellij.execution.executors.DefaultRunExecutor import com.intellij.execution.impl.ConsoleViewImpl import com.intellij.execution.process.NopProcessHandler import com.intellij.execution.testframework.sm.runner.ui.SMTRunnerConsoleView import io.mockk.every import io.mockk.mockk import com.intellij.execution.ui.ConsoleViewContentType import com.intellij.execution.ui.RunContentDescriptor import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.DataContext import com.intellij.openapi.actionSystem.LangDataKeys import com.intellij.openapi.util.Disposer import com.intellij.testFramework.TestActionEvent.createTestEvent import com.intellij.util.ui.UIUtil import com.jetbrains.php.util.pathmapper.PhpPathMapper import com.pestphp.pest.PestLightCodeFixture import com.pestphp.pest.configuration.PestLocationProvider import com.pestphp.pest.configuration.PestRunConfigurationType import java.io.ByteArrayOutputStream import java.io.OutputStream class PestPressToContinueActionTest : PestLightCodeFixture() { private class CapturingProcessHandler : NopProcessHandler() { val input = ByteArrayOutputStream() override fun getProcessInput(): OutputStream = input } private data class TestContext( val consoleView: SMTRunnerConsoleView, val processHandler: CapturingProcessHandler, val action: PestPressToContinueAction, val event: AnActionEvent, ) private fun setupWithPrinted(output: String): TestContext { val config = mockk() every { config.project } returns project every { config.name } returns "Test" val executor = DefaultRunExecutor.getRunExecutorInstance() val locator = PestLocationProvider(PhpPathMapper.create(project), project, myFixture.testDataPath) val props = PestConsoleProperties(config, executor, locator) val consoleView = SMTRunnerConsoleView(props) consoleView.initUI() Disposer.register(testRootDisposable, consoleView) val processHandler = CapturingProcessHandler() processHandler.startNotify() consoleView.attachToProcess(processHandler) val descriptor = RunContentDescriptor(consoleView, processHandler, consoleView.component, "Pest") descriptor.runConfigurationTypeId = PestRunConfigurationType.instance.id Disposer.register(testRootDisposable, descriptor) val innerConsole = (consoleView.console as ConsoleViewImpl) innerConsole.print(output, ConsoleViewContentType.NORMAL_OUTPUT) innerConsole.flushDeferredText() UIUtil.dispatchAllInvocationEvents() val action = PestPressToContinueAction() val dataContext = DataContext { dataId -> if (LangDataKeys.RUN_CONTENT_DESCRIPTOR.name == dataId) descriptor else null } val event = createTestEvent(action, dataContext) action.update(event) return TestContext(consoleView, processHandler, action, event) } fun testWritesNewlineWhenPromptPresent() { val ctx = setupWithPrinted("Press any key to continue...") assertTrue(ctx.event.presentation.isVisible) assertTrue(ctx.event.presentation.isEnabled) ctx.action.actionPerformed(ctx.event) assertEquals("\n", ctx.processHandler.input.toString(Charsets.UTF_8)) } fun testDisabledWhenPromptAbsent() { val ctx = setupWithPrinted("no prompt here") assertTrue(ctx.event.presentation.isVisible) assertFalse(ctx.event.presentation.isEnabled) } } ================================================ FILE: src/test/kotlin/com/pestphp/pest/runner/PestTestStackTraceParserTest.kt ================================================ package com.pestphp.pest.runner import com.intellij.testFramework.TestDataPath import com.intellij.util.PathMappingSettings import com.jetbrains.php.util.pathmapper.PhpPathMapper import com.jetbrains.php.util.pathmapper.PhpRemotePathMapper import com.pestphp.pest.PestLightCodeFixture import com.pestphp.pest.configuration.PestLocationProvider @TestDataPath("\$CONTENT_ROOT/../resources/com/pestphp/pest/runner/pestTestStacktraceParser") class PestTestStackTraceParserTest : PestLightCodeFixture() { override fun getBasePath(): String = "${super.getBasePath()}/runner/pestTestStacktraceParser" private fun createStackTraceParser(url: String, stacktrace: String?, message: String?): PestTestStackTraceParser { val locator = PestLocationProvider(PhpPathMapper.create(project), project, myFixture.testDataPath) return parse(url, stacktrace, message, locator, project) } private fun createRemoteStackTraceParser(url: String, stacktrace: String?, message: String?): PestTestStackTraceParser { val locator = PestLocationProvider(PhpRemotePathMapper.create(listOf(PathMappingSettings.PathMapping(getFileBeforeFullPath(), "/opt/project"))), project, myFixture.testDataPath) return parse(url, stacktrace, message, locator, project) } fun testNull() { val stackTraceParser = createStackTraceParser("pest_qn://path/to/test.php::myTest", null, null) assertNull(stackTraceParser.errorMessage) assertEquals(-1, stackTraceParser.failedLine) assertNull(stackTraceParser.topLocationLine) assertNull(stackTraceParser.failedMethodName) } fun testEmptyLine() { val stackTraceParser = createStackTraceParser("pest_qn://path/to/test.php::myTest", "", "error") assertEquals("error", stackTraceParser.errorMessage) assertEquals(-1, stackTraceParser.failedLine) assertNull(stackTraceParser.topLocationLine) assertNull(stackTraceParser.failedMethodName) } fun testWithoutTestMention() { val stackTraceParser = createStackTraceParser("pest_qn://path/to/test.php::myTest", "at /path/to/test2.php:14\nat /path/to/test3.php:11", "error") assertEquals("error", stackTraceParser.errorMessage) assertEquals(-1, stackTraceParser.failedLine) assertNull(stackTraceParser.topLocationLine) assertNull(stackTraceParser.failedMethodName) } fun testOneLine() { configureByFile() val stackTraceParser = createStackTraceParser("pest_qn://" + getFileBeforeFullPath() + "::myTest", getFileBeforeFullPath() + ":4", "error") assertEquals("error", stackTraceParser.errorMessage) assertEquals(4, stackTraceParser.failedLine) assertNull(stackTraceParser.topLocationLine) assertEquals(" expect(1)->toBe(2);", stackTraceParser.failedMethodName) } fun testMultiline() { configureByFile() val stackTraceParser = createStackTraceParser( "pest_qn://" + getFileBeforeFullPath() + "::\\MyTest::test", getFileBeforeFullPath() + ":12\n " + getFileBeforeFullPath() + ":8\n" + getFileBeforeFullPath() + ":4", null ) assertEquals(getFileBeforeFullPath() + ":12", stackTraceParser.errorMessage) assertEquals(4, stackTraceParser.failedLine) assertNull(stackTraceParser.topLocationLine) assertEquals(" foo();", stackTraceParser.failedMethodName) } fun testWrongLineNumber() { configureByFile() val stackTraceParser = createStackTraceParser("pest_qn://" + getFileBeforeFullPath() + "::myTest", getFileBeforeFullPath() + ":seven", "error") assertEquals("error", stackTraceParser.errorMessage) assertEquals(-1, stackTraceParser.failedLine) assertNull(stackTraceParser.topLocationLine) assertNull(stackTraceParser.failedMethodName) } fun testOutRangeLineNumber() { configureByFile() val stackTraceParser = createStackTraceParser("pest_qn://" + getFileBeforeFullPath() + "::myTest", getFileBeforeFullPath() + ":10", "error") assertEquals("error", stackTraceParser.errorMessage) assertEquals(10, stackTraceParser.failedLine) assertNull(stackTraceParser.topLocationLine) assertNull(stackTraceParser.failedMethodName) } fun testOneLineRemote() { configureByFile() val stackTraceParser = createRemoteStackTraceParser("pest_qn://" + getFileBeforeFullPath() + "::myTest", getFileBeforeFullPath() + ":4", "error") assertEquals("error", stackTraceParser.errorMessage) assertEquals(4, stackTraceParser.failedLine) assertNull(stackTraceParser.topLocationLine) assertEquals(" expect(1)->toBe(2);", stackTraceParser.failedMethodName) } } ================================================ FILE: src/test/kotlin/com/pestphp/pest/surrounders/ExpectStatementSurrounderTest.kt ================================================ package com.pestphp.pest.surrounders class ExpectStatementSurrounderTest: SurroundTestCase() { override fun getBasePath(): String = "${super.getBasePath()}/surrounders" fun testSurroundSingleElements() { doTest( ExpectStatementSurrounder(), """ true }); """.trimIndent(), """ expect(true) }); """.trimIndent() ) } fun testSurroundMultipleElements() { doTest( ExpectStatementSurrounder(), """ 100 === 200 }); """.trimIndent(), """ expect(100 === 200) }); """.trimIndent() ) } fun testNoSurroundOutsideTest() { doTest( ExpectStatementSurrounder(), """ true """.trimIndent(), """ it('basic', function () { expect(true)->toBeTrue(); }); """.trimIndent(), """ toBeTrue(); }); """.trimIndent() ) } fun testSurroundInsideDescribeBlock() { doTest( ExpectStatementSurrounder(), """ 1 + 1 }); }); """.trimIndent(), """ expect(1 + 1) }); }); """.trimIndent() ) } } ================================================ FILE: src/test/kotlin/com/pestphp/pest/surrounders/SurroundTestCase.kt ================================================ package com.pestphp.pest.surrounders import com.intellij.codeInsight.generation.surroundWith.SurroundWithHandler import com.intellij.lang.surroundWith.Surrounder import com.pestphp.pest.PestLightCodeFixture abstract class SurroundTestCase: PestLightCodeFixture() { protected fun doTest(surrounder: Surrounder, textBefore: String, textAfter: String) { myFixture.configureByText("simpleTest.php", textBefore) SurroundWithHandler.invoke(project, myFixture.editor, myFixture.file, surrounder) myFixture.checkResult(textAfter) } } ================================================ FILE: src/test/kotlin/com/pestphp/pest/templates/PestPostfixTemplateProviderTest.kt ================================================ package com.pestphp.pest.templates import com.pestphp.pest.PestLightCodeFixture class PestPostfixTemplateProviderTest: PestLightCodeFixture() { override fun getBasePath(): String = "${super.getBasePath()}/templates" private fun doTest() { myFixture.configureByFile(getTestName(true) + ".php") myFixture.type('\t') myFixture.checkResultByFile(getTestName(true) + ".after.php", true) } fun testIt() { doTest() } fun testDescribe() { doTest() } } ================================================ FILE: src/test/kotlin/com/pestphp/pest/types/BaseTypeTestCase.kt ================================================ package com.pestphp.pest.types import com.intellij.testFramework.TestDataPath import com.pestphp.pest.PestLightCodeFixture @TestDataPath("\$CONTENT_ROOT/../resources/com/pestphp/pest/types") abstract class BaseTypeTestCase : PestLightCodeFixture() { override fun setUp() { super.setUp() myFixture.copyFileToProject("TestCase.php") } override fun getBasePath(): String = "${super.getBasePath()}/types" } ================================================ FILE: src/test/kotlin/com/pestphp/pest/types/ExpectCallCompletionTest.kt ================================================ package com.pestphp.pest.types import com.intellij.psi.PsiElement import com.intellij.testFramework.replaceService import com.jetbrains.php.composer.configData.ComposerConfigManager import io.mockk.every import io.mockk.mockk import java.util.concurrent.TimeUnit class ExpectCallCompletionTest : BaseTypeTestCase() { override fun setUp() { super.setUp() val dir = myFixture.copyDirectoryToProject("expect", "tests") myFixture.addFileToProject("composer.json", "") val composerConfigManagerMock = mockk(relaxUnitFun = true) { every { getConfig(null as PsiElement?) } returns dir } project.replaceService( ComposerConfigManager::class.java, composerConfigManagerMock, testRootDisposable ) } fun testFieldCompletions() { myFixture.configureByFile("tests/expectCallCompletion.php") waitForAppLeakingThreads(10, TimeUnit.SECONDS) assertCompletion("someExtend") } fun testFieldCompletionsChainedNotProperty() { myFixture.configureByFile("tests/expectCallCompletionChainedNotProperty.php") assertCompletion("someExtend") } fun testFieldCompletionsChainedNotMethod() { myFixture.configureByFile("tests/expectCallCompletionChainedNotMethod.php") assertCompletion("someExtend") } } ================================================ FILE: src/test/kotlin/com/pestphp/pest/types/FunctionTypeTest.kt ================================================ package com.pestphp.pest.types class FunctionTypeTest : BaseTypeTestCase() { override fun setUp() { super.setUp() myFixture.copyDirectoryToProject("function", "/") } fun testTestFunction() { myFixture.configureByFile("testTest.php") assertFunctionCompletion() } private fun assertFunctionCompletion() { assertCompletion("expectException", "expectExceptionCode") } } ================================================ FILE: src/test/kotlin/com/pestphp/pest/types/ThisFieldCompletionTest.kt ================================================ package com.pestphp.pest.types class ThisFieldCompletionTest : BaseTypeTestCase() { override fun setUp() { super.setUp() myFixture.copyDirectoryToProject("thisField", "tests") } fun testFieldCompletions() { myFixture.configureByFile("tests/beforeEachCompletion.php") assertCompletion("foo") } fun testFieldCompletionWithNamespace() { myFixture.configureByFile("tests/beforeEachNamespaceCompletion.php") assertCompletion("foo") } } ================================================ FILE: src/test/kotlin/com/pestphp/pest/types/ThisFieldTypeTest.kt ================================================ package com.pestphp.pest.types class ThisFieldTypeTest : BaseTypeTestCase() { override fun setUp() { super.setUp() myFixture.copyDirectoryToProject("thisField", "tests") } fun testBeforeEach() { myFixture.configureByFile("tests/beforeEach.php") assertCompletion("a", "b") } fun testBeforeEachNamespace() { myFixture.configureByFile("tests/beforeEachNamespace.php") assertCompletion("a", "b") } fun testAfterEach() { myFixture.configureByFile("tests/afterEach.php") assertCompletion("a", "b") } fun testAfterEachNamespace() { myFixture.configureByFile("tests/afterEachNamespace.php") assertCompletion("a", "b") } } ================================================ FILE: src/test/kotlin/com/pestphp/pest/types/ThisTypeTest.kt ================================================ package com.pestphp.pest.types class ThisTypeTest : BaseTypeTestCase() { override fun setUp() { super.setUp() myFixture.copyDirectoryToProject("this", "/") } fun testItFunction() { myFixture.configureByFile("itTest.php") assertThisCompletion() } fun testTestFunction() { myFixture.configureByFile("testTest.php") assertThisCompletion() } fun testShortLambda() { myFixture.configureByFile("itShortLambdaTest.php") assertThisCompletion() } fun testBeforeEach() { myFixture.configureByFile("beforeEach.php") assertThisCompletion() } private fun assertThisCompletion() { assertCompletion( "expectException", "expectExceptionCode", "someProtectedMethod", "someStaticMethod", "protectedField", ) } } ================================================ FILE: src/test/kotlin/com/pestphp/pest/utilTests/GetPestTestNameTests.kt ================================================ package com.pestphp.pest.utilTests import com.intellij.psi.PsiElement import com.intellij.psi.util.childrenOfType import com.intellij.testFramework.TestDataPath import com.jetbrains.php.lang.psi.elements.FieldReference import com.jetbrains.php.lang.psi.elements.FunctionReference import com.jetbrains.php.lang.psi.elements.Statement import com.jetbrains.php.lang.psi.elements.impl.FunctionReferenceImpl import com.pestphp.pest.PestLightCodeFixture import com.pestphp.pest.getPestTestName import com.pestphp.pest.isDescribeFunction @TestDataPath("\$CONTENT_ROOT/../resources/com/pestphp/pest/PestUtil") class GetPestTestNameTests : PestLightCodeFixture() { override fun getBasePath(): String = "${super.getBasePath()}/PestUtil" fun testFunctionCallNamedItWithDescriptionAndClosure() { val file = myFixture.configureByFile("PestItFunctionCallWithDescriptionAndClosure.php") val testElement = file.firstChild.lastChild.firstChild assertEquals("it basic", testElement.getPestTestName()) } fun testFunctionCallNamedTestWithDescriptionAndClosure() { val file = myFixture.configureByFile("PestTestFunctionCallWithDescriptionAndClosure.php") val testElement = file.firstChild.lastChild.firstChild assertEquals("basic", testElement.getPestTestName()) } fun testFunctionCallNamedItWithConcatStringTest() { val file = myFixture.configureByFile("PestItFunctionCallWithConcatString.php") val testElement = file.firstChild.lastChild.firstChild assertEquals("it basic supertest", testElement.getPestTestName()) } fun testFunctionCallNamedTestWithConcatStringTest() { val file = myFixture.configureByFile("PestTestFunctionCallWithConcatString.php") val testElement = file.firstChild.lastChild.firstChild assertEquals("basic super", testElement.getPestTestName()) } fun testFunctionCallNamedDescribeWithDescriptionAndClosure() { val file = myFixture.configureByFile("PestDescribeBlock.php") val testElement = file.firstChild.lastChild.firstChild assertEquals("`sum` → ", testElement.getPestTestName()) } fun testNestedDescribeFunctionCalls() { val file = myFixture.configureByFile("NestedDescribeFunctionCalls.php") val testElements = getAllPestTests(file.firstChild) listOf( "`SomeClass` → it works as well", "`SomeClass` → `SomeMethod` → it does not work", "`SomeClass` → `SomeMethod` → ", "`SomeClass` → ", ).zip(testElements).toMap().forEach { (expected, describeBlock) -> assertEquals(expected, describeBlock.getPestTestName()) } } fun testArchFunctionCall() { val file = myFixture.configureByFile("PestArchFunctionCall.php") val testElements = file.firstChild.childrenOfType().map { getPestTestFieldReference(it.firstChild) } listOf( "preset → laravel ", "preset → laravel → ignoring 'A'", "preset → laravel → ignoring ['A']", "preset → laravel → ignoring ['A']", "expect 'src' → toUseStrictTypes → not → toUse ['die', 'dd', 'dump']" ).zip(testElements).toMap().forEach { (expected, archTest) -> assertEquals(expected, archTest.getPestTestName()) } } private fun getPestTestFieldReference(test: PsiElement): PsiElement { when (test.firstChild) { is FunctionReference, is FieldReference -> return getPestTestFieldReference(test.firstChild) else -> return test } } private fun getAllPestTests(root: PsiElement): List { if (root is FunctionReferenceImpl && !root.isDescribeFunction()) return listOf(root) return root.children.fold(mutableListOf()) { list, element -> list.addAll( getAllPestTests(element) + if (root is FunctionReferenceImpl && root.isDescribeFunction()) listOf(root) else listOf() ) list } } } ================================================ FILE: src/test/kotlin/com/pestphp/pest/utilTests/GetPestTestsTest.kt ================================================ package com.pestphp.pest.utilTests import com.pestphp.pest.PestLightCodeFixture import com.pestphp.pest.getPestTests class GetPestTestsTest : PestLightCodeFixture() { override fun getBasePath(): String = "${super.getBasePath()}/PestUtil" fun testMethodCallNamedTestIsNotPestTest() { val file = myFixture.configureByFile("MethodCallNamedTest.php") assertEmpty(file.getPestTests()) } fun testMethodCallNamedItIsNotPestTest() { val file = myFixture.configureByFile("MethodCallNamedIt.php") assertEmpty(file.getPestTests()) } fun testFunctionCallNamedItWithDescriptionAndClosure() { val file = myFixture.configureByFile("PestItFunctionCallWithDescriptionAndClosure.php") assertNotEmpty(file.getPestTests()) } fun testFunctionCallNamedItWithDescriptionAndHigherOrder() { val file = myFixture.configureByFile("PestItFunctionCallWithDescriptionAndHigherOrder.php") assertNotEmpty(file.getPestTests()) } fun testFunctionCallNamedTestWithDescriptionAndHigherOrder() { val file = myFixture.configureByFile("PestTestFunctionCallWithDescriptionAndHigherOrder.php") assertNotEmpty(file.getPestTests()) } fun testMethodCallNamedItAndVariableTestIsNotPestTest() { val file = myFixture.configureByFile("MethodCallNamedItAndVariableTest.php") assertEmpty(file.getPestTests()) } fun testPestTestWithNamespaceIsPestTest() { val file = myFixture.configureByFile("PestTestFunctionCallWithNamesapce.php") assertNotEmpty(file.getPestTests()) } fun testNestedDescribeBlockTestsAreIncluded() { val file = myFixture.configureByFile("NestedDescribeFunctionCalls.php") val tests = file.getPestTests() assertEquals(4, tests.size) } } ================================================ FILE: src/test/kotlin/com/pestphp/pest/utilTests/IsPestTestFileTest.kt ================================================ package com.pestphp.pest.utilTests import com.intellij.testFramework.TestDataPath import com.pestphp.pest.PestLightCodeFixture import com.pestphp.pest.isPestTestFile @TestDataPath("\$CONTENT_ROOT/../resources/com/pestphp/pest/PestUtil") class IsPestTestFileTest : PestLightCodeFixture() { override fun getBasePath(): String = "${super.getBasePath()}/PestUtil" fun testMethodCallNamedTestIsNotPestTest() { val file = myFixture.configureByFile("MethodCallNamedTest.php") assertFalse(file.isPestTestFile()) } fun testMethodCallNamedItIsNotPestTest() { val file = myFixture.configureByFile("MethodCallNamedIt.php") assertFalse(file.isPestTestFile()) } fun testFunctionCallNamedItWithDescriptionAndClosure() { val file = myFixture.configureByFile("PestItFunctionCallWithDescriptionAndClosure.php") assertTrue(file.isPestTestFile()) } fun testFunctionCallNamedItWithDescriptionAndHigherOrder() { val file = myFixture.configureByFile("PestItFunctionCallWithDescriptionAndHigherOrder.php") assertTrue(file.isPestTestFile()) } fun testFunctionCallNamedTestWithDescriptionAndHigherOrder() { val file = myFixture.configureByFile("PestTestFunctionCallWithDescriptionAndHigherOrder.php") assertTrue(file.isPestTestFile()) } fun testMethodCallNamedItAndVariableTestIsNotPestTest() { val file = myFixture.configureByFile("MethodCallNamedItAndVariableTest.php") assertFalse(file.isPestTestFile()) } fun testPestTestWithNamespaceIsPestTest() { val file = myFixture.configureByFile("PestTestFunctionCallWithNamesapce.php") assertTrue(file.isPestTestFile()) } } ================================================ FILE: src/test/kotlin/com/pestphp/pest/utilTests/IsPestTestFunctionTest.kt ================================================ package com.pestphp.pest.utilTests import com.intellij.testFramework.TestDataPath import com.pestphp.pest.PestLightCodeFixture import com.pestphp.pest.isPestTestReference @TestDataPath("\$CONTENT_ROOT/../resources/com/pestphp/pest/PestUtil") class IsPestTestFunctionTest : PestLightCodeFixture() { override fun getBasePath(): String = "${super.getBasePath()}/PestUtil" fun testMethodCallNamedTestIsNotPestTest() { val file = myFixture.configureByFile("MethodCallNamedTest.php") val testElement = file.firstChild.lastChild.firstChild assertFalse(testElement.isPestTestReference()) } fun testMethodCallNamedItIsNotPestTest() { val file = myFixture.configureByFile("MethodCallNamedIt.php") val testElement = file.firstChild.lastChild.firstChild assertFalse(testElement.isPestTestReference()) } fun testFunctionCallNamedItWithDescriptionAndClosure() { val file = myFixture.configureByFile("PestItFunctionCallWithDescriptionAndClosure.php") val testElement = file.firstChild.lastChild.firstChild assertTrue(testElement.isPestTestReference()) } fun testFunctionCallNamedItWithDescriptionAndHigherOrder() { val file = myFixture.configureByFile("PestItFunctionCallWithDescriptionAndHigherOrder.php") val testElement = file.firstChild.lastChild.firstChild assertTrue(testElement.isPestTestReference()) } fun testFunctionCallNamedTestWithDescriptionAndHigherOrder() { val file = myFixture.configureByFile("PestTestFunctionCallWithDescriptionAndHigherOrder.php") val testElement = file.firstChild.lastChild.firstChild assertTrue(testElement.isPestTestReference()) } fun testMethodCallNamedItAndVariableTestIsNotPestTest() { val file = myFixture.configureByFile("MethodCallNamedItAndVariableTest.php") val testElement = file.firstChild.lastChild.firstChild assertFalse(testElement.isPestTestReference()) } fun testFunctionCallNamedItWithConcatStringTest() { val file = myFixture.configureByFile("PestItFunctionCallWithConcatString.php") val testElement = file.firstChild.lastChild.firstChild assertTrue(testElement.isPestTestReference()) } fun testFunctionCallNamedTestWithConcatStringTest() { val file = myFixture.configureByFile("PestTestFunctionCallWithConcatString.php") val testElement = file.firstChild.lastChild.firstChild assertTrue(testElement.isPestTestReference()) } } ================================================ FILE: src/test/kotlin/com/pestphp/pest/utilTests/PestUtilTest.kt ================================================ package com.pestphp.pest.utilTests import com.intellij.psi.util.PsiTreeUtil import com.jetbrains.php.lang.psi.elements.impl.FunctionReferenceImpl import com.pestphp.pest.PestLightCodeFixture import com.pestphp.pest.getPestTestName import com.pestphp.pest.isPestTestFunction import java.util.stream.Collectors class PestUtilTest : PestLightCodeFixture() { override fun setUp() { super.setUp() myFixture.copyFileToProject("SimpleTest.php") } override fun getBasePath(): String = "${super.getBasePath()}/utilTests" fun testCanGetTestName() { val file = myFixture.configureByFile("SimpleTest.php") val functions = PsiTreeUtil.findChildrenOfType(file, FunctionReferenceImpl::class.java) .stream().filter(FunctionReferenceImpl::isPestTestFunction) .collect(Collectors.toList()) assertEquals(1, functions.count()) assertEquals( "it basic", functions.first().getPestTestName() ) } fun testClassNameResolutionTestName() { val file = myFixture.configureByFile("ClassNameResolutionTest.php") val functions = PsiTreeUtil.findChildrenOfType(file, FunctionReferenceImpl::class.java) .filter(FunctionReferenceImpl::isPestTestFunction) assertEquals(1, functions.count()) assertEquals( "A", functions.first().getPestTestName() ) } fun testClassNameResolutionInNamespaceTestName() { val file = myFixture.configureByFile("ClassNameResolutionInNamespaceTest.php") val functions = PsiTreeUtil.findChildrenOfType(file, FunctionReferenceImpl::class.java) .filter(FunctionReferenceImpl::isPestTestFunction) assertEquals(1, functions.count()) assertEquals( "A\\\\B", functions.first().getPestTestName() ) } } ================================================ FILE: src/test/kotlin/com/pestphp/pest/utilTests/ToPestFqnTests.kt ================================================ package com.pestphp.pest.utilTests import com.pestphp.pest.PestLightCodeFixture import com.pestphp.pest.toPestFqn class ToPestFqnTests : PestLightCodeFixture() { override fun getBasePath(): String = "${super.getBasePath()}/PestUtil" fun testCanGeneratePqn() { val file = myFixture.configureByFile( "PestItFunctionCallWithDescriptionAndClosure.php", ) val testElement = file?.firstChild?.lastChild?.firstChild val pqn = testElement?.toPestFqn() assertTrue(pqn?.any { it.contains("::") } == true) assertTrue(pqn?.any { it.contains("pest_qn:") } == true) } } ================================================ FILE: src/test/kotlin/com/pestphp/pest/utilTests/ToPestTestRegexTests.kt ================================================ package com.pestphp.pest.utilTests import com.jetbrains.php.util.pathmapper.PhpPathMapper import com.pestphp.pest.PestLightCodeFixture import com.pestphp.pest.toPestTestRegex class ToPestTestRegexTests : PestLightCodeFixture() { override fun getBasePath(): String = "${super.getBasePath()}/PestUtil" fun testRegexContainsStartBounds() { val file = myFixture.configureByFile( "PestItFunctionCallWithDescriptionAndClosure.php", ) val testElement = file.firstChild?.lastChild?.firstChild val regex = testElement?.toPestTestRegex("src") assertTrue(regex?.startsWith('^') == true) } fun testRegexContainsPestNamespacePrefix() { val file = myFixture.configureByFile( "PestItFunctionCallWithDescriptionAndClosure.php" ) val testElement = file.firstChild?.lastChild?.firstChild val regex = testElement?.toPestTestRegex("src") assertTrue(regex?.contains("P\\\\") == true) } fun testRegexContainsClassMethodSeparator() { val file = myFixture.configureByFile( "PestItFunctionCallWithDescriptionAndClosure.php" ) val testElement = file.firstChild?.lastChild?.firstChild val regex = testElement?.toPestTestRegex("src") assertTrue(regex?.contains("::") == true) } fun testRegexContainsItWhenItFunctionCall() { val file = myFixture.configureByFile( "PestItFunctionCallWithDescriptionAndClosure.php" ) val testElement = file.firstChild?.lastChild?.firstChild val regex = testElement?.toPestTestRegex("src") assertTrue(regex?.contains("it") == true) } fun testRegexEscapesParenthesis() { val file = myFixture.configureByFile( "PestTestFunctionCallWithParenthesis.php" ) val testElement = file.firstChild?.lastChild?.firstChild val regex = testElement?.toPestTestRegex("src") assertTrue(regex?.contains("\\(") == true) assertTrue(regex?.contains("\\)") == true) } fun testRegexEscapesCircumflexes() { val file = myFixture.configureByFile( "PestTestFunctionCallWithCircumflex.php" ) val testElement = file.firstChild?.lastChild?.firstChild val regex = testElement?.toPestTestRegex("src") assertTrue(regex?.contains("\\^") == true) } fun testRegexEscapesPlusAndQuestionMark() { val file = myFixture.configureByFile( "PestTestWithPlusAndQuestionMark.php" ) val testElement = file.firstChild?.lastChild?.firstChild val regex = testElement?.toPestTestRegex("src") assertTrue(regex?.contains("\\+") == true) assertTrue(regex?.contains("\\?") == true) } fun testRegexEndOfLine() { val file = myFixture.configureByFile( "PestDescribeBlockAndTestFunctionEndOfLine.php" ) val testElement = file.firstChild?.children?.map { it.firstChild }?.lastOrNull() val describeElement = file.firstChild?.children?.map { it.firstChild }?.firstOrNull() val testRegex = testElement?.toPestTestRegex("src") val describeRegex = describeElement?.toPestTestRegex("src") assertTrue(testRegex?.endsWith("$") == true) assertTrue(describeRegex?.endsWith("$") == false) } fun testRegexHandlesTestSuffix() { val file = myFixture.configureByFile( "Login.test.php" ) val testElement = file.firstChild?.lastChild?.firstChild val regex = testElement?.toPestTestRegex("src") assertFalse(regex?.contains(".test") == true) assertTrue(regex?.contains("Login") == true) } fun testRegexHandlesSpecSuffix() { val file = myFixture.configureByFile( "User.spec.php" ) val testElement = file.firstChild?.lastChild?.firstChild val regex = testElement?.toPestTestRegex("src") assertFalse(regex?.contains(".spec") == true) assertTrue(regex?.contains("User") == true) } fun testRegexHandlesEmojiInPath() { val regex = "it renders components".toPestTestRegex( "src", "/src/livewire⚡/Component.php", PhpPathMapper.create(this.project) ) assertFalse(regex.contains("⚡")) assertTrue(regex.contains("livewire")) assertTrue(regex.contains("Component")) } fun testRegexHandlesDotInDirectoryName() { val file = myFixture.configureByFile( "dir.name/Test.php" ) val testElement = file.firstChild?.lastChild?.firstChild val regex = testElement?.toPestTestRegex("src") assertFalse(regex?.contains("dir.name") == true) assertTrue(regex?.contains("dirname") == true) } fun testRegexHandlesMultipleDots() { val file = myFixture.configureByFile( "Login.integration.test.php" ) val testElement = file.firstChild?.lastChild?.firstChild val regex = testElement?.toPestTestRegex("src") assertFalse(regex?.contains(".integration") == true) assertFalse(regex?.contains(".test") == true) assertTrue(regex?.contains("Login") == true) } } ================================================ FILE: src/test/resources/com/pestphp/pest/Dataset.php ================================================ in('Feature'); uses(TestCase::class)->in('Unit'); ================================================ FILE: src/test/resources/com/pestphp/pest/PestTestRunLineMarkerProviderTest/AssignmentFunctionCallNamedTest.php ================================================ toBe(1); }); ================================================ FILE: src/test/resources/com/pestphp/pest/PestTestRunLineMarkerProviderTest/AssignmentFunctionCallNamedTestWithoutPest.php ================================================ createMock(""); }); ================================================ FILE: src/test/resources/com/pestphp/pest/PestTestRunLineMarkerProviderTest/FunctionCallNamedTestWithoutPest.php ================================================ it(""); ================================================ FILE: src/test/resources/com/pestphp/pest/PestTestRunLineMarkerProviderTest/NamedDataSets.php ================================================ with([ "dataset a" => [3, 3], "dataset b" => [4, 4] ]); it('Can act as summator', function ($a) { $result = (new SimpleCalculator)->add($a, b: 0); expect ($result) ->toBe ( expected: 1); }) ->with([ 'first' => fn() => array_map (fn() => rand(1,10), range( start: 1, end: 10)) , 'second' => [1], ]); }); ================================================ FILE: src/test/resources/com/pestphp/pest/PestTestRunLineMarkerProviderTest/PestItFunctionCallWithDescriptionAndClosure.php ================================================ assertTrue(true); }); ================================================ FILE: src/test/resources/com/pestphp/pest/PestTestRunLineMarkerProviderTest/PestItFunctionCallWithRedefinition.php ================================================ assertTrue(true); }); ================================================ FILE: src/test/resources/com/pestphp/pest/PestTestRunLineMarkerProviderTest/contextProject/tests/Test.php ================================================ assertTrue(true); }); ================================================ FILE: src/test/resources/com/pestphp/pest/PestUtil/Login.integration.test.php ================================================ assertTrue(true); }); ================================================ FILE: src/test/resources/com/pestphp/pest/PestUtil/Login.test.php ================================================ assertTrue(true); }); ================================================ FILE: src/test/resources/com/pestphp/pest/PestUtil/MethodCallNamedIt.php ================================================ it(); ================================================ FILE: src/test/resources/com/pestphp/pest/PestUtil/MethodCallNamedItAndVariableTest.php ================================================ it(); ================================================ FILE: src/test/resources/com/pestphp/pest/PestUtil/MethodCallNamedTest.php ================================================ test(); ================================================ FILE: src/test/resources/com/pestphp/pest/PestUtil/NestedDescribeFunctionCalls.php ================================================ toBeTrue(); }); describe('SomeMethod', function () { it('does not work', function () { expect(true)->toBeTrue(); }); }); }); ================================================ FILE: src/test/resources/com/pestphp/pest/PestUtil/PestArchFunctionCall.php ================================================ preset()->laravel(); arch()->preset()->laravel()->ignoring("A"); arch()->preset()->laravel()->ignoring(["A"]); arch()->preset()->laravel()->ignoring(array("A")); arch()->expect('src')->toUseStrictTypes()->not->toUse(['die', 'dd', 'dump']); ================================================ FILE: src/test/resources/com/pestphp/pest/PestUtil/PestDescribeBlock.php ================================================ assertTrue(true); }); ================================================ FILE: src/test/resources/com/pestphp/pest/PestUtil/PestItFunctionCallWithDescriptionAndClosure.php ================================================ assertTrue(true); }); ================================================ FILE: src/test/resources/com/pestphp/pest/PestUtil/PestItFunctionCallWithDescriptionAndHigherOrder.php ================================================ assertTrue(true); ================================================ FILE: src/test/resources/com/pestphp/pest/PestUtil/PestTestFunctionCallWithCircumflex.php ================================================ assertTrue(true); }); ================================================ FILE: src/test/resources/com/pestphp/pest/PestUtil/PestTestFunctionCallWithConcatString.php ================================================ assertTrue(true); }); ================================================ FILE: src/test/resources/com/pestphp/pest/PestUtil/PestTestFunctionCallWithDescriptionAndClosure.php ================================================ assertTrue(true); }); ================================================ FILE: src/test/resources/com/pestphp/pest/PestUtil/PestTestFunctionCallWithDescriptionAndHigherOrder.php ================================================ assertTrue(true); ================================================ FILE: src/test/resources/com/pestphp/pest/PestUtil/PestTestFunctionCallWithNamesapce.php ================================================ assertTrue(true); }); ================================================ FILE: src/test/resources/com/pestphp/pest/PestUtil/PestTestFunctionCallWithParenthesis.php ================================================ assertTrue(true); }); ================================================ FILE: src/test/resources/com/pestphp/pest/PestUtil/PestTestWithPlusAndQuestionMark.php ================================================ assertTrue(true); }); ================================================ FILE: src/test/resources/com/pestphp/pest/PestUtil/User.spec.php ================================================ assertTrue(true); }); ================================================ FILE: src/test/resources/com/pestphp/pest/PestUtil/dir.name/Test.php ================================================ assertTrue(true); }); ================================================ FILE: src/test/resources/com/pestphp/pest/SimpleHigherOrderNotTest.php ================================================ not->assertTrue(true); ================================================ FILE: src/test/resources/com/pestphp/pest/SimpleHigherOrderTestWithName.php ================================================ assertTrue(true); ================================================ FILE: src/test/resources/com/pestphp/pest/SimpleScript.php ================================================ assertTrue(true); }); ================================================ FILE: src/test/resources/com/pestphp/pest/annotator/DuplicateCustomExpectation.afterDelete.php ================================================ extend('toBeOne', function (): bool { return false; }); ================================================ FILE: src/test/resources/com/pestphp/pest/annotator/DuplicateCustomExpectation.afterNavigate.php ================================================ expect()->extend('toBeOne', function (): bool { return true; }); expect()->extend('toBeOne', function (): bool { return false; }); ================================================ FILE: src/test/resources/com/pestphp/pest/annotator/DuplicateCustomExpectation.php ================================================ expect()->extend('toBeOne', function (): bool { return true; }); expect()->extend('toBeOne', function (): bool { return false; }); ================================================ FILE: src/test/resources/com/pestphp/pest/annotator/DuplicateTestName.afterDelete.php ================================================ assertTrue(false); }); ================================================ FILE: src/test/resources/com/pestphp/pest/annotator/DuplicateTestName.afterNavigate.php ================================================ test('basic', function () { $this->assertTrue(true); }); test('basic', function () { $this->assertTrue(false); }); ================================================ FILE: src/test/resources/com/pestphp/pest/annotator/DuplicateTestName.php ================================================ test('basic', function () { $this->assertTrue(true); }); test('basic', function () { $this->assertTrue(false); }); ================================================ FILE: src/test/resources/com/pestphp/pest/annotator/DuplicateTestNameInDescribeBlock.php ================================================ test('adds numbers', function () { expect(1 + 1)->toBe(2); }); test('adds numbers', function () { expect(2 + 2)->toBe(4); }); }); ================================================ FILE: src/test/resources/com/pestphp/pest/annotator/NoDuplicateCustomExpectation.php ================================================ extend('toBeOne', function (): bool { return true; }); expect()->extend('toBeTwo', function (): bool { return true; }); ================================================ FILE: src/test/resources/com/pestphp/pest/annotator/NoDuplicateTestName.php ================================================ assertTrue(true); }); test('Another test', function () { $this->assertTrue(true); }); class A { } class B { } test(A::class, function () { $this->assertTrue(true); }); test(B::class, function () { $this->assertTrue(true); }); class C { const c = "something"; } test(C::c, function () { $this->assertTrue(true); }); test(C::c, function () { $this->assertTrue(true); }); ================================================ FILE: src/test/resources/com/pestphp/pest/annotator/stub/Functions.php ================================================ $this->call()))->toBe("bar"); }); function someFoo($closure) { return $closure(); } trait T { function foo() { return "bar"; } } ================================================ FILE: src/test/resources/com/pestphp/pest/codeInsight/typeInference/ThisInSubproject/Test.php ================================================ $this; }); ================================================ FILE: src/test/resources/com/pestphp/pest/configuration/FileWithPestTest.php ================================================ test('basic', function () { $this->assertTrue(true); }); ================================================ FILE: src/test/resources/com/pestphp/pest/configuration/locationProvider/DescribeBlock/subdir/Test.php ================================================ toBe(true); }); it('check valid', function () { expect(1)->toBe(1); }); }); ================================================ FILE: src/test/resources/com/pestphp/pest/configuration/locationProvider/DescribeBlockIt/subdir/Test.php ================================================ toBe(true); }); it('check valid', function () { expect(1)->toBe(1); }); }); ================================================ FILE: src/test/resources/com/pestphp/pest/configuration/locationProvider/SubprojectFor1xVersion/subdir/Test.php ================================================ }); ================================================ FILE: src/test/resources/com/pestphp/pest/configuration/pest/tests/DynamicFeature/FeatureTest.php ================================================ }); ================================================ FILE: src/test/resources/com/pestphp/pest/configuration/pest/tests/Feature/FeatureTest.php ================================================ }); ================================================ FILE: src/test/resources/com/pestphp/pest/configuration/pest/tests/GlobPattern/DirectoryTest.php ================================================ }); ================================================ FILE: src/test/resources/com/pestphp/pest/configuration/pest/tests/GlobPattern/FileTest.php ================================================ }); ================================================ FILE: src/test/resources/com/pestphp/pest/configuration/pest/tests/GlobPattern/FileWithRelativePathTest.php ================================================ }); ================================================ FILE: src/test/resources/com/pestphp/pest/configuration/pest/tests/GroupedFeature/GroupedFeatureTest.php ================================================ }); ================================================ FILE: src/test/resources/com/pestphp/pest/configuration/pest/tests/Pest.php ================================================ extend(BaseTestCase::class); pest()->extend(FeatureTestCase::class)->in("Feature"); pest()->extend(FeatureTestCase::class, SomeBaseTrait::class)->group("some group")->in("GroupedFeature"); pest()->extend(FeatureTestCase::class)->in(__DIR__ . "/DIRFeature"); pest()->extend(FeatureTestCase::class)->in('../tests/DynamicFeature'); pest()->extend(FeatureTestCase::class)->in("*/DirectoryTest.php", "GlobPattern/Fi*st.php", "../tests/GlobPattern/Fi*st.php"); ================================================ FILE: src/test/resources/com/pestphp/pest/configuration/pest/tests/Unit/PestExtendUnitTest.php ================================================ extend(SomeTrait::class); it('has home', function (){ $this-> }); ================================================ FILE: src/test/resources/com/pestphp/pest/configuration/pest/tests/Unit/UnitTest.php ================================================ }); ================================================ FILE: src/test/resources/com/pestphp/pest/configuration/pest/tests2/Unit/UnitTest.php ================================================ }); ================================================ FILE: src/test/resources/com/pestphp/pest/configuration/php ================================================ ================================================ FILE: src/test/resources/com/pestphp/pest/configuration/uses/DIRFeature/FeatureTest.php ================================================ }); ================================================ FILE: src/test/resources/com/pestphp/pest/configuration/uses/DynamicFeature/FeatureTest.php ================================================ }); ================================================ FILE: src/test/resources/com/pestphp/pest/configuration/uses/Feature/FeatureTest.php ================================================ }); ================================================ FILE: src/test/resources/com/pestphp/pest/configuration/uses/GlobPattern/DirectoryTest.php ================================================ }); ================================================ FILE: src/test/resources/com/pestphp/pest/configuration/uses/GlobPattern/FileTest.php ================================================ }); ================================================ FILE: src/test/resources/com/pestphp/pest/configuration/uses/GlobPattern/FileWithRelativePathTest.php ================================================ }); ================================================ FILE: src/test/resources/com/pestphp/pest/configuration/uses/GroupedFeature/GroupedFeatureTest.php ================================================ }); ================================================ FILE: src/test/resources/com/pestphp/pest/configuration/uses/Pest.php ================================================ in("Feature"); uses(FeatureTestCase::class, SomeBaseTrait::class)->group("some group")->in("GroupedFeature"); uses(FeatureTestCase::class)->in(__DIR__ . "/DIRFeature"); uses(FeatureTestCase::class)->in('../tests/DynamicFeature'); uses(FeatureTestCase::class)->in("*/DirectoryTest.php", "GlobPattern/Fi*st.php", "../tests/GlobPattern/Fi*st.php"); ================================================ FILE: src/test/resources/com/pestphp/pest/configuration/uses/Unit/UnitTest.php ================================================ }); ================================================ FILE: src/test/resources/com/pestphp/pest/configuration/uses/Unit/UsesUnitTest.php ================================================ }); ================================================ FILE: src/test/resources/com/pestphp/pest/customExpectations/CustomExpectation.php ================================================ extend('toBeOne', function (): bool { return true; }); ================================================ FILE: src/test/resources/com/pestphp/pest/customExpectations/CustomExpectationWithParameter.php ================================================ extend('toHaveValues', function ($valueA, string $valueB, bool $valueC = true): bool { return true; }); ================================================ FILE: src/test/resources/com/pestphp/pest/customExpectations/CustomThisExpectation.php ================================================ extend('toThis', function () { return $this->toBeEmpty(); }); ================================================ FILE: src/test/resources/com/pestphp/pest/customExpectations/CustomUserExpectation.php ================================================ extend('createUser', function (): User { return new User(); }); ================================================ FILE: src/test/resources/com/pestphp/pest/customExpectations/UnfinishedCustomExpectation.php ================================================ extend('toBeOne'); ================================================ FILE: src/test/resources/com/pestphp/pest/customExpectations/generators/ExpectationGenerator/GeneratedWithMethod.php ================================================ extend('toBeExpectationFromSubFolder', function (): bool { return true; }); ================================================ FILE: src/test/resources/com/pestphp/pest/features/configuration/pest/CompleteFakePestInFolder.php ================================================ in(''); ================================================ FILE: src/test/resources/com/pestphp/pest/features/configuration/pest/CompleteInFolder.php ================================================ extend(TestCase::class)->in(''); ================================================ FILE: src/test/resources/com/pestphp/pest/features/configuration/pest/Test.php ================================================ assertTrue(true); }); ================================================ FILE: src/test/resources/com/pestphp/pest/features/configuration/uses/CompleteFakeInFolder.php ================================================ in(''); ================================================ FILE: src/test/resources/com/pestphp/pest/features/configuration/uses/CompleteInFolder.php ================================================ in(''); ================================================ FILE: src/test/resources/com/pestphp/pest/features/configuration/uses/Test.php ================================================ assertTrue(true); }); ================================================ FILE: src/test/resources/com/pestphp/pest/features/datasets/AutocompleteDatasetTest.php ================================================ with(''); ================================================ FILE: src/test/resources/com/pestphp/pest/features/datasets/DatasetInDescribeBlock.php ================================================ toBe(1); })->with(''); dataset('some_numbers', function () { return [1, 2]; }); }); ================================================ FILE: src/test/resources/com/pestphp/pest/features/datasets/DatasetInDescribeBlockReference.php ================================================ toBe(1); })->with('some_numbers'); dataset('some_numbers', function () { return [1, 2]; }); }); ================================================ FILE: src/test/resources/com/pestphp/pest/features/datasets/DatasetInsideDescribeBlockTest.php ================================================ toBe(true); }); test('check valid', function ($number) { expect($number)->toBe(1); })->with('some_numbers'); dataset('some_numbers', function () { return [ 1, 2 ]; }); }); ================================================ FILE: src/test/resources/com/pestphp/pest/features/datasets/DatasetNoArgsTest.php ================================================ with(); ================================================ FILE: src/test/resources/com/pestphp/pest/features/datasets/DatasetOnNonPestTest.php ================================================ with('stances'); ================================================ FILE: src/test/resources/com/pestphp/pest/features/datasets/DatasetOnNonPestTestCompletion.php ================================================ with(''); ================================================ FILE: src/test/resources/com/pestphp/pest/features/datasets/DatasetReference.php ================================================ with('dojos'); ================================================ FILE: src/test/resources/com/pestphp/pest/features/datasets/DatasetTest.php ================================================ with('dojos'); ================================================ FILE: src/test/resources/com/pestphp/pest/features/datasets/Datasets.php ================================================ with([ Bar::class, Restaurant::class, ])->with(''); ================================================ FILE: src/test/resources/com/pestphp/pest/features/datasets/InvalidDatasetInDescribeBlockTest.php ================================================ toBeInt(); })->with('invalid_dataset'); }); ================================================ FILE: src/test/resources/com/pestphp/pest/features/datasets/InvalidDatasetNameCase.after.php ================================================ toBe(1); })->with('some numbers'); dataset('some numbers', function () { return [ 1 ]; }); test('test 2', function ($number) { expect($number)->toBe(2); })->with('some numbers'); ================================================ FILE: src/test/resources/com/pestphp/pest/features/datasets/InvalidDatasetNameCase.php ================================================ toBe(1); })->with('some_numbers'); dataset('some_numbers', function () { return [ 1 ]; }); test('test 2', function ($number) { expect($number)->toBe(2); })->with('some_numbers'); ================================================ FILE: src/test/resources/com/pestphp/pest/features/datasets/InvalidDatasetTest.php ================================================ with('dodos'); ================================================ FILE: src/test/resources/com/pestphp/pest/features/datasets/NotDatasetReference.php ================================================ notDataset(''); ================================================ FILE: src/test/resources/com/pestphp/pest/features/datasets/SharedDatasetReference.php ================================================ with('stances'); ================================================ FILE: src/test/resources/com/pestphp/pest/features/datasets/ValidDatasetNameCase.php ================================================ assertTrue($user->isPestDeveloper()); }); }); ================================================ FILE: src/test/resources/com/pestphp/pest/goto/PestTestFinder/test/App/UserTest.php ================================================ assertEquals("Oliver Nybroe", $user->getName()); }); test("is pest developer", function () { $user = new User(); $this->assertTrue($user->isPestDeveloper()); }); test("is pest developer check if not false", function () { $user = new User(); $this->assertNotEquals(false, $user->isPestDeveloper()); }); test("incorrect is pest developer", function () { $user = new User(); $this->assertNotEquals(false, $user->isPestDeveloper()); }); ================================================ FILE: src/test/resources/com/pestphp/pest/goto/datasetUsages/DatasetDeclaration.php ================================================ datasetName', [ 'Seiza', 'Musubi Dachi', 'Heisoku Dachi', 'Heiko Dachi', 'Zenkutsu Dachi', 'Han Zenkutsu Dachi', 'Kokutsu Dachi', ]); it('can check if in the valley', function (string $dojo) { })->with('datasetName'); ================================================ FILE: src/test/resources/com/pestphp/pest/goto/datasetUsages/DatasetUsage.php ================================================ with([])->with('datasetName'); ================================================ FILE: src/test/resources/com/pestphp/pest/higherOrderExpectations/.phpstorm.meta.php ================================================ getTest()-> ================================================ FILE: src/test/resources/com/pestphp/pest/higherOrderExpectations/ExpectMethodAssertionCompletionChained.php ================================================ getDate()->getTimestamp()-> ================================================ FILE: src/test/resources/com/pestphp/pest/higherOrderExpectations/ExpectMethodAssertionCompletionChainedAssertions.php ================================================ getTest()->toBeTrue() ->getOtherExample()-> ================================================ FILE: src/test/resources/com/pestphp/pest/higherOrderExpectations/ExpectMethodCompletion.php ================================================ ================================================ FILE: src/test/resources/com/pestphp/pest/higherOrderExpectations/ExpectMethodCompletionChained.php ================================================ getDate()-> ================================================ FILE: src/test/resources/com/pestphp/pest/higherOrderExpectations/ExpectMethodCompletionChainedAssertions.php ================================================ getTest()->toBeString() -> ================================================ FILE: src/test/resources/com/pestphp/pest/higherOrderExpectations/ExpectPropertyAssertionCompletion.php ================================================ test-> ================================================ FILE: src/test/resources/com/pestphp/pest/higherOrderExpectations/ExpectPropertyAssertionCompletionChained.php ================================================ test = new DateTime(); } } $example = new Example(); expect($example)->test->getTimestamp()-> ================================================ FILE: src/test/resources/com/pestphp/pest/higherOrderExpectations/ExpectPropertyAssertionCompletionChainedAssertions.php ================================================ test->toBeString() ->otherExample-> ================================================ FILE: src/test/resources/com/pestphp/pest/higherOrderExpectations/ExpectPropertyCompletion.php ================================================ ================================================ FILE: src/test/resources/com/pestphp/pest/higherOrderExpectations/ExpectPropertyCompletionChained.php ================================================ date = new Chained(); } } $example = new Example(); expect($example)->date-> ================================================ FILE: src/test/resources/com/pestphp/pest/higherOrderExpectations/ExpectPropertyCompletionChainedAssertions.php ================================================ test->toBeString() -> ================================================ FILE: src/test/resources/com/pestphp/pest/higherOrderExpectations/stubs.php ================================================ value = $value; } /** * Creates a new expectation. * * @param TValue $value * * @return Expectation */ public function and($value): Expectation { return new self($value); } /** * Creates a new expectation with the decoded JSON value. */ public function json(): Expectation { return $this->toBeJson()->and(json_decode($this->value, true)); } /** * Dump the expectation value and end the script. * * @param mixed $arguments * * @return never */ public function dd(...$arguments): void { if (function_exists('dd')) { dd($this->value, ...$arguments); } var_dump($this->value); exit(1); } /** * Send the expectation value to Ray along with all given arguments. * * @param mixed $arguments */ public function ray(...$arguments): self { if (function_exists('ray')) { ray($this->value, ...$arguments); } return $this; } /** * Creates the opposite expectation for the value. */ public function not(): OppositeExpectation { return new OppositeExpectation($this); } /** * Creates an expectation on each item of the iterable "value". */ public function each(callable $callback = null): Each { if (!is_iterable($this->value)) { throw new BadMethodCallException('Expectation value is not iterable.'); } if (is_callable($callback)) { foreach ($this->value as $item) { $callback(new self($item)); } } return new Each($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 */ public function sequence(...$callbacks): Expectation { if (!is_iterable($this->value)) { throw new BadMethodCallException('Expectation value is not iterable.'); } $value = is_array($this->value) ? $this->value : iterator_to_array($this->value); $keys = array_keys($value); $values = array_values($value); $callbacksCount = count($callbacks); $index = 0; while (count($callbacks) < count($values)) { $callbacks[] = $callbacks[$index]; $index = $index < count($values) - 1 ? $index + 1 : 0; } if ($callbacksCount > count($values)) { Assert::assertLessThanOrEqual(count($value), count($callbacks)); } foreach ($values as $key => $item) { if ($callbacks[$key] instanceof Closure) { call_user_func($callbacks[$key], new self($item), new self($keys[$key])); continue; } (new self($item))->toEqual($callbacks[$key]); } 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 */ public function match($subject, array $expressions): Expectation { $subject = is_callable($subject) ? $subject : function () use ($subject) { return $subject; }; $subject = $subject(); $matched = false; foreach ($expressions as $key => $callback) { if ($subject != $key) { 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 */ public function unless($condition, callable $callback): Expectation { $condition = is_callable($condition) ? $condition : static function () use ($condition): bool { return (bool) $condition; // @phpstan-ignore-line }; return $this->when(!$condition(), $callback); } /** * Apply the callback if the given "condition" is truthy. * * @param (callable(): bool)|bool $condition * @param callable(Expectation): mixed $callback */ public function when($condition, callable $callback): Expectation { $condition = is_callable($condition) ? $condition : static function () use ($condition): bool { return (bool) $condition; // @phpstan-ignore-line }; if ($condition()) { $callback($this->and($this->value)); } return $this; } /** * Asserts that two variables have the same type and * value. Used on objects, it asserts that two * variables reference the same object. * * @param mixed $expected */ public function toBe($expected): Expectation { Assert::assertSame($expected, $this->value); return $this; } /** * Asserts that the value is empty. */ public function toBeEmpty(): Expectation { Assert::assertEmpty($this->value); return $this; } /** * Asserts that the value is true. */ public function toBeTrue(): Expectation { Assert::assertTrue($this->value); return $this; } /** * Asserts that the value is truthy. */ public function toBeTruthy(): Expectation { Assert::assertTrue((bool) $this->value); return $this; } /** * Asserts that the value is false. */ public function toBeFalse(): Expectation { Assert::assertFalse($this->value); return $this; } /** * Asserts that the value is falsy. */ public function toBeFalsy(): Expectation { Assert::assertFalse((bool) $this->value); return $this; } /** * Asserts that the value is greater than $expected. * * @param int|float $expected */ public function toBeGreaterThan($expected): Expectation { Assert::assertGreaterThan($expected, $this->value); return $this; } /** * Asserts that the value is greater than or equal to $expected. * * @param int|float $expected */ public function toBeGreaterThanOrEqual($expected): Expectation { Assert::assertGreaterThanOrEqual($expected, $this->value); return $this; } /** * Asserts that the value is less than or equal to $expected. * * @param int|float $expected */ public function toBeLessThan($expected): Expectation { Assert::assertLessThan($expected, $this->value); return $this; } /** * Asserts that the value is less than $expected. * * @param int|float $expected */ public function toBeLessThanOrEqual($expected): Expectation { Assert::assertLessThanOrEqual($expected, $this->value); return $this; } /** * Asserts that $needle is an element of the value. * * @param mixed $needles */ public function toContain(...$needles): Expectation { foreach ($needles as $needle) { if (is_string($this->value)) { Assert::assertStringContainsString($needle, $this->value); } else { Assert::assertContains($needle, $this->value); } } return $this; } /** * Asserts that the value starts with $expected. */ public function toStartWith(string $expected): Expectation { Assert::assertStringStartsWith($expected, $this->value); return $this; } /** * Asserts that the value ends with $expected. */ public function toEndWith(string $expected): Expectation { Assert::assertStringEndsWith($expected, $this->value); return $this; } /** * Asserts that $number matches value's Length. */ public function toHaveLength(int $number): Expectation { if (is_string($this->value)) { Assert::assertEquals($number, mb_strlen($this->value)); return $this; } if (is_iterable($this->value)) { return $this->toHaveCount($number); } if (is_object($this->value)) { if (method_exists($this->value, 'toArray')) { $array = $this->value->toArray(); } else { $array = (array) $this->value; } Assert::assertCount($number, $array); return $this; } throw new BadMethodCallException('Expectation value length is not countable.'); } /** * Asserts that $count matches the number of elements of the value. */ public function toHaveCount(int $count): Expectation { Assert::assertCount($count, $this->value); return $this; } /** * Asserts that the value contains the property $name. * * @param mixed $value */ public function toHaveProperty(string $name, $value = null): Expectation { $this->toBeObject(); Assert::assertTrue(property_exists($this->value, $name)); if (func_num_args() > 1) { /* @phpstan-ignore-next-line */ Assert::assertEquals($value, $this->value->{$name}); } return $this; } /** * Asserts that the value contains the provided properties $names. * * @param iterable $names */ public function toHaveProperties(iterable $names): Expectation { foreach ($names as $name) { $this->toHaveProperty($name); } return $this; } /** * Asserts that two variables have the same value. * * @param mixed $expected */ public function toEqual($expected): Expectation { Assert::assertEquals($expected, $this->value); 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. * * @param mixed $expected */ public function toEqualCanonicalizing($expected): Expectation { Assert::assertEqualsCanonicalizing($expected, $this->value); return $this; } /** * Asserts that the absolute difference between the value and $expected * is lower than $delta. * * @param mixed $expected */ public function toEqualWithDelta($expected, float $delta): Expectation { Assert::assertEqualsWithDelta($expected, $this->value, $delta); return $this; } /** * Asserts that the value is one of the given values. * * @param iterable $values */ public function toBeIn(iterable $values): Expectation { Assert::assertContains($this->value, $values); return $this; } /** * Asserts that the value is infinite. */ public function toBeInfinite(): Expectation { Assert::assertInfinite($this->value); return $this; } /** * Asserts that the value is an instance of $class. * * @param string $class */ public function toBeInstanceOf($class): Expectation { /* @phpstan-ignore-next-line */ Assert::assertInstanceOf($class, $this->value); return $this; } /** * Asserts that the value is an array. */ public function toBeArray(): Expectation { Assert::assertIsArray($this->value); return $this; } /** * Asserts that the value is of type bool. */ public function toBeBool(): Expectation { Assert::assertIsBool($this->value); return $this; } /** * Asserts that the value is of type callable. */ public function toBeCallable(): Expectation { Assert::assertIsCallable($this->value); return $this; } /** * Asserts that the value is of type float. */ public function toBeFloat(): Expectation { Assert::assertIsFloat($this->value); return $this; } /** * Asserts that the value is of type int. */ public function toBeInt(): Expectation { Assert::assertIsInt($this->value); return $this; } /** * Asserts that the value is of type iterable. */ public function toBeIterable(): Expectation { Assert::assertIsIterable($this->value); return $this; } /** * Asserts that the value is of type numeric. */ public function toBeNumeric(): Expectation { Assert::assertIsNumeric($this->value); return $this; } /** * Asserts that the value is of type object. */ public function toBeObject(): Expectation { Assert::assertIsObject($this->value); return $this; } /** * Asserts that the value is of type resource. */ public function toBeResource(): Expectation { Assert::assertIsResource($this->value); return $this; } /** * Asserts that the value is of type scalar. */ public function toBeScalar(): Expectation { Assert::assertIsScalar($this->value); return $this; } /** * Asserts that the value is of type string. */ public function toBeString(): Expectation { Assert::assertIsString($this->value); return $this; } /** * Asserts that the value is a JSON string. */ public function toBeJson(): Expectation { Assert::assertIsString($this->value); Assert::assertJson($this->value); return $this; } /** * Asserts that the value is NAN. */ public function toBeNan(): Expectation { Assert::assertNan($this->value); return $this; } /** * Asserts that the value is null. */ public function toBeNull(): Expectation { Assert::assertNull($this->value); return $this; } /** * Asserts that the value array has the provided $key. * * @param string|int $key * @param mixed $value */ public function toHaveKey($key, $value = null): Expectation { 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) { throw new ExpectationFailedException("Failed asserting that an array has the key '$key'", $exception->getComparisonFailure()); } if (func_num_args() > 1) { Assert::assertEquals($value, Arr::get($array, $key)); } return $this; } /** * Asserts that the value array has the provided $keys. * * @param array $keys */ public function toHaveKeys(array $keys): Expectation { foreach ($keys as $key) { $this->toHaveKey($key); } return $this; } /** * Asserts that the value is a directory. */ public function toBeDirectory(): Expectation { Assert::assertDirectoryExists($this->value); return $this; } /** * Asserts that the value is a directory and is readable. */ public function toBeReadableDirectory(): Expectation { Assert::assertDirectoryIsReadable($this->value); return $this; } /** * Asserts that the value is a directory and is writable. */ public function toBeWritableDirectory(): Expectation { Assert::assertDirectoryIsWritable($this->value); return $this; } /** * Asserts that the value is a file. */ public function toBeFile(): Expectation { Assert::assertFileExists($this->value); return $this; } /** * Asserts that the value is a file and is readable. */ public function toBeReadableFile(): Expectation { Assert::assertFileIsReadable($this->value); return $this; } /** * Asserts that the value is a file and is writable. */ public function toBeWritableFile(): Expectation { Assert::assertFileIsWritable($this->value); return $this; } /** * Asserts that the value array matches the given array subset. * * @param array $array */ public function toMatchArray($array): Expectation { 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); Assert::assertEquals( $value, $valueAsArray[$key], sprintf( 'Failed asserting that an array has a key %s with the value %s.', $this->export($key), $this->export($valueAsArray[$key]), ), ); } return $this; } /** * Asserts that the value object matches a subset * of the properties of an given object. * * @param array|object $object */ public function toMatchObject($object): Expectation { foreach ((array) $object as $property => $value) { Assert::assertTrue(property_exists($this->value, $property)); /* @phpstan-ignore-next-line */ $propertyValue = $this->value->{$property}; Assert::assertEquals( $value, $propertyValue, sprintf( 'Failed asserting that an object has a property %s with the value %s.', $this->export($property), $this->export($propertyValue), ), ); } return $this; } /** * Asserts that the value matches a regular expression. */ public function toMatch(string $expression): Expectation { Assert::assertMatchesRegularExpression($expression, $this->value); return $this; } /** * Asserts that the value matches a constraint. */ public function toMatchConstraint(Constraint $constraint): Expectation { Assert::assertThat($this->value, $constraint); return $this; } /** * Asserts that executing value throws an exception. * * @param (Closure(Throwable): mixed)|string $exception */ public function toThrow($exception, string $exceptionMessage = null): Expectation { $callback = NullClosure::create(); if ($exception instanceof Closure) { $callback = $exception; $parameters = (new ReflectionFunction($exception))->getParameters(); if (1 !== count($parameters)) { 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) { // @phpstan-ignore-line if (!class_exists($exception)) { Assert::assertStringContainsString($exception, $e->getMessage()); return $this; } if ($exceptionMessage !== null) { Assert::assertStringContainsString($exceptionMessage, $e->getMessage()); } Assert::assertInstanceOf($exception, $e); $callback($e); return $this; } if (!class_exists($exception)) { throw new ExpectationFailedException("Exception with message \"{$exception}\" not thrown."); } throw new ExpectationFailedException("Exception \"{$exception}\" not thrown."); } /** * Exports the given value. * * @param mixed $value */ private function export($value): string { if ($this->exporter === null) { $this->exporter = new Exporter(); } return $this->exporter->export($value); } /** * Dynamically handle calls to the class or * creates a new higher order expectation. * * @param array $parameters * * @return HigherOrderExpectation|mixed */ public function __call(string $method, array $parameters) { if (!static::hasExtend($method)) { /* @phpstan-ignore-next-line */ return new HigherOrderExpectation($this, $this->value->$method(...$parameters)); } return $this->__extendsCall($method, $parameters); } /** * Dynamically calls methods on the class without any arguments * or creates a new higher order expectation. * * @return Expectation|HigherOrderExpectation */ public function __get(string $name) { if (!method_exists($this, $name) && !static::hasExtend($name)) { return new HigherOrderExpectation($this, $this->retrieve($name, $this->value)); } /* @phpstan-ignore-next-line */ return $this->{$name}(); } } } ================================================ FILE: src/test/resources/com/pestphp/pest/indexers/PestTestIndexTest/FileWithDescribeBlockTest.php ================================================ toBe(2); }); it('handles zero', function () { expect(0 + 0)->toBe(0); }); }); ================================================ FILE: src/test/resources/com/pestphp/pest/indexers/PestTestIndexTest/FileWithPestTest.php ================================================ assertTrue(true); }); ================================================ FILE: src/test/resources/com/pestphp/pest/indexers/PestTestIndexTest/FileWithPestTodosTest.php ================================================ toBe(42); Earth::tryCalculateQuestion(); expect($question)->toBeNull(); ================================================ FILE: src/test/resources/com/pestphp/pest/inspections/InvalidTestNameAndDatasetName.after.php ================================================ toBeTrue(); })->with('someDataSet'); ================================================ FILE: src/test/resources/com/pestphp/pest/inspections/InvalidTestNameAndDatasetName.php ================================================ 'NotSentenceCase', function () { expect(true)->toBeTrue(); })->with('someDataSet'); ================================================ FILE: src/test/resources/com/pestphp/pest/inspections/InvalidTestNameCase.after.php ================================================ assertTrue(true); }); it('is basic test', function () { $this->assertTrue(true); }); it('is.basic test', function () { $this->assertTrue(true); }); it('is::basic test', function () { $this->assertTrue(true); }); it('is basic.test 2', function () { $this->assertTrue(true); }); ================================================ FILE: src/test/resources/com/pestphp/pest/inspections/InvalidTestNameCase.php ================================================ 'basic_test', function () { $this->assertTrue(true); }); it('is_basic_test', function () { $this->assertTrue(true); }); it('is.basic_test', function () { $this->assertTrue(true); }); it('is::basic_test', function () { $this->assertTrue(true); }); it('isBasic._test2', function () { $this->assertTrue(true); }); ================================================ FILE: src/test/resources/com/pestphp/pest/inspections/ManyExpectCall.after.php ================================================ toBe(42) ->and($question)->toBeNull() ->and($question)->toBeNull() ->and($question)->toBeNull() ->and($question)->toBeNull() ->and($question)->toBeNull() ->and($question)->toBeNull(); ================================================ FILE: src/test/resources/com/pestphp/pest/inspections/ManyExpectCall.php ================================================ expect($answer)->toBe(42); expect($question)->toBeNull(); expect($question)->toBeNull(); expect($question)->toBeNull(); expect($question)->toBeNull(); expect($question)->toBeNull(); expect($question)->toBeNull(); ================================================ FILE: src/test/resources/com/pestphp/pest/inspections/MultipleExpectCall.after.php ================================================ toBe(42) ->and($question)->toBeNull(); ================================================ FILE: src/test/resources/com/pestphp/pest/inspections/MultipleExpectCall.php ================================================ expect($answer)->toBe(42); expect($question)->toBeNull(); ================================================ FILE: src/test/resources/com/pestphp/pest/inspections/MultipleExpectCallsWithOtherStatementsBetween.after.php ================================================ toBeInstanceOf(Mouse::class) ->and($answer)->toBe(42); Earth::tryCalculateQuestion(); expect($question)->toBeNull() ->and(Earth::exists())->toBeFalse(); ================================================ FILE: src/test/resources/com/pestphp/pest/inspections/MultipleExpectCallsWithOtherStatementsBetween.php ================================================ expect(Earth::owner())->toBeInstanceOf(Mouse::class); expect($answer)->toBe(42); Earth::tryCalculateQuestion(); expect($question)->toBeNull(); expect(Earth::exists())->toBeFalse(); ================================================ FILE: src/test/resources/com/pestphp/pest/inspections/SingleExpectCall.php ================================================ toBe(42); ================================================ FILE: src/test/resources/com/pestphp/pest/inspections/ValidHigherOrderTestNameCase.php ================================================ expect(true) ->toBeTrue(); ================================================ FILE: src/test/resources/com/pestphp/pest/inspections/ValidItTestNameWithoutSpaces.php ================================================ assertTrue(true); }); ================================================ FILE: src/test/resources/com/pestphp/pest/inspections/ValidTestNameCase.php ================================================ assertTrue(true); }); test('basic.test', function () { $this->assertTrue(true); }); test('basic::test', function () { $this->assertTrue(true); }); ================================================ FILE: src/test/resources/com/pestphp/pest/inspections/ValidTestNameWhenWrongCasingOnOneWord.php ================================================ assertTrue(true); }); test('can be used with snake_case or UPPER_CASE', function () { $this->assertTrue(true); }); test('can "quote_out" the word', function () { $this->assertTrue(true); }); ================================================ FILE: src/test/resources/com/pestphp/pest/inspections/assertionCanBeSimplified/ToBeWithFalse.after.php ================================================ toBeFalse(); expect(true)->toBeFalse("message"); expect(true)->toBeFalse(message: "message"); }); ================================================ FILE: src/test/resources/com/pestphp/pest/inspections/assertionCanBeSimplified/ToBeWithFalse.php ================================================ toBe(false); expect(true)->toBe(false, "message"); expect(true)->toBe(expected: false, message: "message"); }); ================================================ FILE: src/test/resources/com/pestphp/pest/inspections/assertionCanBeSimplified/ToBeWithNull.after.php ================================================ toBeNull(); expect(true)->toBeNull("message"); expect(true)->toBeNull(message: "message"); }); ================================================ FILE: src/test/resources/com/pestphp/pest/inspections/assertionCanBeSimplified/ToBeWithNull.php ================================================ toBe(null); expect(true)->toBe(null, "message"); expect(true)->toBe(expected: null, message: "message"); }); ================================================ FILE: src/test/resources/com/pestphp/pest/inspections/assertionCanBeSimplified/ToBeWithTrue.after.php ================================================ toBeTrue(); expect(true)->toBeTrue("message"); expect(true)->toBeTrue(message: "message"); expect(true)->toBeTrue(); }); ================================================ FILE: src/test/resources/com/pestphp/pest/inspections/assertionCanBeSimplified/ToBeWithTrue.php ================================================ toBe(true); expect(true)->toBe(true, "message"); expect(true)->toBe(expected: true, message: "message"); expect(true)->toBe(TrUe); }); ================================================ FILE: src/test/resources/com/pestphp/pest/inspections/assertionCanBeSimplified/ToHaveCountWithZero.after.php ================================================ toBeEmpty(); expect([1])->toBeEmpty("message"); expect([1])->toBeEmpty(message: "message"); }); ================================================ FILE: src/test/resources/com/pestphp/pest/inspections/assertionCanBeSimplified/ToHaveCountWithZero.php ================================================ toHaveCount(0); expect([1])->toHaveCount(0, "message"); expect([1])->toHaveCount(count: 0, message: "message"); }); ================================================ FILE: src/test/resources/com/pestphp/pest/inspections/pestTestFailedLine/AnonymousFunction.php ================================================ myAssert(function () { echo 1; expect(1)->toBe(2); }); }); function myAssert(callable $a) { $a(); } ================================================ FILE: src/test/resources/com/pestphp/pest/inspections/pestTestFailedLine/FailedOneLine.php ================================================ toBe(1); expect(1)->toBe(2); expect(2)->toBe(2); }); ================================================ FILE: src/test/resources/com/pestphp/pest/inspections/pestTestFailedLine/LambdaFunction.php ================================================ myAssert( fn() => expect(1)->toBe(2) ); }); function myAssert(callable $a) { $a(); } ================================================ FILE: src/test/resources/com/pestphp/pest/inspections/pestTestFailedLine/MismatchLine.php ================================================ toBe(1); expect(1)->toBe(2); expect(2)->toBe(2); }); ================================================ FILE: src/test/resources/com/pestphp/pest/inspections/pestTestFailedLine/MultipleStatementsInOneLine.php ================================================ expect(1)->toBe(1); expect(1)->toBe(2); }); ================================================ FILE: src/test/resources/com/pestphp/pest/inspections/pestTestFailedLine/OutRange.php ================================================ toBe(1); expect(1)->toBe(2); expect(2)->toBe(2); }); ================================================ FILE: src/test/resources/com/pestphp/pest/inspections/pestTestFailedLine/SingleLeafElementReported.php ================================================ myAssert(function () { echo 1; expect(1)->toBe(2); } ); }); function myAssert(callable $a) { $a(); } ================================================ FILE: src/test/resources/com/pestphp/pest/inspections/pestTestFailedLine/TypeBefore.php ================================================ toBe(1); expect(1)->toBe(2); expect(2)->toBe(2); }); ================================================ FILE: src/test/resources/com/pestphp/pest/inspections/pestTestFailedLine/TypeInside.php ================================================ toBe(1); expect(1)->toBe(2); expect(2)->toBe(2); }); ================================================ FILE: src/test/resources/com/pestphp/pest/inspections/pestTestFailedLine/WithDataSet.php ================================================ toBe($a); })->with([[1], [2], [3]]); ================================================ FILE: src/test/resources/com/pestphp/pest/inspections/pestTestFailedLine/WithDataSetAndKeys.php ================================================ expect(1)->toBe($a); })->with(["first dataset" => [1], "second dataset" => [2], "third dataset" => [3]]); ================================================ FILE: src/test/resources/com/pestphp/pest/inspections/pestTestFailedLine/WithDataSetAndSeveralFails.php ================================================ expect(1)->toBe($a); expect(2)->toBe($a); })->with(["first dataset" => [1, 1], "second dataset" => [2, 2]]); ================================================ FILE: src/test/resources/com/pestphp/pest/inspections/pestTestFailedLine/WithNamedDataSet.php ================================================ toBe($a); })->with("my dataset"); dataset("my dataset", [[1], [2], [3]]); ================================================ FILE: src/test/resources/com/pestphp/pest/inspections/phpstorm/MultipleClassesDeclarationsInPestFileTest.php ================================================ toBeInstanceOf(TestingClass::class); }); class TestingClass { // } class AnotherTestingClass { // } ================================================ FILE: src/test/resources/com/pestphp/pest/inspections/screenshotProject/tests/.pest/snapshots/Feature/ScreenshotSnapshot/it_browser_testing.snap ================================================ ================================================ FILE: src/test/resources/com/pestphp/pest/inspections/screenshotProject/tests/.pest/snapshots/Feature/ScreenshotSnapshotComplexName/it_1__2_3_4_.snap ================================================ ================================================ FILE: src/test/resources/com/pestphp/pest/inspections/screenshotProject/tests/.pest/snapshots/Feature/ScreenshotSnapshotMultiple/it_test.snap ================================================ ================================================ FILE: src/test/resources/com/pestphp/pest/inspections/screenshotProject/tests/.pest/snapshots/Feature/ScreenshotSnapshotMultiple/it_test2.snap ================================================ ================================================ FILE: src/test/resources/com/pestphp/pest/inspections/screenshotProject/tests/.pest/snapshots/Feature/nested/ScreenshotSnapshotNested/nested.snap ================================================ ================================================ FILE: src/test/resources/com/pestphp/pest/inspections/screenshotProject/tests/Feature/MissingScreenshotSnapshot.php ================================================ on()->mobile(); $page->assertScreenshotMatches(); }); ================================================ FILE: src/test/resources/com/pestphp/pest/inspections/screenshotProject/tests/Feature/MissingScreenshotSnapshotComplexName.php ================================================ assertScreenshotMatches(); }); ================================================ FILE: src/test/resources/com/pestphp/pest/inspections/screenshotProject/tests/Feature/MissingScreenshotSnapshotMultiple.php ================================================ assertScreenshotMatches(); }); it('test', function () { $page = visit('/'); $page->assertScreenshotMatches(); $page2 = visit('/home'); $page2->assertScreenshotMatches(); }); ================================================ FILE: src/test/resources/com/pestphp/pest/inspections/screenshotProject/tests/Feature/ScreenshotSnapshot.php ================================================ on()->mobile(); $page->assertScreenshotMatches(); }); ================================================ FILE: src/test/resources/com/pestphp/pest/inspections/screenshotProject/tests/Feature/ScreenshotSnapshotComplexName.php ================================================ assertScreenshotMatches(); }); ================================================ FILE: src/test/resources/com/pestphp/pest/inspections/screenshotProject/tests/Feature/ScreenshotSnapshotMultiple.php ================================================ assertScreenshotMatches(); }); it('test', function () { $page = visit('/'); $page->assertScreenshotMatches(); $page2 = visit('/home'); $page2->assertScreenshotMatches(); }); ================================================ FILE: src/test/resources/com/pestphp/pest/inspections/screenshotProject/tests/Feature/nested/MissingScreenshotSnapshotNested.php ================================================ on()->mobile(); $page->assertScreenshotMatches(); }); ================================================ FILE: src/test/resources/com/pestphp/pest/inspections/screenshotProject/tests/Feature/nested/ScreenshotSnapshotNested.php ================================================ assertScreenshotMatches(); }); ================================================ FILE: src/test/resources/com/pestphp/pest/runner/pestTestStacktraceParser/Multiline.php ================================================ toBe(2); } ================================================ FILE: src/test/resources/com/pestphp/pest/runner/pestTestStacktraceParser/OneLine.php ================================================ toBe(2); }); ================================================ FILE: src/test/resources/com/pestphp/pest/runner/pestTestStacktraceParser/OneLineRemote.php ================================================ toBe(2); }); ================================================ FILE: src/test/resources/com/pestphp/pest/runner/pestTestStacktraceParser/OutRangeLineNumber.php ================================================ toBe(2); }); ================================================ FILE: src/test/resources/com/pestphp/pest/runner/pestTestStacktraceParser/WrongLineNumber.php ================================================ toBe(2); }); ================================================ FILE: src/test/resources/com/pestphp/pest/snapshotTesting/allSnapshotAssertions.php ================================================ value = $value; } /** * Creates a new expectation. * * @param mixed $value */ public function and($value): Expectation { return new self($value); } /** * Creates the opposite expectation for the value. */ public function not(): Expectation { return new Expectation($this); } /** * Asserts that two variables have the same type and * value. Used on objects, it asserts that two * variables reference the same object. * * @param mixed $expected */ public function toBe($expected): Expectation { Assert::assertSame($expected, $this->value); return $this; } /** * Asserts that the value is empty. */ public function toBeEmpty(): Expectation { Assert::assertEmpty($this->value); return $this; } /** * Asserts that the value is true. */ public function toBeTrue(): Expectation { Assert::assertTrue($this->value); return $this; } /** * Asserts that the value is false. */ public function toBeFalse(): Expectation { Assert::assertFalse($this->value); return $this; } /** * Asserts that the value is greater than $expected. * * @param int|float $expected */ public function toBeGreaterThan($expected): Expectation { Assert::assertGreaterThan($expected, $this->value); return $this; } /** * Asserts that the value is greater than or equal to $expected. * * @param int|float $expected */ public function toBeGreaterThanOrEqual($expected): Expectation { Assert::assertGreaterThanOrEqual($expected, $this->value); return $this; } /** * Asserts that the value is less than or equal to $expected. * * @param int|float $expected */ public function toBeLessThan($expected): Expectation { Assert::assertLessThan($expected, $this->value); return $this; } /** * Asserts that the value is less than $expected. * * @param int|float $expected */ public function toBeLessThanOrEqual($expected): Expectation { Assert::assertLessThanOrEqual($expected, $this->value); return $this; } /** * Asserts that $needle is an element of the value. * * @param mixed $needle */ public function toContain($needle): Expectation { if (is_string($this->value)) { Assert::assertStringContainsString($needle, $this->value); } else { Assert::assertContains($needle, $this->value); } return $this; } /** * Asserts that the value starts with $expected. */ public function toStartWith(string $expected): Expectation { Assert::assertStringStartsWith($expected, $this->value); return $this; } /** * Asserts that the value ends with $expected. */ public function toEndWith(string $expected): Expectation { Assert::assertStringEndsWith($expected, $this->value); return $this; } /** * Asserts that $count matches the number of elements of the value. */ public function toHaveCount(int $count): Expectation { Assert::assertCount($count, $this->value); return $this; } /** * Asserts that the value contains the property $name. * * @param mixed $value */ public function toHaveProperty(string $name, $value = null): Expectation { $this->toBeObject(); Assert::assertTrue(property_exists($this->value, $name)); if (func_num_args() > 1) { /* @phpstan-ignore-next-line */ Assert::assertEquals($value, $this->value->{$name}); } return $this; } /** * Asserts that two variables have the same value. * * @param mixed $expected */ public function toEqual($expected): Expectation { Assert::assertEquals($expected, $this->value); 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. * * @param mixed $expected */ public function toEqualCanonicalizing($expected): Expectation { Assert::assertEqualsCanonicalizing($expected, $this->value); return $this; } /** * Asserts that the absolute difference between the value and $expected * is lower than $delta. * * @param mixed $expected */ public function toEqualWithDelta($expected, float $delta): Expectation { Assert::assertEqualsWithDelta($expected, $this->value, $delta); return $this; } /** * Asserts that the value is infinite. */ public function toBeInfinite(): Expectation { Assert::assertInfinite($this->value); return $this; } /** * Asserts that the value is an instance of $class. * * @param string $class */ public function toBeInstanceOf($class): Expectation { /* @phpstan-ignore-next-line */ Assert::assertInstanceOf($class, $this->value); return $this; } /** * Asserts that the value is an array. */ public function toBeArray(): Expectation { Assert::assertIsArray($this->value); return $this; } /** * Asserts that the value is of type bool. */ public function toBeBool(): Expectation { Assert::assertIsBool($this->value); return $this; } /** * Asserts that the value is of type callable. */ public function toBeCallable(): Expectation { Assert::assertIsCallable($this->value); return $this; } /** * Asserts that the value is of type float. */ public function toBeFloat(): Expectation { Assert::assertIsFloat($this->value); return $this; } /** * Asserts that the value is of type int. */ public function toBeInt(): Expectation { Assert::assertIsInt($this->value); return $this; } /** * Asserts that the value is of type iterable. */ public function toBeIterable(): Expectation { Assert::assertIsIterable($this->value); return $this; } /** * Asserts that the value is of type numeric. */ public function toBeNumeric(): Expectation { Assert::assertIsNumeric($this->value); return $this; } /** * Asserts that the value is of type object. */ public function toBeObject(): Expectation { Assert::assertIsObject($this->value); return $this; } /** * Asserts that the value is of type resource. */ public function toBeResource(): Expectation { Assert::assertIsResource($this->value); return $this; } /** * Asserts that the value is of type scalar. */ public function toBeScalar(): Expectation { Assert::assertIsScalar($this->value); return $this; } /** * Asserts that the value is of type string. */ public function toBeString(): Expectation { Assert::assertIsString($this->value); return $this; } /** * Asserts that the value is a JSON string. */ public function toBeJson(): Expectation { Assert::assertIsString($this->value); Assert::assertJson($this->value); return $this; } /** * Asserts that the value is NAN. */ public function toBeNan(): Expectation { Assert::assertNan($this->value); return $this; } /** * Asserts that the value is null. */ public function toBeNull(): Expectation { Assert::assertNull($this->value); return $this; } /** * Asserts that the value array has the provided $key. * * @param string|int $key * @param mixed $value */ public function toHaveKey($key, $value = null): Expectation { if (is_object($this->value) && method_exists($this->value, 'toArray')) { $array = $this->value->toArray(); } else { $array = (array) $this->value; } Assert::assertArrayHasKey($key, $array); if (func_num_args() > 1) { Assert::assertEquals($value, $array[$key]); } return $this; } /** * Asserts that the value array has the provided $keys. * * @param array $keys */ public function toHaveKeys(array $keys): Expectation { foreach ($keys as $key) { $this->toHaveKey($key); } return $this; } /** * Asserts that the value is a directory. */ public function toBeDirectory(): Expectation { Assert::assertDirectoryExists($this->value); return $this; } /** * Asserts that the value is a directory and is readable. */ public function toBeReadableDirectory(): Expectation { Assert::assertDirectoryIsReadable($this->value); return $this; } /** * Asserts that the value is a directory and is writable. */ public function toBeWritableDirectory(): Expectation { Assert::assertDirectoryIsWritable($this->value); return $this; } /** * Asserts that the value is a file. */ public function toBeFile(): Expectation { Assert::assertFileExists($this->value); return $this; } /** * Asserts that the value is a file and is readable. */ public function toBeReadableFile(): Expectation { Assert::assertFileIsReadable($this->value); return $this; } /** * Asserts that the value is a file and is writable. */ public function toBeWritableFile(): Expectation { Assert::assertFileIsWritable($this->value); return $this; } /** * Asserts that the value array matches the given array subset. * * @param array $array */ public function toMatchArray($array): Expectation { 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); Assert::assertEquals( $value, $valueAsArray[$key], sprintf( 'Failed asserting that an array has a key %s with the value %s.', $this->export($key), $this->export($valueAsArray[$key]), ), ); } return $this; } /** * Asserts that the value object matches a subset * of the properties of an given object. * * @param array|object $object */ public function toMatchObject($object): Expectation { foreach ((array) $object as $property => $value) { Assert::assertTrue(property_exists($this->value, $property)); /* @phpstan-ignore-next-line */ $propertyValue = $this->value->{$property}; Assert::assertEquals( $value, $propertyValue, sprintf( 'Failed asserting that an object has a property %s with the value %s.', $this->export($property), $this->export($propertyValue), ), ); } return $this; } /** * Asserts that the value matches a regular expression. */ public function toMatch(string $expression): Expectation { Assert::assertMatchesRegularExpression($expression, $this->value); return $this; } /** * Asserts that the value matches a constraint. */ public function toMatchConstraint(Constraint $constraint): Expectation { Assert::assertThat($this->value, $constraint); return $this; } /** * Exports the given value. * * @param mixed $value */ private function export($value): string { if ($this->exporter === null) { $this->exporter = new Exporter(); } return $this->exporter->export($value); } /** * Dynamically calls methods on the class without any arguments. * * @return Expectation */ public function __get(string $name) { /* @phpstan-ignore-next-line */ return $this->{$name}(); } } } ================================================ FILE: src/test/resources/com/pestphp/pest/templates/describe.after.php ================================================ ================================================ FILE: src/test/resources/com/pestphp/pest/templates/it.after.php ================================================ ================================================ FILE: src/test/resources/com/pestphp/pest/types/TestCase.php ================================================ extend('someExtend', function ($min, $max) { return $this->toBeGreaterThanOrEqual($min) ->toBeLessThanOrEqual($max); }); test('numeric ranges', function () { expect(100)-> }); ================================================ FILE: src/test/resources/com/pestphp/pest/types/expect/expectCallCompletionChainedNotMethod.php ================================================ extend('someExtend', function ($min, $max) { return $this->toBeGreaterThanOrEqual($min) ->toBeLessThanOrEqual($max); }); test('numeric ranges', function () { expect(100)->toBeInt() ->not() -> }); ================================================ FILE: src/test/resources/com/pestphp/pest/types/expect/expectCallCompletionChainedNotProperty.php ================================================ extend('someExtend', function ($min, $max) { return $this->toBeGreaterThanOrEqual($min) ->toBeLessThanOrEqual($max); }); test('numeric ranges', function () { expect(100)->toBeInt() ->not -> }); ================================================ FILE: src/test/resources/com/pestphp/pest/types/expect/expectExtendCallOnNonExpectFunction.php ================================================ extend('toBeWithinRange', function ($min, $max) { return $this->toBeGreaterThanOrEqual($min) ->toBeLessThanOrEqual($max); }); test('numeric ranges', function () { notExpect(100)->toBeWithinRange(90, 110)->; }); ================================================ FILE: src/test/resources/com/pestphp/pest/types/expect/expectExtendReturnType.php ================================================ extend('someExtend', function ($min, $max) { return $this->toBeGreaterThanOrEqual($min) ->toBeLessThanOrEqual($max); }); test('numeric ranges', function () { expect(100)->someExtend(90, 110)->; }); ================================================ FILE: src/test/resources/com/pestphp/pest/types/expect/expectInvalidExtendNoReturnType.php ================================================ toNonExtendMethod(90, 110)->; }); ================================================ FILE: src/test/resources/com/pestphp/pest/types/expect/extendCallOnChainedExpectation.php ================================================ extend('toChained', function ($min, $max) { return $this->toBeGreaterThanOrEqual($min) ->toBeLessThanOrEqual($max); }); test('numeric ranges', function () { expect(100) ->toBe(100) ->toChained(90, 110)->; }); ================================================ FILE: src/test/resources/com/pestphp/pest/types/function/testTest.php ================================================ ================================================ FILE: src/test/resources/com/pestphp/pest/types/this/beforeEach.php ================================================ }); ================================================ FILE: src/test/resources/com/pestphp/pest/types/this/itShortLambdaTest.php ================================================ $this-> ); ================================================ FILE: src/test/resources/com/pestphp/pest/types/this/itTest.php ================================================ }); ================================================ FILE: src/test/resources/com/pestphp/pest/types/this/testTest.php ================================================ }); ================================================ FILE: src/test/resources/com/pestphp/pest/types/thisField/afterEach.php ================================================ foo = new Foo(); }); afterEach('has home', function () { $this->foo-> }); ================================================ FILE: src/test/resources/com/pestphp/pest/types/thisField/afterEachNamespace.php ================================================ foo = new Foo(); }); afterEach('has home', function () { $this->foo-> }); ================================================ FILE: src/test/resources/com/pestphp/pest/types/thisField/beforeEach.php ================================================ foo = new Foo(); }); it('has home', function () { $this->foo-> }); ================================================ FILE: src/test/resources/com/pestphp/pest/types/thisField/beforeEachCompletion.php ================================================ foo = 1; }); it('has home', function () { $this-> }); ================================================ FILE: src/test/resources/com/pestphp/pest/types/thisField/beforeEachNamespace.php ================================================ foo = new Foo(); }); it('has home', function () { $this->foo-> }); ================================================ FILE: src/test/resources/com/pestphp/pest/types/thisField/beforeEachNamespaceCompletion.php ================================================ foo = 1; }); it('has home', function () { $this-> }); ================================================ FILE: src/test/resources/com/pestphp/pest/utilTests/ClassNameResolutionInNamespaceTest.php ================================================ assertTrue(true); }); ================================================ FILE: src/test/resources/com/pestphp/pest/utilTests/ClassNameResolutionTest.php ================================================ assertTrue(true); }); ================================================ FILE: src/test/resources/com/pestphp/pest/utilTests/SimpleTest.php ================================================ assertTrue(true); });