From 22fc6e17165e47e5347772aaeba4d07f20eb264d Mon Sep 17 00:00:00 2001 From: "Ilya.Kazakevich" Date: Mon, 19 Jun 2017 23:38:43 +0300 Subject: [PATCH] PY-24770: CleanUp and unify test class detection PythonUnitTestUtil been refactored, see its doc. Code that is not test-case specific moved to PyClassExt.kt New logic is the following: * UnitTest believes any TestCase inheritor is test case, so its method is test, its parent file is test and so on. * Other runners think that any "test_" function is test when it is located on toplevel or in test class (see PythonUnitTestUtil) --- .../com/jetbrains/extensions/PyClassExt.kt | 27 ++ .../testIntegration/PyTestFinder.java | 7 +- .../PyAttributeOutsideInitInspection.java | 3 +- .../PyBaseDocstringInspection.java | 7 +- .../field/PyIntroduceFieldHandler.java | 3 +- .../jetbrains/python/testing/PyNoseTest.kt | 5 +- .../com/jetbrains/python/testing/PyTest.kt | 3 +- .../jetbrains/python/testing/PyTestsShared.kt | 383 ++++++++++-------- .../jetbrains/python/testing/PyUnitTest.kt | 2 +- ...PythonTestLegacyConfigurationProducer.java | 27 +- .../python/testing/PythonUnitTestUtil.java | 228 ++++------- .../PythonNoseTestConfigurationProducer.java | 3 +- .../python/testing/pytest/PyTestUtil.java | 35 +- .../tests_folder/test_functions.py | 5 + .../CreateConfigurationMultipleCasesTask.java | 12 +- .../testing/CreateConfigurationTestTask.java | 20 +- .../python/testing/PythonUnitTestingTest.java | 12 +- 17 files changed, 428 insertions(+), 354 deletions(-) create mode 100644 python/src/com/jetbrains/extensions/PyClassExt.kt create mode 100644 python/testData/testRunner/env/createConfigurationTest/tests_folder/test_functions.py diff --git a/python/src/com/jetbrains/extensions/PyClassExt.kt b/python/src/com/jetbrains/extensions/PyClassExt.kt new file mode 100644 index 000000000000..997c890ffd0b --- /dev/null +++ b/python/src/com/jetbrains/extensions/PyClassExt.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * 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 + * + * http://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. + */ +package com.jetbrains.extensions + +import com.jetbrains.python.psi.PyClass +import com.jetbrains.python.psi.types.TypeEvalContext + +/** + * @author Ilya.Kazakevich + */ +fun PyClass.inherits(evalContext: TypeEvalContext, parentNames: Set) = + this.getAncestorTypes(evalContext).filterNotNull().map { it.classQName }.filterNotNull().any { parentNames.contains(it) } + +fun PyClass.inherits(evalContext: TypeEvalContext, vararg parentNames: String)= this.inherits(evalContext, parentNames.toHashSet()) \ No newline at end of file diff --git a/python/src/com/jetbrains/python/codeInsight/testIntegration/PyTestFinder.java b/python/src/com/jetbrains/python/codeInsight/testIntegration/PyTestFinder.java index 5a4577a83de2..c2722ef3941e 100644 --- a/python/src/com/jetbrains/python/codeInsight/testIntegration/PyTestFinder.java +++ b/python/src/com/jetbrains/python/codeInsight/testIntegration/PyTestFinder.java @@ -22,6 +22,7 @@ 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.ThreeState; import com.jetbrains.python.psi.PyClass; import com.jetbrains.python.psi.PyDocStringOwner; import com.jetbrains.python.psi.PyFunction; @@ -60,7 +61,7 @@ public class PyTestFinder implements TestFinder { for (String eachName : names) { if (eachName.contains(sourceName)) { for (PyClass eachClass : PyClassNameIndex.find(eachName, element.getProject(), GlobalSearchScope.projectScope(element.getProject()))) { - if (PythonUnitTestUtil.isTestCaseClass(eachClass, null) || PythonDocTestUtil.isDocTestClass(eachClass)) { + if (PythonUnitTestUtil.isTestClass(eachClass, ThreeState.UNSURE, null) || PythonDocTestUtil.isDocTestClass(eachClass)) { classesWithProximities.add( new Pair(eachClass, TestFinderHelper.calcTestNameProximity(sourceName, eachName))); } @@ -73,8 +74,8 @@ public class PyTestFinder implements TestFinder { for (String eachName : names) { if (eachName.contains(sourceName)) { for (PyFunction eachFunction : PyFunctionNameIndex.find(eachName, element.getProject(), GlobalSearchScope.projectScope(element.getProject()))) { - if (PythonUnitTestUtil.isTestCaseFunction( - eachFunction) || PythonDocTestUtil.isDocTestFunction(eachFunction)) { + if (PythonUnitTestUtil.isTestFunction( + eachFunction, ThreeState.UNSURE, null) || PythonDocTestUtil.isDocTestFunction(eachFunction)) { classesWithProximities.add( new Pair(eachFunction, TestFinderHelper.calcTestNameProximity(sourceName, eachName))); } diff --git a/python/src/com/jetbrains/python/inspections/PyAttributeOutsideInitInspection.java b/python/src/com/jetbrains/python/inspections/PyAttributeOutsideInitInspection.java index e2813766b466..83b9c14a3098 100644 --- a/python/src/com/jetbrains/python/inspections/PyAttributeOutsideInitInspection.java +++ b/python/src/com/jetbrains/python/inspections/PyAttributeOutsideInitInspection.java @@ -18,6 +18,7 @@ package com.jetbrains.python.inspections; import com.intellij.codeInspection.LocalInspectionToolSession; import com.intellij.codeInspection.ProblemsHolder; import com.intellij.psi.PsiElementVisitor; +import com.intellij.util.ThreeState; import com.jetbrains.python.PyBundle; import com.jetbrains.python.PyNames; import com.jetbrains.python.inspections.quickfix.PyMoveAttributeToInitQuickFix; @@ -113,6 +114,6 @@ public class PyAttributeOutsideInitInspection extends PyInspection { } private static boolean isApplicable(@NotNull PyClass containingClass, @NotNull TypeEvalContext context) { - return !PythonUnitTestUtil.isUnitTestCaseClass(containingClass) && !containingClass.isSubclass("django.db.models.base.Model", context); + return !PythonUnitTestUtil.isTestClass(containingClass, ThreeState.UNSURE, context) && !containingClass.isSubclass("django.db.models.base.Model", context); } } diff --git a/python/src/com/jetbrains/python/inspections/PyBaseDocstringInspection.java b/python/src/com/jetbrains/python/inspections/PyBaseDocstringInspection.java index 68df5e219014..a486f814d596 100644 --- a/python/src/com/jetbrains/python/inspections/PyBaseDocstringInspection.java +++ b/python/src/com/jetbrains/python/inspections/PyBaseDocstringInspection.java @@ -17,6 +17,7 @@ package com.jetbrains.python.inspections; import com.intellij.codeInspection.LocalInspectionToolSession; import com.intellij.codeInspection.ProblemsHolder; +import com.intellij.util.ThreeState; import com.jetbrains.python.psi.*; import com.jetbrains.python.testing.PythonUnitTestUtil; import org.jetbrains.annotations.NotNull; @@ -42,9 +43,7 @@ public abstract class PyBaseDocstringInspection extends PyInspection { @Override public final void visitPyFunction(@NotNull PyFunction node) { - if (PythonUnitTestUtil.isUnitTestCaseFunction(node)) return; - final PyClass containingClass = node.getContainingClass(); - if (containingClass != null && PythonUnitTestUtil.isUnitTestCaseClass(containingClass)) return; + if (PythonUnitTestUtil.isTestFunction(node, ThreeState.UNSURE, myTypeEvalContext)) return; final Property property = node.getProperty(); if (property != null && (node == property.getSetter().valueOrNull() || node == property.getDeleter().valueOrNull())) { return; @@ -55,7 +54,7 @@ public abstract class PyBaseDocstringInspection extends PyInspection { @Override public final void visitPyClass(@NotNull PyClass node) { - if (PythonUnitTestUtil.isUnitTestCaseClass(node)) return; + if (PythonUnitTestUtil.isTestClass(node, ThreeState.UNSURE, myTypeEvalContext)) return; final String name = node.getName(); if (name == null || name.startsWith("_")) { return; diff --git a/python/src/com/jetbrains/python/refactoring/introduce/field/PyIntroduceFieldHandler.java b/python/src/com/jetbrains/python/refactoring/introduce/field/PyIntroduceFieldHandler.java index 7c2ca49c4677..daf919e0cb2a 100644 --- a/python/src/com/jetbrains/python/refactoring/introduce/field/PyIntroduceFieldHandler.java +++ b/python/src/com/jetbrains/python/refactoring/introduce/field/PyIntroduceFieldHandler.java @@ -34,6 +34,7 @@ import com.intellij.refactoring.introduce.inplace.InplaceVariableIntroducer; import com.intellij.refactoring.util.CommonRefactoringUtil; import com.intellij.util.Function; import com.intellij.util.FunctionUtil; +import com.intellij.util.ThreeState; import com.jetbrains.python.PyNames; import com.jetbrains.python.codeInsight.controlflow.ScopeOwner; import com.jetbrains.python.codeInsight.dataflow.scope.ScopeUtil; @@ -88,7 +89,7 @@ public class PyIntroduceFieldHandler extends IntroduceHandler { } if (element1 != null) { final PyClass clazz = PyUtil.getContainingClassOrSelf(element1); - if (clazz != null && PythonUnitTestUtil.isTestCaseClass(clazz, null)) return true; + if (clazz != null && PythonUnitTestUtil.isTestClass(clazz, ThreeState.UNSURE, null)) return true; } return false; } diff --git a/python/src/com/jetbrains/python/testing/PyNoseTest.kt b/python/src/com/jetbrains/python/testing/PyNoseTest.kt index 9c90363ab6d3..60404e18756b 100644 --- a/python/src/com/jetbrains/python/testing/PyNoseTest.kt +++ b/python/src/com/jetbrains/python/testing/PyNoseTest.kt @@ -23,8 +23,6 @@ import com.intellij.execution.runners.ExecutionEnvironment import com.intellij.openapi.options.SettingsEditor import com.intellij.openapi.project.Project import com.jetbrains.python.PythonHelper -import com.jetbrains.python.testing.PythonTestConfigurationsModel -import com.jetbrains.python.testing.VFSTestFrameworkListener /** * Nose runner @@ -41,7 +39,8 @@ class PyNoseTestExecutionEnvironment(configuration: PyNoseTestConfiguration, env } -class PyNoseTestConfiguration(project: Project, factory: PyNoseTestFactory) : PyAbstractTestConfiguration(project, factory) { +class PyNoseTestConfiguration(project: Project, factory: PyNoseTestFactory) : + PyAbstractTestConfiguration(project, factory, PythonTestConfigurationsModel.PYTHONS_NOSETEST_NAME) { @ConfigField var regexPattern = "" diff --git a/python/src/com/jetbrains/python/testing/PyTest.kt b/python/src/com/jetbrains/python/testing/PyTest.kt index 4bd2d3b90f5a..b5dfccee66cf 100644 --- a/python/src/com/jetbrains/python/testing/PyTest.kt +++ b/python/src/com/jetbrains/python/testing/PyTest.kt @@ -37,7 +37,8 @@ class PyPyTestExecutionEnvironment(configuration: PyTestConfiguration, environme } -class PyTestConfiguration(project: Project, factory: PyTestFactory) : PyAbstractTestConfiguration(project, factory) { +class PyTestConfiguration(project: Project, factory: PyTestFactory) + : PyAbstractTestConfiguration(project, factory, PythonTestConfigurationsModel.PY_TEST_NAME) { @ConfigField var keywords = "" diff --git a/python/src/com/jetbrains/python/testing/PyTestsShared.kt b/python/src/com/jetbrains/python/testing/PyTestsShared.kt index b1ee4474ec2e..8339e89dfafb 100644 --- a/python/src/com/jetbrains/python/testing/PyTestsShared.kt +++ b/python/src/com/jetbrains/python/testing/PyTestsShared.kt @@ -17,40 +17,85 @@ package com.jetbrains.python.testing +import com.intellij.execution.ExecutionException import com.intellij.execution.Location +import com.intellij.execution.PsiLocation +import com.intellij.execution.RunnerAndConfigurationSettings +import com.intellij.execution.actions.ConfigurationContext import com.intellij.execution.actions.ConfigurationFromContext +import com.intellij.execution.configurations.ConfigurationFactory +import com.intellij.execution.configurations.GeneralCommandLine +import com.intellij.execution.configurations.RefactoringListenerProvider +import com.intellij.execution.configurations.RuntimeConfigurationWarning +import com.intellij.execution.runners.ExecutionEnvironment import com.intellij.execution.testframework.AbstractTestProxy +import com.intellij.execution.testframework.sm.runner.SMTestLocator +import com.intellij.openapi.module.Module +import com.intellij.openapi.module.impl.scopes.ModuleWithDependenciesScope +import com.intellij.openapi.options.SettingsEditor +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.JDOMExternalizerUtil.readField +import com.intellij.openapi.util.JDOMExternalizerUtil.writeField +import com.intellij.openapi.util.Pair +import com.intellij.openapi.util.Ref import com.intellij.openapi.vfs.LocalFileSystem +import com.intellij.openapi.vfs.VfsUtil +import com.intellij.openapi.vfs.VirtualFile import com.intellij.psi.PsiDirectory import com.intellij.psi.PsiElement +import com.intellij.psi.PsiFileSystemItem +import com.intellij.psi.PsiNamedElement +import com.intellij.psi.search.GlobalSearchScope +import com.intellij.psi.util.PsiTreeUtil +import com.intellij.psi.util.QualifiedName +import com.intellij.refactoring.listeners.RefactoringElementListener +import com.intellij.refactoring.listeners.UndoRefactoringElementAdapter +import com.intellij.util.ThreeState import com.jetbrains.extensions.getQName +import com.jetbrains.extenstions.QNameResolveContext import com.jetbrains.extenstions.getElementAndResolvableName import com.jetbrains.extenstions.resolveToElement +import com.jetbrains.python.PyBundle +import com.jetbrains.python.psi.PyClass import com.jetbrains.python.psi.PyFile +import com.jetbrains.python.psi.PyFunction +import com.jetbrains.python.psi.PyQualifiedNameOwner import com.jetbrains.python.psi.types.TypeEvalContext +import com.jetbrains.python.run.AbstractPythonRunConfiguration +import com.jetbrains.python.run.CommandLinePatcher +import com.jetbrains.python.run.PythonConfigurationFactoryBase import com.jetbrains.python.run.PythonRunConfiguration +import com.jetbrains.reflection.DelegationProperty +import com.jetbrains.reflection.Properties +import com.jetbrains.reflection.Property +import com.jetbrains.reflection.getProperties /** * New configuration factories */ -val factories: Array = arrayOf( - com.jetbrains.python.testing.PyUnitTestFactory, - com.jetbrains.python.testing.PyTestFactory, - com.jetbrains.python.testing.PyNoseTestFactory) +val factories: Array = arrayOf( + PyUnitTestFactory, + PyTestFactory, + PyNoseTestFactory) internal fun getAdditionalArgumentsPropertyName() = com.jetbrains.python.testing.PyAbstractTestConfiguration::additionalArguments.name +/** + * If runner name is here that means test runner only can run inheritors for TestCase + */ +val RunnersThatRequireTestCaseClass = setOf(PythonTestConfigurationsModel.PYTHONS_UNITTEST_NAME) + /** * Since runners report names of tests as qualified name, no need to convert it to PSI and back to string. * We just save its name and provide it again to rerun * TODO: Doc derived problem */ -private class PyTargetBasedPsiLocation(val target: com.jetbrains.python.testing.ConfigurationTarget, - element: com.intellij.psi.PsiElement) : com.intellij.execution.PsiLocation(element) { +private class PyTargetBasedPsiLocation(val target: ConfigurationTarget, + element: PsiElement) : PsiLocation(element) { override fun equals(other: Any?): Boolean { - if (other is com.jetbrains.python.testing.PyTargetBasedPsiLocation) { + if (other is PyTargetBasedPsiLocation) { return target == other.target } return false @@ -65,9 +110,9 @@ private class PyTargetBasedPsiLocation(val target: com.jetbrains.python.testing. /** * @return factory chosen by user in "test runner" settings */ -private fun findConfigurationFactoryFromSettings(module: com.intellij.openapi.module.Module): com.intellij.execution.configurations.ConfigurationFactory { - val name = com.jetbrains.python.testing.TestRunnerService.getInstance(module).projectConfiguration - val factories = com.jetbrains.python.testing.PythonTestConfigurationType.getInstance().configurationFactories +private fun findConfigurationFactoryFromSettings(module: Module): ConfigurationFactory { + val name = TestRunnerService.getInstance(module).projectConfiguration + val factories = PythonTestConfigurationType.getInstance().configurationFactories val configurationFactory = factories.find { it.name == name } return configurationFactory ?: factories.first() } @@ -76,41 +121,41 @@ private fun findConfigurationFactoryFromSettings(module: com.intellij.openapi.mo // folder provided by python side. Resolve test names versus it private val PATH_URL = java.util.regex.Pattern.compile("^python<([^<>]+)>$") -object PyTestsLocator : com.intellij.execution.testframework.sm.runner.SMTestLocator { +object PyTestsLocator : SMTestLocator { override fun getLocation(protocol: String, path: String, - project: com.intellij.openapi.project.Project, - scope: com.intellij.psi.search.GlobalSearchScope): List> { - if (scope !is com.intellij.openapi.module.impl.scopes.ModuleWithDependenciesScope) { + project: Project, + scope: GlobalSearchScope): List> { + if (scope !is ModuleWithDependenciesScope) { return listOf() } - val matcher = com.jetbrains.python.testing.PATH_URL.matcher(protocol) + val matcher = PATH_URL.matcher(protocol) val folder = if (matcher.matches()) { - com.intellij.openapi.vfs.LocalFileSystem.getInstance().findFileByPath(matcher.group(1)) + LocalFileSystem.getInstance().findFileByPath(matcher.group(1)) } else { null } //TODO: Doc we will not bae able to resolve if different SDK - val qualifiedName = com.intellij.psi.util.QualifiedName.fromDottedString(path) + val qualifiedName = QualifiedName.fromDottedString(path) // Assume qname id good and resolve it directly - val element = qualifiedName.resolveToElement(com.jetbrains.extenstions.QNameResolveContext(scope.module, - evalContext = TypeEvalContext.codeAnalysis( - project, - null), - folderToStart = folder, - allowInaccurateResult = true)) + val element = qualifiedName.resolveToElement(QNameResolveContext(scope.module, + evalContext = TypeEvalContext.codeAnalysis( + project, + null), + folderToStart = folder, + allowInaccurateResult = true)) if (element != null) { // Path is qualified name of python test according to runners protocol // Parentheses are part of generators / parametrized tests // Until https://github.com/JetBrains/teamcity-messages/issues/121 they are disabled, // so we cut them out of path not to provide unsupported targets to runners - val pathNoParentheses = com.intellij.psi.util.QualifiedName.fromComponents( + val pathNoParentheses = QualifiedName.fromComponents( qualifiedName.components.filter { !it.contains('(') }).toString() return listOf( - com.jetbrains.python.testing.PyTargetBasedPsiLocation(ConfigurationTarget(pathNoParentheses, TestTargetType.PYTHON), element)) + PyTargetBasedPsiLocation(ConfigurationTarget(pathNoParentheses, TestTargetType.PYTHON), element)) } else { return listOf() @@ -118,15 +163,15 @@ object PyTestsLocator : com.intellij.execution.testframework.sm.runner.SMTestLoc } } -abstract class PyTestExecutionEnvironment(configuration: T, - environment: com.intellij.execution.runners.ExecutionEnvironment) - : com.jetbrains.python.testing.PythonTestCommandLineStateBase(configuration, environment) { +abstract class PyTestExecutionEnvironment(configuration: T, + environment: ExecutionEnvironment) + : PythonTestCommandLineStateBase(configuration, environment) { - override fun getTestLocator(): com.intellij.execution.testframework.sm.runner.SMTestLocator = com.jetbrains.python.testing.PyTestsLocator + override fun getTestLocator(): SMTestLocator = PyTestsLocator override fun getTestSpecs(): MutableList = java.util.ArrayList(configuration.getTestSpec()) - override fun generateCommandLine(patchers: Array?): com.intellij.execution.configurations.GeneralCommandLine { + override fun generateCommandLine(patchers: Array?): GeneralCommandLine { val line = super.generateCommandLine(patchers) line.workDirectory = java.io.File(configuration.workingDirectorySafe) return line @@ -134,19 +179,19 @@ abstract class PyTestExecutionEnvironment() { +abstract class PyAbstractTestSettingsEditor(private val sharedForm: PyTestSharedForm) + : SettingsEditor() { - override fun resetEditorFrom(s: com.jetbrains.python.testing.PyAbstractTestConfiguration) { + override fun resetEditorFrom(s: PyAbstractTestConfiguration) { // usePojoProperties is true because we know that Form is java-based - com.jetbrains.python.run.AbstractPythonRunConfiguration.copyParams(s, sharedForm.optionsForm) - s.copyTo(com.jetbrains.reflection.getProperties(sharedForm, usePojoProperties = true)) + AbstractPythonRunConfiguration.copyParams(s, sharedForm.optionsForm) + s.copyTo(getProperties(sharedForm, usePojoProperties = true)) } - override fun applyEditorTo(s: com.jetbrains.python.testing.PyAbstractTestConfiguration) { - com.jetbrains.python.run.AbstractPythonRunConfiguration.copyParams(sharedForm.optionsForm, s) - s.copyFrom(com.jetbrains.reflection.getProperties(sharedForm, usePojoProperties = true)) + override fun applyEditorTo(s: PyAbstractTestConfiguration) { + AbstractPythonRunConfiguration.copyParams(sharedForm.optionsForm, s) + s.copyFrom(getProperties(sharedForm, usePojoProperties = true)) } override fun createEditor(): javax.swing.JComponent = sharedForm.panel @@ -164,9 +209,9 @@ private val DEFAULT_PATH = "" /** * Target depends on target type. It could be path to file/folder or python target */ -data class ConfigurationTarget(@com.jetbrains.python.testing.ConfigField var target: String, - @com.jetbrains.python.testing.ConfigField var targetType: com.jetbrains.python.testing.TestTargetType) { - fun copyTo(dst: com.jetbrains.python.testing.ConfigurationTarget) { +data class ConfigurationTarget(@ConfigField var target: String, + @ConfigField var targetType: TestTargetType) { + fun copyTo(dst: ConfigurationTarget) { // TODO: do we have such method it in Kotlin? dst.target = target dst.targetType = targetType @@ -176,21 +221,21 @@ data class ConfigurationTarget(@com.jetbrains.python.testing.ConfigField var tar * Validates configuration and throws exception if target is invalid */ fun checkValid() { - if (targetType != com.jetbrains.python.testing.TestTargetType.CUSTOM && target.isEmpty()) { - throw com.intellij.execution.configurations.RuntimeConfigurationWarning("Target should be set for anything but custom") + if (targetType != TestTargetType.CUSTOM && target.isEmpty()) { + throw RuntimeConfigurationWarning("Target should be set for anything but custom") } } /** * Converts target to PSI element if possible resolving it against roots and working directory */ - fun asPsiElement(configuration: com.jetbrains.python.testing.PyAbstractTestConfiguration): com.intellij.psi.PsiElement? { - if (targetType == com.jetbrains.python.testing.TestTargetType.PYTHON) { + fun asPsiElement(configuration: PyAbstractTestConfiguration): PsiElement? { + if (targetType == TestTargetType.PYTHON) { val module = configuration.module ?: return null - val context = com.jetbrains.python.psi.types.TypeEvalContext.userInitiated(configuration.project, null) + val context = TypeEvalContext.userInitiated(configuration.project, null) val workDir = configuration.getWorkingDirectoryAsVirtual() - val name = com.intellij.psi.util.QualifiedName.fromDottedString(target) - return name.resolveToElement(com.jetbrains.extenstions.QNameResolveContext(module, configuration.sdk, context, workDir, true)) + val name = QualifiedName.fromDottedString(target) + return name.resolveToElement(QNameResolveContext(module, configuration.sdk, context, workDir, true)) } return null } @@ -198,40 +243,40 @@ data class ConfigurationTarget(@com.jetbrains.python.testing.ConfigField var tar /** * Converts target to file if possible */ - fun asVirtualFile(): com.intellij.openapi.vfs.VirtualFile? { - if (targetType == com.jetbrains.python.testing.TestTargetType.PATH) { - return com.intellij.openapi.vfs.LocalFileSystem.getInstance().findFileByPath(target) + fun asVirtualFile(): VirtualFile? { + if (targetType == TestTargetType.PATH) { + return LocalFileSystem.getInstance().findFileByPath(target) } return null } - fun generateArgumentsLine(configuration: com.jetbrains.python.testing.PyAbstractTestConfiguration): List = + fun generateArgumentsLine(configuration: PyAbstractTestConfiguration): List = when (targetType) { - com.jetbrains.python.testing.TestTargetType.CUSTOM -> emptyList() - com.jetbrains.python.testing.TestTargetType.PYTHON -> getArgumentsForPythonTarget(configuration) - com.jetbrains.python.testing.TestTargetType.PATH -> listOf("--path", target.trim()) + TestTargetType.CUSTOM -> emptyList() + TestTargetType.PYTHON -> getArgumentsForPythonTarget(configuration) + TestTargetType.PATH -> listOf("--path", target.trim()) } - private fun getArgumentsForPythonTarget(configuration: com.jetbrains.python.testing.PyAbstractTestConfiguration): List { + private fun getArgumentsForPythonTarget(configuration: PyAbstractTestConfiguration): List { val element = asPsiElement(configuration) ?: - throw com.intellij.execution.ExecutionException( + throw ExecutionException( "Can't resolve $target. Try to remove configuration and generate is again") - if (element is com.intellij.psi.PsiDirectory) { + if (element is PsiDirectory) { // Directory is special case: we can't run it as package for now, so we run it as path return listOf("--path", element.virtualFile.path) } - val context = com.jetbrains.python.psi.types.TypeEvalContext.userInitiated(configuration.project, null) - val qNameResolveContext = com.jetbrains.extenstions.QNameResolveContext( + val context = TypeEvalContext.userInitiated(configuration.project, null) + val qNameResolveContext = QNameResolveContext( module = configuration.module!!, evalContext = context, folderToStart = LocalFileSystem.getInstance().findFileByPath(configuration.workingDirectorySafe), allowInaccurateResult = true ) - val qualifiedNameParts = com.intellij.psi.util.QualifiedName.fromDottedString(target.trim()).tryResolveAndSplit(qNameResolveContext) ?: - throw com.intellij.execution.ExecutionException("Can't find file where $target declared. " + - "Make sure it is in project root") + val qualifiedNameParts = QualifiedName.fromDottedString(target.trim()).tryResolveAndSplit(qNameResolveContext) ?: + throw ExecutionException("Can't find file where $target declared. " + + "Make sure it is in project root") // We can't provide element qname here: it may point to parent class in case of inherited functions, // so we make fix file part, but obey element(symbol) part of qname @@ -249,8 +294,8 @@ data class ConfigurationTarget(@com.jetbrains.python.testing.ConfigField var tar return listOf("--target", elementAndName.name.toString()) } // Use "full" (path from closest root) otherwise - val name = (element.containingFile as? com.jetbrains.python.psi.PyFile)?.getQName()?.append(qualifiedNameParts.elementName) ?: - throw com.intellij.execution.ExecutionException( + val name = (element.containingFile as? PyFile)?.getQName()?.append(qualifiedNameParts.elementName) ?: + throw ExecutionException( "Can't get importable name for ${element.containingFile}. Is it a python file in project?") return listOf("--target", name.toString()) @@ -263,7 +308,7 @@ data class ConfigurationTarget(@com.jetbrains.python.testing.ConfigField var tar val elementFile = element.containingFile.virtualFile val workingDir = elementFile.fileSystem.findFileByPath(configuration.workingDirectorySafe) - val fileSystemPartOfTarget = (if (workingDir != null) com.intellij.openapi.vfs.VfsUtil.getRelativePath(elementFile, workingDir) + val fileSystemPartOfTarget = (if (workingDir != null) VfsUtil.getRelativePath(elementFile, workingDir) else null) ?: elementFile.path @@ -280,8 +325,8 @@ data class ConfigurationTarget(@com.jetbrains.python.testing.ConfigField var tar /** * @return directory which target is situated */ - fun getElementDirectory(configuration: com.jetbrains.python.testing.PyAbstractTestConfiguration): com.intellij.openapi.vfs.VirtualFile? { - if (target == com.jetbrains.python.testing.DEFAULT_PATH) { + fun getElementDirectory(configuration: PyAbstractTestConfiguration): VirtualFile? { + if (target == DEFAULT_PATH) { //This means "current directory", so we do not know where is it // getting vitualfile for it may return PyCharm working directory which is not what we want return null @@ -296,37 +341,35 @@ data class ConfigurationTarget(@com.jetbrains.python.testing.ConfigField var tar * To prevent legacy configuration options from clashing with new names, we add prefix * to use for writing/reading xml */ -private val com.jetbrains.reflection.Property.prefixedName: String +private val Property.prefixedName: String get() = "_new_" + this.getName() /** * Parent of all new test configurations. * All config-specific fields are implemented as properties. They are saved/restored automatically and passed to GUI form. * - * @param runBareFunctions if config supports running functions directly in modules or only class methods */ -abstract class PyAbstractTestConfiguration(project: com.intellij.openapi.project.Project, - configurationFactory: com.intellij.execution.configurations.ConfigurationFactory, - private val runBareFunctions: Boolean = true) - : com.jetbrains.python.testing.AbstractPythonTestRunConfiguration(project, - configurationFactory), com.jetbrains.python.testing.PyRerunAwareConfiguration, - com.intellij.execution.configurations.RefactoringListenerProvider { - @com.jetbrains.reflection.DelegationProperty - val target = com.jetbrains.python.testing.ConfigurationTarget(DEFAULT_PATH, TestTargetType.PATH) - @com.jetbrains.python.testing.ConfigField +abstract class PyAbstractTestConfiguration(project: Project, + configurationFactory: ConfigurationFactory, + private val runnerName: String) + : AbstractPythonTestRunConfiguration(project, configurationFactory), PyRerunAwareConfiguration, + RefactoringListenerProvider { + @DelegationProperty + val target = ConfigurationTarget(DEFAULT_PATH, TestTargetType.PATH) + @ConfigField var additionalArguments = "" val testFrameworkName = configurationFactory.name!! @Suppress("LeakingThis") // Legacy adapter is used to support legacy configs. Leak is ok here since everything takes place in one thread - @com.jetbrains.reflection.DelegationProperty - val legacyConfigurationAdapter = com.jetbrains.python.testing.PyTestLegacyConfigurationAdapter(this) + @DelegationProperty + val legacyConfigurationAdapter = PyTestLegacyConfigurationAdapter(this) /** * Renames working directory if folder physically renamed */ - private open inner class PyConfigurationRenamer(private val workingDirectoryFile: com.intellij.openapi.vfs.VirtualFile?) : com.intellij.refactoring.listeners.UndoRefactoringElementAdapter() { - override fun refactored(element: com.intellij.psi.PsiElement, oldQualifiedName: String?) { + private open inner class PyConfigurationRenamer(private val workingDirectoryFile: VirtualFile?) : UndoRefactoringElementAdapter() { + override fun refactored(element: PsiElement, oldQualifiedName: String?) { if (workingDirectoryFile != null) { workingDirectory = workingDirectoryFile.path } @@ -336,9 +379,9 @@ abstract class PyAbstractTestConfiguration(project: com.intellij.openapi.project /** * For real launch use [getWorkingDirectorySafe] instead */ - internal fun getWorkingDirectoryAsVirtual(): com.intellij.openapi.vfs.VirtualFile? { + internal fun getWorkingDirectoryAsVirtual(): VirtualFile? { if (!workingDirectory.isNullOrEmpty()) { - return com.intellij.openapi.vfs.LocalFileSystem.getInstance().findFileByPath(workingDirectory) + return LocalFileSystem.getInstance().findFileByPath(workingDirectory) } return null } @@ -355,15 +398,15 @@ abstract class PyAbstractTestConfiguration(project: com.intellij.openapi.project /** * Renames python target if python symbol, module or folder renamed */ - private inner class PyElementTargetRenamer(private val originalElement: com.intellij.psi.PsiElement, - workingDirectoryFile: com.intellij.openapi.vfs.VirtualFile?) : - com.jetbrains.python.testing.PyAbstractTestConfiguration.PyConfigurationRenamer(workingDirectoryFile) { - override fun refactored(element: com.intellij.psi.PsiElement, oldQualifiedName: String?) { + private inner class PyElementTargetRenamer(private val originalElement: PsiElement, + workingDirectoryFile: VirtualFile?) : + PyAbstractTestConfiguration.PyConfigurationRenamer(workingDirectoryFile) { + override fun refactored(element: PsiElement, oldQualifiedName: String?) { super.refactored(element, oldQualifiedName) - if (originalElement is com.jetbrains.python.psi.PyQualifiedNameOwner) { + if (originalElement is PyQualifiedNameOwner) { target.target = originalElement.qualifiedName ?: return } - else if (originalElement is com.intellij.psi.PsiNamedElement) { + else if (originalElement is PsiNamedElement) { target.target = originalElement.name ?: return } } @@ -372,25 +415,25 @@ abstract class PyAbstractTestConfiguration(project: com.intellij.openapi.project /** * Renames folder target if file or folder really renamed */ - private inner class PyVirtualFileRenamer(private val virtualFile: com.intellij.openapi.vfs.VirtualFile, - workingDirectoryFile: com.intellij.openapi.vfs.VirtualFile?) : - com.jetbrains.python.testing.PyAbstractTestConfiguration.PyConfigurationRenamer(workingDirectoryFile) { - override fun refactored(element: com.intellij.psi.PsiElement, oldQualifiedName: String?) { + private inner class PyVirtualFileRenamer(private val virtualFile: VirtualFile, + workingDirectoryFile: VirtualFile?) : + PyAbstractTestConfiguration.PyConfigurationRenamer(workingDirectoryFile) { + override fun refactored(element: PsiElement, oldQualifiedName: String?) { super.refactored(element, oldQualifiedName) target.target = virtualFile.path } } - override fun getRefactoringElementListener(element: com.intellij.psi.PsiElement?): com.intellij.refactoring.listeners.RefactoringElementListener? { + override fun getRefactoringElementListener(element: PsiElement?): RefactoringElementListener? { val targetElement = target.asPsiElement(this) val workingDirectoryFile = getWorkingDirectoryAsVirtual() val targetFile = target.asVirtualFile() - if (targetElement != null && com.intellij.psi.util.PsiTreeUtil.isAncestor(element, targetElement, false)) { + if (targetElement != null && PsiTreeUtil.isAncestor(element, targetElement, false)) { return PyElementTargetRenamer(targetElement, workingDirectoryFile) } - if (targetFile != null && element is com.intellij.psi.PsiFileSystemItem && com.intellij.openapi.vfs.VfsUtil.isAncestor( + if (targetFile != null && element is PsiFileSystemItem && VfsUtil.isAncestor( element.virtualFile, targetFile, false)) { return PyVirtualFileRenamer(targetFile, workingDirectoryFile) } @@ -400,8 +443,8 @@ abstract class PyAbstractTestConfiguration(project: com.intellij.openapi.project override fun checkConfiguration() { super.checkConfiguration() if (!isFrameworkInstalled()) { - throw com.intellij.execution.configurations.RuntimeConfigurationWarning( - com.jetbrains.python.PyBundle.message("runcfg.testing.no.test.framework", testFrameworkName)) + throw RuntimeConfigurationWarning( + PyBundle.message("runcfg.testing.no.test.framework", testFrameworkName)) } target.checkValid() } @@ -414,25 +457,25 @@ abstract class PyAbstractTestConfiguration(project: com.intellij.openapi.project override fun isTestBased() = true - private fun getPythonTestSpecByLocation(location: com.intellij.execution.Location<*>): List { + private fun getPythonTestSpecByLocation(location: Location<*>): List { - if (location is com.jetbrains.python.testing.PyTargetBasedPsiLocation) { + if (location is PyTargetBasedPsiLocation) { return location.target.generateArgumentsLine(this) } - if (location !is com.intellij.execution.PsiLocation) { + if (location !is PsiLocation) { return emptyList() } - if (location.psiElement !is com.jetbrains.python.psi.PyQualifiedNameOwner) { + if (location.psiElement !is PyQualifiedNameOwner) { return emptyList() } - val qualifiedName = (location.psiElement as com.jetbrains.python.psi.PyQualifiedNameOwner).qualifiedName ?: return emptyList() + val qualifiedName = (location.psiElement as PyQualifiedNameOwner).qualifiedName ?: return emptyList() // Resolve name as python qname as last resort - return com.jetbrains.python.testing.ConfigurationTarget(qualifiedName, TestTargetType.PYTHON).generateArgumentsLine(this) + return ConfigurationTarget(qualifiedName, TestTargetType.PYTHON).generateArgumentsLine(this) } - override fun getTestSpec(location: com.intellij.execution.Location<*>, + override fun getTestSpec(location: Location<*>, failedTest: com.intellij.execution.testframework.AbstractTestProxy): String? { val list = getPythonTestSpecByLocation(location) if (list.isEmpty()) { @@ -444,7 +487,7 @@ abstract class PyAbstractTestConfiguration(project: com.intellij.openapi.project } override fun getTestSpecsForRerun(scope: com.intellij.psi.search.GlobalSearchScope, - locations: MutableList, AbstractTestProxy>>): List { + locations: MutableList, AbstractTestProxy>>): List { val result = java.util.ArrayList() // Set used to remove duplicate targets locations.map { it.first }.distinctBy { it.psiElement }.map { getPythonTestSpecByLocation(it) }.filterNotNull().forEach { @@ -470,11 +513,11 @@ abstract class PyAbstractTestConfiguration(project: com.intellij.openapi.project override fun suggestedName() = when (target.targetType) { - com.jetbrains.python.testing.TestTargetType.PATH -> { + TestTargetType.PATH -> { val name = target.asVirtualFile()?.name "$testFrameworkName in " + (name ?: target.target) } - com.jetbrains.python.testing.TestTargetType.PYTHON -> { + TestTargetType.PYTHON -> { "$testFrameworkName for " + target.target } else -> { @@ -489,16 +532,16 @@ abstract class PyAbstractTestConfiguration(project: com.intellij.openapi.project protected open fun getCustomRawArgumentsString(forRerun: Boolean = false) = "" fun reset() { - target.target = com.jetbrains.python.testing.DEFAULT_PATH - target.targetType = com.jetbrains.python.testing.TestTargetType.PATH + target.target = DEFAULT_PATH + target.targetType = TestTargetType.PATH additionalArguments = "" } - fun copyFrom(src: com.jetbrains.reflection.Properties) { + fun copyFrom(src: Properties) { src.copyTo(getConfigFields()) } - fun copyTo(dst: com.jetbrains.reflection.Properties) { + fun copyTo(dst: Properties) { getConfigFields().copyTo(dst) } @@ -515,7 +558,7 @@ abstract class PyAbstractTestConfiguration(project: com.intellij.openapi.project val value = it.get() if (value != null) { // No need to write null since null is default value - com.intellij.openapi.util.JDOMExternalizerUtil.writeField(element, it.prefixedName, gson.toJson(value)) + writeField(element, it.prefixedName, gson.toJson(value)) } } } @@ -526,7 +569,7 @@ abstract class PyAbstractTestConfiguration(project: com.intellij.openapi.project val gson = com.google.gson.Gson() getConfigFields().properties.forEach { - val fromJson: Any? = gson.fromJson(com.intellij.openapi.util.JDOMExternalizerUtil.readField(element, it.prefixedName), it.getType()) + val fromJson: Any? = gson.fromJson(readField(element, it.prefixedName), it.getType()) if (fromJson != null) { it.set(fromJson) } @@ -535,7 +578,7 @@ abstract class PyAbstractTestConfiguration(project: com.intellij.openapi.project } - private fun getConfigFields() = com.jetbrains.reflection.getProperties(this, ConfigField::class.java) + private fun getConfigFields() = getProperties(this, ConfigField::class.java) /** * Checks if element could be test target for this config. @@ -544,23 +587,30 @@ abstract class PyAbstractTestConfiguration(project: com.intellij.openapi.project * If yes, and element is [PsiElement] then it is [TestTargetType.PYTHON]. * If file then [TestTargetType.PATH] */ - fun couldBeTestTarget(element: com.intellij.psi.PsiElement) = + fun couldBeTestTarget(element: PsiElement): Boolean { + // TODO: PythonUnitTestUtil logic is weak. We should give user ability to launch test on symbol since user knows better if folder // contains tests etc - when (element) { - is com.jetbrains.python.psi.PyFile -> com.jetbrains.python.testing.isTestFile(element) - is com.intellij.psi.PsiDirectory -> element.name.contains("test", true) || element.children.any { - it is com.jetbrains.python.psi.PyFile && com.jetbrains.python.testing.isTestFile( - it) + val context = TypeEvalContext.userInitiated(element.project, element.containingFile) + val testCaseClassRequired: ThreeState = if (RunnersThatRequireTestCaseClass.contains(runnerName)) { + ThreeState.YES + } + else { + ThreeState.NO + } + return when (element) { + is PyFile -> PythonUnitTestUtil.isTestFile(element, testCaseClassRequired, context) + is PsiDirectory -> element.name.contains("test", true) || element.children.any { + it is PyFile && PythonUnitTestUtil.isTestFile(it, testCaseClassRequired, context) + } + is PyFunction -> PythonUnitTestUtil.isTestFunction(element, + testCaseClassRequired, context) + is PyClass -> { + PythonUnitTestUtil.isTestClass(element, testCaseClassRequired, context) } - is com.jetbrains.python.psi.PyFunction -> com.jetbrains.python.testing.PythonUnitTestUtil.isTestCaseFunction(element, - runBareFunctions) - is com.jetbrains.python.psi.PyClass -> com.jetbrains.python.testing.PythonUnitTestUtil.isTestCaseClass(element, - com.jetbrains.python.psi.types.TypeEvalContext.userInitiated( - element.project, - element.containingFile)) else -> false } + } /** * There are 2 ways to provide target to runner: @@ -572,38 +622,31 @@ abstract class PyAbstractTestConfiguration(project: com.intellij.openapi.project internal open fun shouldSeparateTargetPath(): Boolean = true } -private fun isTestFile(file: com.jetbrains.python.psi.PyFile): Boolean { - return com.jetbrains.python.testing.PythonUnitTestUtil.isUnitTestFile(file) || - com.jetbrains.python.testing.PythonUnitTestUtil.getTestCaseClassesFromFile(file, - com.jetbrains.python.psi.types.TypeEvalContext.userInitiated( - file.project, file)).isNotEmpty() -} - -abstract class PyAbstractTestFactory : com.jetbrains.python.run.PythonConfigurationFactoryBase( - com.jetbrains.python.testing.PythonTestConfigurationType.getInstance()) { - override abstract fun createTemplateConfiguration(project: com.intellij.openapi.project.Project): CONF_T +abstract class PyAbstractTestFactory : PythonConfigurationFactoryBase( + PythonTestConfigurationType.getInstance()) { + override abstract fun createTemplateConfiguration(project: Project): CONF_T } /** * Only one producer is registered with EP, but it uses factory configured by user to produce different configs */ -object PyTestsConfigurationProducer : com.jetbrains.python.testing.AbstractPythonTestConfigurationProducer( - com.jetbrains.python.testing.PythonTestConfigurationType.getInstance()) { +object PyTestsConfigurationProducer : AbstractPythonTestConfigurationProducer( + PythonTestConfigurationType.getInstance()) { - override val configurationClass = com.jetbrains.python.testing.PyAbstractTestConfiguration::class.java + override val configurationClass = PyAbstractTestConfiguration::class.java - override fun cloneTemplateConfiguration(context: com.intellij.execution.actions.ConfigurationContext): com.intellij.execution.RunnerAndConfigurationSettings { - return cloneTemplateConfigurationStatic(context, com.jetbrains.python.testing.findConfigurationFactoryFromSettings(context.module)) + override fun cloneTemplateConfiguration(context: ConfigurationContext): RunnerAndConfigurationSettings { + return cloneTemplateConfigurationStatic(context, findConfigurationFactoryFromSettings(context.module)) } - override fun createConfigurationFromContext(context: com.intellij.execution.actions.ConfigurationContext?): com.intellij.execution.actions.ConfigurationFromContext? { + override fun createConfigurationFromContext(context: ConfigurationContext?): ConfigurationFromContext? { // Since we need module, no need to even try to create config with out of it context?.module ?: return null return super.createConfigurationFromContext(context) } - override fun findOrCreateConfigurationFromContext(context: com.intellij.execution.actions.ConfigurationContext?): com.intellij.execution.actions.ConfigurationFromContext? { - if (!com.jetbrains.python.testing.isNewTestsModeEnabled()) { + override fun findOrCreateConfigurationFromContext(context: ConfigurationContext?): ConfigurationFromContext? { + if (!isNewTestsModeEnabled()) { return null } return super.findOrCreateConfigurationFromContext(context) @@ -616,9 +659,9 @@ object PyTestsConfigurationProducer : com.jetbrains.python.testing.AbstractPytho override fun isPreferredConfiguration(self: ConfigurationFromContext?, other: ConfigurationFromContext) = other.configuration is PythonRunConfiguration - override fun setupConfigurationFromContext(configuration: com.jetbrains.python.testing.PyAbstractTestConfiguration?, - context: com.intellij.execution.actions.ConfigurationContext?, - sourceElement: com.intellij.openapi.util.Ref?): Boolean { + override fun setupConfigurationFromContext(configuration: PyAbstractTestConfiguration?, + context: ConfigurationContext?, + sourceElement: Ref?): Boolean { if (sourceElement == null || configuration == null) { return false @@ -627,12 +670,12 @@ object PyTestsConfigurationProducer : com.jetbrains.python.testing.AbstractPytho val location = context?.location configuration.module = context?.module configuration.isUseModuleSdk = true - if (location is com.jetbrains.python.testing.PyTargetBasedPsiLocation) { + if (location is PyTargetBasedPsiLocation) { location.target.copyTo(configuration.target) } else { - val targetForConfig = com.jetbrains.python.testing.PyTestsConfigurationProducer.getTargetForConfig(configuration, - sourceElement.get()) ?: return false + val targetForConfig = PyTestsConfigurationProducer.getTargetForConfig(configuration, + sourceElement.get()) ?: return false targetForConfig.first.copyTo(configuration.target) // Directory may be set in Default configuration. In that case no need to rewrite it. if (configuration.workingDirectory.isNullOrEmpty()) { @@ -661,8 +704,8 @@ object PyTestsConfigurationProducer : com.jetbrains.python.testing.AbstractPytho * Also reports working dir what should be set to configuration to work correctly * @return [target, workingDirectory] */ - private fun getTargetForConfig(configuration: com.jetbrains.python.testing.PyAbstractTestConfiguration, - baseElement: com.intellij.psi.PsiElement): com.intellij.openapi.util.Pair? { + private fun getTargetForConfig(configuration: PyAbstractTestConfiguration, + baseElement: PsiElement): Pair? { var element = baseElement @@ -672,22 +715,22 @@ object PyTestsConfigurationProducer : com.jetbrains.python.testing.AbstractPytho do { if (configuration.couldBeTestTarget(element)) { when (element) { - is com.jetbrains.python.psi.PyQualifiedNameOwner -> { // Function, class, method + is PyQualifiedNameOwner -> { // Function, class, method val module = configuration.module ?: return null val elementFile = element.containingFile as? PyFile ?: return null val workingDirectory = getDirectoryForFileToBeImportedFrom(elementFile) ?: return null - val context = com.jetbrains.extenstions.QNameResolveContext(module, - evalContext = TypeEvalContext.userInitiated(configuration.project, - null), - folderToStart = workingDirectory.virtualFile) + val context = QNameResolveContext(module, + evalContext = TypeEvalContext.userInitiated(configuration.project, + null), + folderToStart = workingDirectory.virtualFile) val parts = element.tryResolveAndSplit(context) ?: return null val qualifiedName = parts.getElementNamePrependingFile(workingDirectory) - return com.intellij.openapi.util.Pair(ConfigurationTarget(qualifiedName.toString(), TestTargetType.PYTHON), - workingDirectory.virtualFile.path) + return Pair(ConfigurationTarget(qualifiedName.toString(), TestTargetType.PYTHON), + workingDirectory.virtualFile.path) } - is com.intellij.psi.PsiFileSystemItem -> { + is PsiFileSystemItem -> { val virtualFile = element.virtualFile val path = virtualFile @@ -696,29 +739,29 @@ object PyTestsConfigurationProducer : com.jetbrains.python.testing.AbstractPytho is PsiDirectory -> element else -> return null }?.virtualFile?.path ?: return null - return com.intellij.openapi.util.Pair(ConfigurationTarget(path.path, TestTargetType.PATH), workingDirectory) + return Pair(ConfigurationTarget(path.path, TestTargetType.PATH), workingDirectory) } } } element = element.parent ?: break } - while (element !is com.intellij.psi.PsiDirectory) // if parent is folder, then we are at file level + while (element !is PsiDirectory) // if parent is folder, then we are at file level return null } - override fun isConfigurationFromContext(configuration: com.jetbrains.python.testing.PyAbstractTestConfiguration, - context: com.intellij.execution.actions.ConfigurationContext?): Boolean { + override fun isConfigurationFromContext(configuration: PyAbstractTestConfiguration, + context: ConfigurationContext?): Boolean { val location = context?.location - if (location is com.jetbrains.python.testing.PyTargetBasedPsiLocation) { + if (location is PyTargetBasedPsiLocation) { // With derived classes several configurations for same element may exist return location.target == configuration.target } val psiElement = context?.psiLocation ?: return false - val targetForConfig = com.jetbrains.python.testing.PyTestsConfigurationProducer.getTargetForConfig(configuration, - psiElement) ?: return false + val targetForConfig = PyTestsConfigurationProducer.getTargetForConfig(configuration, + psiElement) ?: return false return configuration.target == targetForConfig.first } } diff --git a/python/src/com/jetbrains/python/testing/PyUnitTest.kt b/python/src/com/jetbrains/python/testing/PyUnitTest.kt index e4c5ec58b82e..8b4245172561 100644 --- a/python/src/com/jetbrains/python/testing/PyUnitTest.kt +++ b/python/src/com/jetbrains/python/testing/PyUnitTest.kt @@ -44,7 +44,7 @@ class PyUnitTestExecutionEnvironment(configuration: PyUnitTestConfiguration, env class PyUnitTestConfiguration(project: Project, factory: PyUnitTestFactory) : - PyAbstractTestConfiguration(project, factory, runBareFunctions = false) { // Bare functions not supported in unittest: classes only + PyAbstractTestConfiguration(project, factory, PythonTestConfigurationsModel.PYTHONS_UNITTEST_NAME) { // Bare functions not supported in unittest: classes only @ConfigField var pattern: String? = null diff --git a/python/src/com/jetbrains/python/testing/PythonTestLegacyConfigurationProducer.java b/python/src/com/jetbrains/python/testing/PythonTestLegacyConfigurationProducer.java index 48d929dcfc9e..96b0755e6b06 100644 --- a/python/src/com/jetbrains/python/testing/PythonTestLegacyConfigurationProducer.java +++ b/python/src/com/jetbrains/python/testing/PythonTestLegacyConfigurationProducer.java @@ -32,6 +32,7 @@ import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.*; import com.intellij.psi.util.PsiTreeUtil; +import com.intellij.util.ThreeState; import com.jetbrains.python.PythonModuleTypeBase; import com.jetbrains.python.facet.PythonFacetSettings; import com.jetbrains.python.psi.*; @@ -45,6 +46,7 @@ import java.io.File; import java.util.Collections; import java.util.HashSet; import java.util.List; +import java.util.stream.Collectors; abstract public class PythonTestLegacyConfigurationProducer> extends AbstractPythonTestConfigurationProducer> { @@ -110,7 +112,6 @@ abstract public class PythonTestLegacyConfigurationProducer configuration, ConfigurationContext context, @@ -136,7 +137,8 @@ abstract public class PythonTestLegacyConfigurationProducer getTestCaseClassesFromFile(@NotNull final PyFile pyFile) { - return PythonUnitTestUtil.getTestCaseClassesFromFile(pyFile, TypeEvalContext.userInitiated(pyFile.getProject(), pyFile)); + final TypeEvalContext context = TypeEvalContext.userInitiated(pyFile.getProject(), pyFile); + return pyFile.getTopLevelClasses().stream() + .filter(o -> PythonUnitTestUtil.isTestClass(o, ThreeState.UNSURE, context)) + .collect(Collectors.toList()); } - } \ No newline at end of file diff --git a/python/src/com/jetbrains/python/testing/PythonUnitTestUtil.java b/python/src/com/jetbrains/python/testing/PythonUnitTestUtil.java index 5f919282f525..3933901a280c 100644 --- a/python/src/com/jetbrains/python/testing/PythonUnitTestUtil.java +++ b/python/src/com/jetbrains/python/testing/PythonUnitTestUtil.java @@ -15,197 +15,145 @@ */ package com.jetbrains.python.testing; -import com.google.common.collect.Lists; import com.google.common.collect.Sets; import com.intellij.execution.Location; import com.intellij.execution.PsiLocation; -import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.module.Module; +import com.intellij.openapi.module.ModuleUtilCore; import com.intellij.openapi.progress.ProgressManager; import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Ref; import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.vfs.LocalFileSystem; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import com.intellij.psi.PsiManager; -import com.intellij.util.containers.Stack; -import com.jetbrains.python.psi.*; +import com.intellij.util.ThreeState; +import com.jetbrains.extensions.PyClassExtKt; +import com.jetbrains.python.psi.PyClass; +import com.jetbrains.python.psi.PyFile; +import com.jetbrains.python.psi.PyFunction; import com.jetbrains.python.psi.stubs.PyClassNameIndex; import com.jetbrains.python.psi.stubs.PyFunctionNameIndex; -import com.jetbrains.python.psi.types.PyClassLikeType; import com.jetbrains.python.psi.types.TypeEvalContext; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.*; -import java.util.regex.Pattern; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; /** + * Tools to check if some file, function or class could be test case. + * There are 2 strategies: "testCaseClassRequired" means only TesCase inheritors are considered as test cases. + * In opposite case any function named "test*" either top level or situated in class named Test* or *Test is test case. + * Providing null to "testCaseClassRequired" means "use default runner settings" and usually best value. + * * @author Leonid Shalupov + * @author Ilya.Kazakevich */ -public class PythonUnitTestUtil { +public final class PythonUnitTestUtil { public static final String TESTCASE_SETUP_NAME = "setUp"; - private static final HashSet PYTHON_TEST_QUALIFIED_CLASSES = Sets.newHashSet("unittest.TestCase", "unittest.case.TestCase"); - private static final Pattern TEST_MATCH_PATTERN = Pattern.compile("(?:^|[\b_\\.%s-])[Tt]est"); - private static final String TESTCASE_METHOD_PREFIX = "test"; - - - public static boolean isTestFunction(PyFunction pyFunction) { - String name = pyFunction.getName(); - if (name != null && name.startsWith("test")) { - return true; - } - return false; - } - - public static boolean isTestClass(final PyClass pyClass, @Nullable final TypeEvalContext context) { - final TypeEvalContext contextToUse = (context != null ? context : TypeEvalContext.codeInsightFallback(pyClass.getProject())); - for (PyClassLikeType type : pyClass.getAncestorTypes(contextToUse)) { - if (type != null && PYTHON_TEST_QUALIFIED_CLASSES.contains(type.getClassQName())) { - return true; - } - } - final String className = pyClass.getName(); - if (className == null) return false; - final String name = className.toLowerCase(); - if (name.startsWith("test")) { - for (PyFunction cls : pyClass.getMethods()) { - if (isTestFunction(cls)) { - return true; - } - } - } - return false; - } + public static final Set PYTHON_TEST_QUALIFIED_CLASSES = Collections.unmodifiableSet(Sets.newHashSet("unittest.TestCase", + "unittest.case.TestCase")); private PythonUnitTestUtil() { } - public static boolean isUnitTestCaseFunction(PyFunction function) { - final String name = function.getName(); - if (name == null || !name.startsWith(TESTCASE_METHOD_PREFIX)) { + + public static boolean isTestFile(@NotNull final PyFile file, + @NotNull final ThreeState testCaseClassRequired, + @Nullable final TypeEvalContext context) { + if (file.getTopLevelClasses().stream().anyMatch(o -> isTestClass(o, testCaseClassRequired, context))) { + return true; + } + + if (isTestCaseClassRequired(file, testCaseClassRequired)) { + return false; + } + return file.getTopLevelFunctions().stream().anyMatch(o -> isTestFunction(o, testCaseClassRequired, context)); + } + + + + public static boolean isTestClass(@NotNull final PyClass cls, + @NotNull final ThreeState testCaseClassRequired, + @Nullable TypeEvalContext context) { + final boolean testCaseOnly = isTestCaseClassRequired(cls, testCaseClassRequired); + if (context == null) { + context = TypeEvalContext.codeInsightFallback(cls.getProject()); + } + final boolean inheritsTestCase = PyClassExtKt.inherits(cls, context, PYTHON_TEST_QUALIFIED_CLASSES); + if (inheritsTestCase) { + return true; + } + + if (testCaseOnly) { return false; } - final PyClass containingClass = function.getContainingClass(); - if (containingClass == null || !isUnitTestCaseClass(containingClass, PYTHON_TEST_QUALIFIED_CLASSES)) { + final String className = cls.getName(); + if (className == null) { return false; } - return true; - } + if (!className.startsWith("Test") && !className.endsWith("Test")) { + return false; + } - public static boolean isUnitTestCaseClass(PyClass cls) { - return isUnitTestCaseClass(cls, PYTHON_TEST_QUALIFIED_CLASSES); - } - - public static boolean isUnitTestFile(PyFile file) { - if (!file.getName().startsWith("test")) return false; - return true; - } - - private static boolean isUnitTestCaseClass(PyClass cls, HashSet testQualifiedNames) { - if (ApplicationManager.getApplication().isUnitTestMode()) { - for (PyExpression expression : cls.getSuperClassExpressions()) { - if (expression.getText().equals("TestCase")) return true; + Ref result = new Ref<>(false); + cls.visitMethods(function -> { + final String name = function.getName(); + if (name != null && name.startsWith("test")) { + result.set(true); + return false; } - } - for (PyClassLikeType type : cls.getAncestorTypes(TypeEvalContext.codeInsightFallback(cls.getProject()))) { - if (type != null && testQualifiedNames.contains(type.getClassQName())) { - return true; - } - } - return false; + return false; + }, true, context); + return result.get(); } - public static List getTestCaseClassesFromFile(PsiFile file, @Nullable final TypeEvalContext context) { - if (file instanceof PyFile) { - return getTestCaseClassesFromFile((PyFile)file, PYTHON_TEST_QUALIFIED_CLASSES, context); - } - return Collections.emptyList(); - } - public static List getTestCaseClassesFromFile(PyFile file, Set testQualifiedNames, @Nullable final TypeEvalContext context) { - List result = Lists.newArrayList(); - for (PyClass cls : file.getTopLevelClasses()) { - if (isTestCaseClassWithContext(cls, testQualifiedNames, context)) { - result.add(cls); - } - } - for (PyFunction cls : file.getTopLevelFunctions()) { - if (isTestCaseFunction(cls, false)) { - result.add(cls); - } - } - return result; - } - - public static boolean isTestCaseFunction(PyFunction function) { - return isTestCaseFunction(function, true); - } - - public static boolean isTestCaseFunction(PyFunction function, boolean checkAssert) { + public static boolean isTestFunction(@NotNull final PyFunction function, + @NotNull final ThreeState testCaseClassRequired, + @Nullable final TypeEvalContext context) { final String name = function.getName(); - if (name != null && TEST_MATCH_PATTERN.matcher(name).find()) { + if (name == null || !name.startsWith("test")) { // Since there are a lot of ways to launch assert in modern frameworks, // we assume any function with "test" word in name is test + return false; + } + // If testcase not required then any test function is test + final PyClass aClass = function.getContainingClass(); + if (!isTestCaseClassRequired(function, testCaseClassRequired) && aClass == null) { return true; } - if (function.getContainingClass() != null) { - if (isTestCaseClass(function.getContainingClass(), null)) return true; - } - if (checkAssert) { - boolean hasAssert = hasAssertOrYield(function.getStatementList()); - if (hasAssert) return true; - } - return false; + return aClass != null && isTestClass(aClass, testCaseClassRequired, context); } - private static boolean hasAssertOrYield(PyStatementList list) { - Stack stack = new Stack<>(); - if (list != null) { - for (PyStatement st : list.getStatements()) { - stack.push(st); - while (!stack.isEmpty()) { - PsiElement e = stack.pop(); - if (e instanceof PyAssertStatement || e instanceof PyYieldExpression) return true; - for (PsiElement psiElement : e.getChildren()) { - stack.push(psiElement); - } - } - } - } - return false; + + /** + * @deprecated Use {@link #isTestClass(PyClass, ThreeState, TypeEvalContext)} instead. + * Will be removed in 2018. + */ + @Deprecated + public static boolean isUnitTestCaseClass(PyClass cls) { + return isTestClass(cls, ThreeState.YES, null); } - public static boolean isTestCaseClass(@NotNull PyClass cls, @Nullable final TypeEvalContext context) { - return isTestCaseClassWithContext(cls, PYTHON_TEST_QUALIFIED_CLASSES, context); - } - public static boolean isTestCaseClassWithContext(@NotNull PyClass cls, - Set testQualifiedNames, - @Nullable TypeEvalContext context) { - final TypeEvalContext contextToUse = (context != null ? context : TypeEvalContext.codeInsightFallback(cls.getProject())); - for (PyClassLikeType type : cls.getAncestorTypes(contextToUse)) { - if (type != null) { - if (testQualifiedNames.contains(type.getClassQName())) { - return true; - } - } + private static boolean isTestCaseClassRequired(@NotNull final PsiElement anchor, @NotNull final ThreeState userProvidedValue) { + if (userProvidedValue != ThreeState.UNSURE) { + return userProvidedValue.toBoolean(); } - String clsName = cls.getQualifiedName(); - String[] names = new String[0]; - if (clsName != null) { - names = clsName.split("\\."); - } - if (names.length == 0) return false; - - clsName = names[names.length - 1]; - if (TEST_MATCH_PATTERN.matcher(clsName).find()) { + final Module module = ModuleUtilCore.findModuleForPsiElement(anchor); + if (module == null) { return true; } - return false; + return PyTestsSharedKt.getRunnersThatRequireTestCaseClass().contains(TestRunnerService.getInstance(module).getProjectConfiguration()); } public static List findLocations(@NotNull final Project project, diff --git a/python/src/com/jetbrains/python/testing/nosetestLegacy/PythonNoseTestConfigurationProducer.java b/python/src/com/jetbrains/python/testing/nosetestLegacy/PythonNoseTestConfigurationProducer.java index aba592ef53e4..2f173f37ed01 100644 --- a/python/src/com/jetbrains/python/testing/nosetestLegacy/PythonNoseTestConfigurationProducer.java +++ b/python/src/com/jetbrains/python/testing/nosetestLegacy/PythonNoseTestConfigurationProducer.java @@ -20,6 +20,7 @@ import com.intellij.openapi.module.Module; import com.intellij.openapi.module.ModuleManager; import com.intellij.openapi.projectRoots.Sdk; import com.intellij.psi.PsiElement; +import com.intellij.util.ThreeState; import com.jetbrains.python.psi.PyFunction; import com.jetbrains.python.sdk.PythonSdkType; import com.jetbrains.python.testing.*; @@ -46,6 +47,6 @@ public class PythonNoseTestConfigurationProducer extends @Override protected boolean isTestFunction(@NotNull final PyFunction pyFunction, @Nullable final AbstractPythonLegacyTestRunConfiguration configuration) { - return PythonUnitTestUtil.isTestCaseFunction(pyFunction, true); + return PythonUnitTestUtil.isTestFunction(pyFunction, ThreeState.NO, null); } } \ No newline at end of file diff --git a/python/src/com/jetbrains/python/testing/pytest/PyTestUtil.java b/python/src/com/jetbrains/python/testing/pytest/PyTestUtil.java index d3525535e01d..e9ce999e11a8 100644 --- a/python/src/com/jetbrains/python/testing/pytest/PyTestUtil.java +++ b/python/src/com/jetbrains/python/testing/pytest/PyTestUtil.java @@ -15,23 +15,16 @@ */ package com.jetbrains.python.testing.pytest; -import com.google.common.collect.Lists; -import com.intellij.psi.PsiDirectory; -import com.intellij.psi.PsiFile; -import com.intellij.psi.PsiFileSystemItem; import com.jetbrains.python.psi.PyClass; -import com.jetbrains.python.psi.PyFile; import com.jetbrains.python.psi.PyFunction; -import com.jetbrains.python.psi.PyStatement; +import com.jetbrains.python.psi.types.PyClassLikeType; import com.jetbrains.python.psi.types.TypeEvalContext; import com.jetbrains.python.testing.PythonUnitTestUtil; -import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.List; - /** * User: catherine + * * @deprecated use {@link PythonUnitTestUtil} will be removed in 2018 */ @Deprecated @@ -40,11 +33,31 @@ public class PyTestUtil { @Deprecated public static boolean isPyTestFunction(PyFunction pyFunction) { - return PythonUnitTestUtil.isTestFunction(pyFunction); + String name = pyFunction.getName(); + if (name != null && name.startsWith("test")) { + return true; + } + return false; } @Deprecated public static boolean isPyTestClass(final PyClass pyClass, @Nullable final TypeEvalContext context) { - return PythonUnitTestUtil.isTestClass(pyClass, context); + final TypeEvalContext contextToUse = (context != null ? context : TypeEvalContext.codeInsightFallback(pyClass.getProject())); + for (PyClassLikeType type : pyClass.getAncestorTypes(contextToUse)) { + if (type != null && PythonUnitTestUtil.PYTHON_TEST_QUALIFIED_CLASSES.contains(type.getClassQName())) { + return true; + } + } + final String className = pyClass.getName(); + if (className == null) return false; + final String name = className.toLowerCase(); + if (name.startsWith("test")) { + for (PyFunction cls : pyClass.getMethods()) { + if (isPyTestFunction(cls)) { + return true; + } + } + } + return false; } } diff --git a/python/testData/testRunner/env/createConfigurationTest/tests_folder/test_functions.py b/python/testData/testRunner/env/createConfigurationTest/tests_folder/test_functions.py new file mode 100644 index 000000000000..5199de8e4517 --- /dev/null +++ b/python/testData/testRunner/env/createConfigurationTest/tests_folder/test_functions.py @@ -0,0 +1,5 @@ +def test_test(): + pass + +def foo(): + pass \ No newline at end of file diff --git a/python/testSrc/com/jetbrains/env/python/testing/CreateConfigurationMultipleCasesTask.java b/python/testSrc/com/jetbrains/env/python/testing/CreateConfigurationMultipleCasesTask.java index 9c313752508a..6ce089229bd0 100644 --- a/python/testSrc/com/jetbrains/env/python/testing/CreateConfigurationMultipleCasesTask.java +++ b/python/testSrc/com/jetbrains/env/python/testing/CreateConfigurationMultipleCasesTask.java @@ -20,8 +20,10 @@ import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.*; import com.jetbrains.python.psi.PyClass; import com.jetbrains.python.psi.PyFile; +import com.jetbrains.python.psi.PyFunction; import com.jetbrains.python.testing.ConfigurationTarget; import com.jetbrains.python.testing.PyAbstractTestConfiguration; +import com.jetbrains.python.testing.TestTargetType; import org.hamcrest.Matchers; import org.jetbrains.annotations.NotNull; import org.junit.Assert; @@ -34,7 +36,7 @@ import java.util.List; * Creates configurations for many different cases: packages, classes, files and folders. It checks then that configuration is ok. * @author Ilya.Kazakevich */ -final class CreateConfigurationMultipleCasesTask extends CreateConfigurationTestTask { +class CreateConfigurationMultipleCasesTask extends CreateConfigurationTestTask { CreateConfigurationMultipleCasesTask(@NotNull final String testRunnerName, @NotNull final Class expectedConfigurationType) { @@ -55,6 +57,8 @@ final class CreateConfigurationMultipleCasesTask(PythonTestConfigurationsModel.PYTHONS_UNITTEST_NAME, - PyUnitTestConfiguration.class)); + new CreateConfigurationMultipleCasesTask(PythonTestConfigurationsModel.PYTHONS_UNITTEST_NAME, + PyUnitTestConfiguration.class){ + @Override + protected boolean configurationShouldBeProducedForElement(@NotNull final PsiElement element) { + // test_functions.py does not conttain any TestCase and can't be launched with unittest + final PsiFile file = element.getContainingFile(); + return file == null || ! file.getName().endsWith("test_functions.py"); + } + }); } /**