diff --git a/python/ide/impl/src/com/jetbrains/python/PlatformPythonModuleType.java b/python/ide/impl/src/com/jetbrains/python/PlatformPythonModuleType.java index 3d4eabca4f7d..1c59a0be76c6 100644 --- a/python/ide/impl/src/com/jetbrains/python/PlatformPythonModuleType.java +++ b/python/ide/impl/src/com/jetbrains/python/PlatformPythonModuleType.java @@ -1,4 +1,4 @@ -// Copyright 2000-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +// Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. package com.jetbrains.python; import com.intellij.ide.util.projectWizard.EmptyModuleBuilder; @@ -21,6 +21,6 @@ public class PlatformPythonModuleType extends PythonModuleTypeBase type) { - return type == JavaSourceRootType.SOURCE; + return type == JavaSourceRootType.SOURCE || type == JavaSourceRootType.TEST_SOURCE; } } diff --git a/python/ide/impl/src/com/jetbrains/python/configuration/PyContentEntriesModuleConfigurable.java b/python/ide/impl/src/com/jetbrains/python/configuration/PyContentEntriesModuleConfigurable.java index 33b97a18da9d..31d74279cc27 100644 --- a/python/ide/impl/src/com/jetbrains/python/configuration/PyContentEntriesModuleConfigurable.java +++ b/python/ide/impl/src/com/jetbrains/python/configuration/PyContentEntriesModuleConfigurable.java @@ -1,18 +1,4 @@ -/* - * Copyright 2000-2016 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. - */ +// Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. package com.jetbrains.python.configuration; import com.intellij.facet.impl.DefaultFacetsProvider; @@ -91,7 +77,7 @@ public class PyContentEntriesModuleConfigurable extends SearchableConfigurable.P } protected PyContentEntriesEditor createEditor(@NotNull Module module, @NotNull ModuleConfigurationStateImpl state) { - return new PyContentEntriesEditor(module, state, true, JavaSourceRootType.SOURCE); + return new PyContentEntriesEditor(module, state, true, JavaSourceRootType.SOURCE, JavaSourceRootType.TEST_SOURCE); } @Override diff --git a/python/resources/META-INF/pycharm-core.xml b/python/resources/META-INF/pycharm-core.xml index 0394b22ac68c..ce9c112b5856 100644 --- a/python/resources/META-INF/pycharm-core.xml +++ b/python/resources/META-INF/pycharm-core.xml @@ -45,6 +45,7 @@ + diff --git a/python/src/com/jetbrains/python/testing/PyTestsShared.kt b/python/src/com/jetbrains/python/testing/PyTestsShared.kt index fdcf4665d51d..f32a98477105 100644 --- a/python/src/com/jetbrains/python/testing/PyTestsShared.kt +++ b/python/src/com/jetbrains/python/testing/PyTestsShared.kt @@ -12,10 +12,12 @@ import com.intellij.execution.testframework.sm.runner.SMTRunnerConsoleProperties import com.intellij.execution.testframework.sm.runner.SMTestLocator import com.intellij.openapi.application.runReadAction import com.intellij.openapi.module.Module +import com.intellij.openapi.module.ModuleUtil import com.intellij.openapi.module.impl.scopes.ModuleWithDependenciesScope import com.intellij.openapi.options.SettingsEditor import com.intellij.openapi.project.Project import com.intellij.openapi.projectRoots.Sdk +import com.intellij.openapi.roots.ModuleRootManager import com.intellij.openapi.util.JDOMExternalizerUtil.readField import com.intellij.openapi.util.JDOMExternalizerUtil.writeField import com.intellij.openapi.util.Pair @@ -26,6 +28,7 @@ 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.PsiManager import com.intellij.psi.search.GlobalSearchScope import com.intellij.psi.util.QualifiedName import com.intellij.refactoring.listeners.RefactoringElementListener @@ -53,6 +56,7 @@ import jetbrains.buildServer.messages.serviceMessages.ServiceMessage import jetbrains.buildServer.messages.serviceMessages.TestStdErr import jetbrains.buildServer.messages.serviceMessages.TestStdOut import org.jetbrains.annotations.PropertyKey +import org.jetbrains.jps.model.java.JavaSourceRootType import java.util.regex.Matcher /** @@ -94,9 +98,7 @@ internal fun getAdditionalArgumentsProperty() = PyAbstractTestConfiguration::add */ fun isTestElement(element: PsiElement, testCaseClassRequired: ThreeState, typeEvalContext: TypeEvalContext): Boolean = when (element) { is PyFile -> PythonUnitTestDetectorsBasedOnSettings.isTestFile(element, testCaseClassRequired, typeEvalContext) - is com.intellij.psi.PsiDirectory -> element.name.contains("test", true) || element.children.any { - it is PyFile && PythonUnitTestDetectorsBasedOnSettings.isTestFile(it, testCaseClassRequired, typeEvalContext) - } + is PsiDirectory -> isTestFolder(element, testCaseClassRequired, typeEvalContext) is PyFunction -> PythonUnitTestDetectorsBasedOnSettings.isTestFunction(element, testCaseClassRequired, typeEvalContext) is com.jetbrains.python.psi.PyClass -> { @@ -105,6 +107,25 @@ fun isTestElement(element: PsiElement, testCaseClassRequired: ThreeState, typeEv else -> false } +/** + * If element is a subelement of the folder excplicitly marked as test root -- use it + */ +private fun getExplicitlyConfiguredTestRoot(element: PsiFileSystemItem): VirtualFile? { + val vfDirectory = element.virtualFile + val module = ModuleUtil.findModuleForPsiElement(element) ?: return null + return ModuleRootManager.getInstance(module).getSourceRoots(JavaSourceRootType.TEST_SOURCE).firstOrNull { + VfsUtil.isAncestor(it, vfDirectory, false) + } +} + +private fun isTestFolder(element: PsiDirectory, + testCaseClassRequired: ThreeState, + typeEvalContext: TypeEvalContext): Boolean { + return (getExplicitlyConfiguredTestRoot(element) != null) || element.name.contains("test", true) || element.children.any { + it is PyFile && PythonUnitTestDetectorsBasedOnSettings.isTestFile(it, testCaseClassRequired, typeEvalContext) + } +} + /** * Since runners report names of tests as qualified name, no need to convert it to PSI and back to string. @@ -734,9 +755,14 @@ internal class PyTestsConfigurationProducer : AbstractPythonTestConfigurationPro } /** - * Inspects file relative imports, finds farthest and returns folder with imported file + * Returns test root for this file. Either it is specified explicitly or calculated using following strategy: + * Inspect file relative imports, find farthest and return folder with imported file */ private fun getDirectoryForFileToBeImportedFrom(file: PyFile): PsiDirectory? { + getExplicitlyConfiguredTestRoot(file)?.let { + return PsiManager.getInstance(file.project).findDirectory(it) + } + val maxRelativeLevel = file.fromImports.map { it.relativeLevel }.maxOrNull() ?: 0 var elementFolder = file.parent ?: return null for (i in 1..maxRelativeLevel) { @@ -794,8 +820,7 @@ internal class PyTestsConfigurationProducer : AbstractPythonTestConfigurationPro location.metainfo?.let { configuration.setMetaInfo(it) } } else { - val targetForConfig = PyTestsConfigurationProducer.getTargetForConfig(configuration, - element) ?: return false + val targetForConfig = getTargetForConfig(configuration, element) ?: return false targetForConfig.configurationTarget.copyTo(configuration.target) // Directory may be set in Default configuration. In that case no need to rewrite it. if (configuration.workingDirectory.isNullOrEmpty()) {