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)
This commit is contained in:
Ilya.Kazakevich
2017-07-04 19:32:23 +03:00
parent c4c750d467
commit 22fc6e1716
17 changed files with 428 additions and 354 deletions
@@ -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<String>) =
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())
@@ -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<PsiNamedElement, Integer>(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<PsiNamedElement, Integer>(eachFunction, TestFinderHelper.calcTestNameProximity(sourceName, eachName)));
}
@@ -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);
}
}
@@ -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;
@@ -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;
}
@@ -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 = ""
@@ -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 = ""
@@ -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<com.jetbrains.python.run.PythonConfigurationFactoryBase> = arrayOf(
com.jetbrains.python.testing.PyUnitTestFactory,
com.jetbrains.python.testing.PyTestFactory,
com.jetbrains.python.testing.PyNoseTestFactory)
val factories: Array<PythonConfigurationFactoryBase> = 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<PsiElement>(element) {
private class PyTargetBasedPsiLocation(val target: ConfigurationTarget,
element: PsiElement) : PsiLocation<PsiElement>(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<com.intellij.execution.Location<out PsiElement>> {
if (scope !is com.intellij.openapi.module.impl.scopes.ModuleWithDependenciesScope) {
project: Project,
scope: GlobalSearchScope): List<Location<out PsiElement>> {
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<T : com.jetbrains.python.testing.PyAbstractTestConfiguration>(configuration: T,
environment: com.intellij.execution.runners.ExecutionEnvironment)
: com.jetbrains.python.testing.PythonTestCommandLineStateBase<T>(configuration, environment) {
abstract class PyTestExecutionEnvironment<T : PyAbstractTestConfiguration>(configuration: T,
environment: ExecutionEnvironment)
: PythonTestCommandLineStateBase<T>(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<String> = java.util.ArrayList(configuration.getTestSpec())
override fun generateCommandLine(patchers: Array<out com.jetbrains.python.run.CommandLinePatcher>?): com.intellij.execution.configurations.GeneralCommandLine {
override fun generateCommandLine(patchers: Array<out CommandLinePatcher>?): GeneralCommandLine {
val line = super.generateCommandLine(patchers)
line.workDirectory = java.io.File(configuration.workingDirectorySafe)
return line
@@ -134,19 +179,19 @@ abstract class PyTestExecutionEnvironment<T : com.jetbrains.python.testing.PyAbs
}
abstract class PyAbstractTestSettingsEditor(private val sharedForm: com.jetbrains.python.testing.PyTestSharedForm)
: com.intellij.openapi.options.SettingsEditor<PyAbstractTestConfiguration>() {
abstract class PyAbstractTestSettingsEditor(private val sharedForm: PyTestSharedForm)
: SettingsEditor<PyAbstractTestConfiguration>() {
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<String> =
fun generateArgumentsLine(configuration: PyAbstractTestConfiguration): List<String> =
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<String> {
private fun getArgumentsForPythonTarget(configuration: PyAbstractTestConfiguration): List<String> {
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<PyAbstractTestConfiguration>(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<PyAbstractTestConfiguration>(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<String> {
private fun getPythonTestSpecByLocation(location: Location<*>): List<String> {
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<com.intellij.openapi.util.Pair<Location<*>, AbstractTestProxy>>): List<String> {
locations: MutableList<Pair<Location<*>, AbstractTestProxy>>): List<String> {
val result = java.util.ArrayList<String>()
// 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<out CONF_T : com.jetbrains.python.testing.PyAbstractTestConfiguration> : 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<out CONF_T : PyAbstractTestConfiguration> : 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<PyAbstractTestConfiguration>(
com.jetbrains.python.testing.PythonTestConfigurationType.getInstance()) {
object PyTestsConfigurationProducer : AbstractPythonTestConfigurationProducer<PyAbstractTestConfiguration>(
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<PsiElement>?): Boolean {
override fun setupConfigurationFromContext(configuration: PyAbstractTestConfiguration?,
context: ConfigurationContext?,
sourceElement: Ref<PsiElement>?): 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<ConfigurationTarget, String?>? {
private fun getTargetForConfig(configuration: PyAbstractTestConfiguration,
baseElement: PsiElement): Pair<ConfigurationTarget, String?>? {
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
}
}
@@ -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
@@ -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<T extends AbstractPythonLegacyTestRunConfiguration<T>>
extends AbstractPythonTestConfigurationProducer<AbstractPythonLegacyTestRunConfiguration<T>> {
@@ -110,7 +112,6 @@ abstract public class PythonTestLegacyConfigurationProducer<T extends AbstractPy
}
@Override
protected boolean setupConfigurationFromContext(AbstractPythonLegacyTestRunConfiguration<T> configuration,
ConfigurationContext context,
@@ -136,7 +137,8 @@ abstract public class PythonTestLegacyConfigurationProducer<T extends AbstractPy
return setupConfigurationFromFunction(pyFunction, configuration);
}
final PyClass pyClass = PsiTreeUtil.getParentOfType(element, PyClass.class, false);
if (pyClass != null && isTestClass(pyClass, configuration, TypeEvalContext.userInitiated(pyClass.getProject(), element.getContainingFile()))) {
if (pyClass != null &&
isTestClass(pyClass, configuration, TypeEvalContext.userInitiated(pyClass.getProject(), element.getContainingFile()))) {
return setupConfigurationFromClass(pyClass, configuration);
}
if (element == null) return false;
@@ -149,7 +151,7 @@ abstract public class PythonTestLegacyConfigurationProducer<T extends AbstractPy
}
private boolean setupConfigurationFromFolder(@NotNull final PsiDirectory element,
@NotNull final AbstractPythonLegacyTestRunConfiguration configuration) {
@NotNull final AbstractPythonLegacyTestRunConfiguration configuration) {
final VirtualFile virtualFile = element.getVirtualFile();
if (!isTestFolder(virtualFile, element.getProject())) return false;
final String path = virtualFile.getPath();
@@ -162,7 +164,8 @@ abstract public class PythonTestLegacyConfigurationProducer<T extends AbstractPy
return true;
}
private static void setModuleSdk(@NotNull final PsiElement element, @NotNull final AbstractPythonLegacyTestRunConfiguration configuration) {
private static void setModuleSdk(@NotNull final PsiElement element,
@NotNull final AbstractPythonLegacyTestRunConfiguration configuration) {
configuration.setUseModuleSdk(true);
configuration.setModule(ModuleUtilCore.findModuleForPsiElement(element));
}
@@ -206,8 +209,9 @@ abstract public class PythonTestLegacyConfigurationProducer<T extends AbstractPy
cfg.setScriptName(vFile.getPath());
if (StringUtil.isEmptyOrSpaces(cfg.getWorkingDirectory()))
if (StringUtil.isEmptyOrSpaces(cfg.getWorkingDirectory())) {
cfg.setWorkingDirectory(parent.getPath());
}
cfg.setGeneratedName();
setModuleSdk(element, cfg);
return true;
@@ -229,13 +233,14 @@ abstract public class PythonTestLegacyConfigurationProducer<T extends AbstractPy
}
protected boolean isTestClass(@NotNull final PyClass pyClass,
@Nullable final AbstractPythonLegacyTestRunConfiguration configuration, @Nullable final TypeEvalContext context) {
return PythonUnitTestUtil.isTestCaseClass(pyClass, context);
@Nullable final AbstractPythonLegacyTestRunConfiguration configuration,
@Nullable final TypeEvalContext context) {
return PythonUnitTestUtil.isTestClass(pyClass, ThreeState.UNSURE, context);
}
protected boolean isTestFunction(@NotNull final PyFunction pyFunction,
@Nullable final AbstractPythonLegacyTestRunConfiguration configuration) {
return PythonUnitTestUtil.isTestCaseFunction(pyFunction);
return PythonUnitTestUtil.isTestFunction(pyFunction, ThreeState.UNSURE, null);
}
protected boolean isTestFile(@NotNull final PyFile file) {
@@ -261,7 +266,9 @@ abstract public class PythonTestLegacyConfigurationProducer<T extends AbstractPy
}
protected List<PyStatement> 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());
}
}
@@ -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<String> 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<String> 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<String> testQualifiedNames) {
if (ApplicationManager.getApplication().isUnitTestMode()) {
for (PyExpression expression : cls.getSuperClassExpressions()) {
if (expression.getText().equals("TestCase")) return true;
Ref<Boolean> 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<PyStatement> 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<PyStatement> getTestCaseClassesFromFile(PyFile file, Set<String> testQualifiedNames, @Nullable final TypeEvalContext context) {
List<PyStatement> 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<PsiElement> 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<String> 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<Location> findLocations(@NotNull final Project project,
@@ -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);
}
}
@@ -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;
}
}
@@ -0,0 +1,5 @@
def test_test():
pass
def foo():
pass
@@ -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<T extends PyAbstractTestConfiguration> extends CreateConfigurationTestTask<T> {
class CreateConfigurationMultipleCasesTask<T extends PyAbstractTestConfiguration> extends CreateConfigurationTestTask<T> {
CreateConfigurationMultipleCasesTask(@NotNull final String testRunnerName,
@NotNull final Class<T> expectedConfigurationType) {
@@ -55,6 +57,8 @@ final class CreateConfigurationMultipleCasesTask<T extends PyAbstractTestConfigu
result.add(getDir("tests_folder"));
result.add(getFile("tests_folder", "test_lonely.py"));
result.add(getFile("tests_folder", "test_lonely.py").findTopLevelClass("TestLonely"));
result.add(getFile("tests_folder", "test_functions.py").findTopLevelFunction("test_test"));
result.add(getFile("tests_folder", "test_functions.py").findTopLevelFunction("foo"));
return result;
@@ -107,6 +111,12 @@ final class CreateConfigurationMultipleCasesTask<T extends PyAbstractTestConfigu
workingDirectory);
Assert.assertEquals("Bad configuration for class no package", "test_lonely.TestLonely", target.getTarget());
}
else if (element instanceof PyFunction && elementName.endsWith("test_test")) {
Assert.assertEquals("Bad configuration target", "test_functions.test_test", target.getTarget());
}
else if (element instanceof PyFunction && elementName.endsWith("foo")) {
Assert.assertEquals("non-test function should lead to level-based test", TestTargetType.PATH, target.getTargetType());
}
else {
throw new AssertionError("Unexpected configuration " + configuration);
}
@@ -27,6 +27,7 @@ import com.intellij.openapi.application.ModalityState;
import com.intellij.psi.PsiElement;
import com.jetbrains.env.PyExecutionFixtureTestTask;
import com.jetbrains.python.run.PythonConfigurationFactoryBase;
import com.jetbrains.python.run.PythonRunConfiguration;
import com.jetbrains.python.sdk.InvalidSdkException;
import com.jetbrains.python.sdkTools.SdkCreationType;
import com.jetbrains.python.testing.AbstractPythonTestRunConfiguration;
@@ -41,7 +42,6 @@ import org.junit.Assert;
import java.io.IOException;
import java.util.List;
import java.util.Optional;
/**
@@ -81,14 +81,24 @@ public abstract class CreateConfigurationTestTask<T extends AbstractPythonTestRu
for (final PsiElement elementToRightClickOn : getPsiElementsToRightClickOn()) {
@SuppressWarnings("unchecked") // Checked one line above
final T typedConfiguration = createConfigurationByElement(elementToRightClickOn, myExpectedConfigurationType);
Assert.assertTrue("Should use module sdk", typedConfiguration.isUseModuleSdk());
checkConfiguration(typedConfiguration, elementToRightClickOn);
if (configurationShouldBeProducedForElement(elementToRightClickOn)) {
@SuppressWarnings("unchecked") // Checked one line above
final T typedConfiguration = createConfigurationByElement(elementToRightClickOn, myExpectedConfigurationType);
Assert.assertTrue("Should use module sdk", typedConfiguration.isUseModuleSdk());
checkConfiguration(typedConfiguration, elementToRightClickOn);
} else {
// Any py file could be run script
// If no test config should be produced for this element then run script should be created
createConfigurationByElement(elementToRightClickOn, PythonRunConfiguration.class);
}
}
}), ModalityState.NON_MODAL);
}
protected boolean configurationShouldBeProducedForElement(@NotNull final PsiElement element) {
return true;
}
/**
* @return default (template) configuration
*/
@@ -21,6 +21,7 @@ import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.testFramework.EdtTestUtil;
import com.intellij.testFramework.fixtures.CodeInsightTestFixture;
import com.jetbrains.env.EnvTestTagsRequired;
@@ -512,8 +513,15 @@ public final class PythonUnitTestingTest extends PyEnvTestCase {
@Test
public void testMultipleCases() throws Exception {
runPythonTest(
new CreateConfigurationMultipleCasesTask<>(PythonTestConfigurationsModel.PYTHONS_UNITTEST_NAME,
PyUnitTestConfiguration.class));
new CreateConfigurationMultipleCasesTask<PyUnitTestConfiguration>(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");
}
});
}
/**