mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-13 21:55:01 +07:00
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:
@@ -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;
|
||||
|
||||
+2
-1
@@ -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
|
||||
|
||||
|
||||
+17
-10
@@ -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,
|
||||
|
||||
+2
-1
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
def test_test():
|
||||
pass
|
||||
|
||||
def foo():
|
||||
pass
|
||||
+11
-1
@@ -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);
|
||||
}
|
||||
|
||||
+15
-5
@@ -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
|
||||
*/
|
||||
|
||||
+10
-2
@@ -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");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user