mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-04-19 13:02:30 +07:00
This commit should fix: PY-22641, PY-23303, PY-22920, PY-23613, PY-23647, PY-23733, PY-23504, PY-23678, PY-23352, PY-23333, PY-22856, PY-20983
Working dir fix: * When configuration is generated -- use element dir as working dir * When launched and working dir is not set -- use element dir as working dir Inherited methods fix: * When resolving element -- obey name provided by runner because it may differ from element.qname (in case of inherited classes). * Fix filesystem part of path making element resolvable for python, but obey element part of path No file is reported by new runners, so we do out best to find file * Use index to resolve element as fallback if runner reported relative path * Resolve it against working dir and all roots Do not import anything in python runners: importing may break Django * Move convertion from qname to "filesystem/element name" to java side Tests added.
This commit is contained in:
@@ -309,64 +309,10 @@ messages.TeamcityServiceMessages = NewTeamcityServiceMessages
|
||||
|
||||
# Monkeypatched
|
||||
|
||||
|
||||
class _SymbolNameSplitter(object):
|
||||
"""
|
||||
Strategy to split symbol name to package/module part and symbols part.
|
||||
|
||||
"""
|
||||
def check_is_importable(self, parts, current_step, separator):
|
||||
"""
|
||||
|
||||
Run this method for each name part. Method throws ImportError when name is not importable.
|
||||
That means previous name is where module name ends.
|
||||
:param parts: list of module name parts
|
||||
:param current_step: from 0 to len(parts)
|
||||
:param separator: module name separator (".")
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class _SymbolName2KSplitter(_SymbolNameSplitter):
|
||||
"""
|
||||
Based on imp which works in 2, but not 3.
|
||||
It also emulates packages for folders with out of __init__.py.
|
||||
Say, you have Python path "spam.eggs" where "spam" is plain folder.
|
||||
It works for Py3, but not Py2.
|
||||
find_module for "spam" raises exception which is processed then (see "_symbol_processed")
|
||||
"""
|
||||
def __init__(self):
|
||||
super(_SymbolNameSplitter, self).__init__()
|
||||
self._path = None
|
||||
# Set to True when at least one find_module success, so we have at least one symbol
|
||||
self._symbol_processed = False
|
||||
|
||||
def check_is_importable(self, parts, current_step, separator):
|
||||
import imp
|
||||
module_to_import = parts[current_step]
|
||||
(fil, self._path, desc) = imp.find_module(module_to_import, [self._path] if self._path else None)
|
||||
self._symbol_processed = True
|
||||
if desc[2] == imp.PKG_DIRECTORY:
|
||||
# Package
|
||||
self._path = imp.load_module(module_to_import, fil, self._path, desc).__path__[0]
|
||||
|
||||
|
||||
|
||||
class _SymbolName3KSplitter(_SymbolNameSplitter):
|
||||
"""
|
||||
Based on importlib which works in 3, but not 2
|
||||
"""
|
||||
def check_is_importable(self, parts, current_step, separator):
|
||||
import importlib
|
||||
module_to_import = separator.join(parts[:current_step + 1])
|
||||
importlib.import_module(module_to_import)
|
||||
|
||||
|
||||
def jb_patch_separator(targets, fs_glue, python_glue, fs_to_python_glue):
|
||||
"""
|
||||
Targets are always dot separated according to manual.
|
||||
However, some runners may need different separators.
|
||||
This function splits target to file/symbol parts and glues them using provided glues.
|
||||
Converts python target if format "/path/foo.py::parts.to.python" provided by Java to
|
||||
python specific format
|
||||
|
||||
:param targets: list of dot-separated targets
|
||||
:param fs_glue: how to glue fs parts of target. I.e.: module "eggs" in "spam" package is "spam[fs_glue]eggs"
|
||||
@@ -378,19 +324,15 @@ def jb_patch_separator(targets, fs_glue, python_glue, fs_to_python_glue):
|
||||
return []
|
||||
|
||||
def _patch_target(target):
|
||||
_jb_utils.VersionAgnosticUtils.is_py3k()
|
||||
splitter = _SymbolName3KSplitter() if _jb_utils.VersionAgnosticUtils.is_py3k() else _SymbolName2KSplitter()
|
||||
|
||||
separator = "."
|
||||
parts = target.split(separator)
|
||||
for i in range(0, len(parts)):
|
||||
try:
|
||||
splitter.check_is_importable(parts, i, separator)
|
||||
except ImportError:
|
||||
fs_part = fs_glue.join(parts[:i])
|
||||
python_path = python_glue.join(parts[i:])
|
||||
return fs_part + fs_to_python_glue + python_path if python_path else fs_part
|
||||
return target
|
||||
# /path/foo.py::parts.to.python
|
||||
match = re.match("^(:?(.+)[.]py::)?(.+)$", target)
|
||||
assert match, "unexpected string: {0}".format(target)
|
||||
fs_part = match.group(2)
|
||||
python_part = match.group(3).replace(".", python_glue)
|
||||
if fs_part:
|
||||
return fs_part.replace("/", fs_glue) + fs_to_python_glue + python_part
|
||||
else:
|
||||
return python_part
|
||||
|
||||
return map(_patch_target, targets)
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* 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.extenstions
|
||||
|
||||
import com.intellij.psi.util.QualifiedName
|
||||
import com.jetbrains.extensions.getQName
|
||||
import com.jetbrains.python.psi.PyFile
|
||||
import com.jetbrains.python.psi.PyQualifiedNameOwner
|
||||
|
||||
/**
|
||||
* @author Ilya.Kazakevich
|
||||
* @see [QualifiedName] extension
|
||||
*/
|
||||
fun PyQualifiedNameOwner.splitNameParts(context: QNameResolveContext): QualifiedNameParts? {
|
||||
val name = this.qualifiedName ?: return null
|
||||
val qualifiedName = QualifiedName.fromDottedString(name)
|
||||
val parts = qualifiedName.splitNameParts(context)
|
||||
if (parts != null) {
|
||||
return parts
|
||||
}
|
||||
val pyFile = containingFile as? PyFile ?:return null
|
||||
val fileQName = pyFile.getQName() ?: return null
|
||||
val relativePath = qualifiedName.getRelativeNameTo(fileQName)?:return null
|
||||
return QualifiedNameParts(fileQName, relativePath, pyFile)
|
||||
}
|
||||
@@ -16,42 +16,111 @@
|
||||
package com.jetbrains.extenstions
|
||||
|
||||
import com.intellij.openapi.module.Module
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.openapi.vfs.VirtualFile
|
||||
import com.intellij.psi.PsiDirectory
|
||||
import com.intellij.psi.PsiElement
|
||||
import com.intellij.psi.PsiManager
|
||||
import com.intellij.psi.util.QualifiedName
|
||||
import com.jetbrains.extensions.getSdk
|
||||
import com.jetbrains.python.PyNames
|
||||
import com.jetbrains.python.psi.PyClass
|
||||
import com.jetbrains.python.psi.PyFile
|
||||
import com.jetbrains.python.psi.resolve.fromModule
|
||||
import com.jetbrains.python.psi.resolve.resolveModuleAt
|
||||
import com.jetbrains.python.psi.resolve.resolveQualifiedName
|
||||
import com.jetbrains.python.psi.stubs.PyModuleNameIndex
|
||||
import com.jetbrains.python.psi.types.TypeEvalContext
|
||||
import com.jetbrains.python.sdk.PythonSdkType
|
||||
|
||||
|
||||
data class QNameResolveContext(
|
||||
val module: Module,
|
||||
/**
|
||||
* Used for language level etc
|
||||
*/
|
||||
val sdk: Sdk? = module.getSdk(),
|
||||
val evalContext: TypeEvalContext,
|
||||
/**
|
||||
* If not provided resolves against roots only. Resolved also against this folder otherwise
|
||||
*/
|
||||
val folderToStart: VirtualFile? = null,
|
||||
/**
|
||||
* Use index, plain dirs with Py2 and so on. May resolve names unresolvable in other cases, but may return false results.
|
||||
*/
|
||||
val allowInaccurateResult:Boolean = false
|
||||
)
|
||||
|
||||
data class QualifiedNameParts(val fileName:QualifiedName, val elementName:QualifiedName, val file: PyFile) {
|
||||
override fun toString() = elementName.toString()
|
||||
|
||||
/**
|
||||
* @return element qname + last part of file qname.
|
||||
*/
|
||||
fun getElementNamePrependingFile() = QualifiedName.fromComponents(listOf(fileName.lastComponent!!) + elementName.components)!!
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Splits qname to [QualifiedNameParts]: filesystem part and element(symbol) part.
|
||||
* @see [com.jetbrains.python.psi.PyQualifiedNameOwner] similar extensions
|
||||
*/
|
||||
fun QualifiedName.splitNameParts(context: QNameResolveContext): QualifiedNameParts? {
|
||||
//TODO: May be slow, cache in this case
|
||||
|
||||
// Find first element that may be file
|
||||
var i = this.componentCount
|
||||
while (i > 0) {
|
||||
val possibleFileName = this.subQualifiedName(0, i)
|
||||
val possibleFile = possibleFileName.toElement(context)
|
||||
if (possibleFile is PyFile) {
|
||||
return QualifiedNameParts(possibleFileName, this.subQualifiedName(possibleFileName.componentCount, this.componentCount), possibleFile)
|
||||
}
|
||||
i--
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* @return qname part relative to root
|
||||
*/
|
||||
fun QualifiedName.getRelativeNameTo(root: QualifiedName): QualifiedName? {
|
||||
if (!toString().startsWith(root.toString())) {
|
||||
return null
|
||||
}
|
||||
return subQualifiedName(root.componentCount, componentCount)
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves qname of any symbol to appropriate PSI element.
|
||||
*/
|
||||
fun QualifiedName.toElement(module: Module,
|
||||
context: TypeEvalContext,
|
||||
folderToStart: VirtualFile? = null): PsiElement? {
|
||||
fun QualifiedName.toElement(context: QNameResolveContext): PsiElement? {
|
||||
var currentName = QualifiedName.fromComponents(this.components)
|
||||
|
||||
|
||||
var element: PsiElement? = null
|
||||
|
||||
// Drill as deep, as we can
|
||||
var lastElement: String? = null
|
||||
|
||||
var lastElement: String? = null
|
||||
var psiDirectory: PsiDirectory? = null
|
||||
val resolveContext = fromModule(module).copyWithMembers()
|
||||
if (folderToStart != null) {
|
||||
psiDirectory = PsiManager.getInstance(module.project).findDirectory(folderToStart)
|
||||
|
||||
var resolveContext = fromModule(context.module).copyWithMembers()
|
||||
if (PythonSdkType.getLanguageLevelForSdk(context.sdk).isPy3K || context.allowInaccurateResult) {
|
||||
resolveContext = resolveContext.copyWithPlainDirectories()
|
||||
}
|
||||
|
||||
if (context.folderToStart != null) {
|
||||
psiDirectory = PsiManager.getInstance(context.module.project).findDirectory(context.folderToStart)
|
||||
}
|
||||
|
||||
|
||||
// Drill as deep, as we can
|
||||
while (currentName.componentCount > 0 && element == null) {
|
||||
if (psiDirectory != null) {
|
||||
if (psiDirectory != null) { // Resolve against folder
|
||||
element = resolveModuleAt(currentName, psiDirectory, resolveContext).firstOrNull()
|
||||
} else {
|
||||
}
|
||||
|
||||
if (element == null) { // Resolve against roots
|
||||
element = resolveQualifiedName(currentName, resolveContext).firstOrNull()
|
||||
}
|
||||
|
||||
@@ -64,11 +133,27 @@ fun QualifiedName.toElement(module: Module,
|
||||
|
||||
if (lastElement != null && element is PyClass) {
|
||||
// Drill in class
|
||||
val method = element.findMethodByName(lastElement, true, context)
|
||||
|
||||
//TODO: Support nested classes
|
||||
val method = element.findMethodByName(lastElement, true, context.evalContext)
|
||||
if (method != null) {
|
||||
return method
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (element == null && this.firstComponent != null && context.allowInaccurateResult) {
|
||||
// If name starts with file which is not in root nor in folders -- use index.
|
||||
val nameToFind = this.firstComponent!!
|
||||
val pyFile = PyModuleNameIndex.find(nameToFind, context.module.project, false).firstOrNull() ?: return element
|
||||
|
||||
val folder =
|
||||
if (pyFile.name == PyNames.INIT_DOT_PY) { // We are in folder
|
||||
pyFile.virtualFile.parent.parent
|
||||
} else {
|
||||
pyFile.virtualFile.parent
|
||||
}
|
||||
return toElement(context.copy(folderToStart = folder))
|
||||
}
|
||||
return element
|
||||
}
|
||||
@@ -18,6 +18,7 @@ package com.jetbrains.python.run;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.intellij.diagnostic.logging.LogConfigurationPanel;
|
||||
import com.intellij.execution.ExecutionBundle;
|
||||
import com.intellij.execution.ExecutionException;
|
||||
import com.intellij.execution.configuration.AbstractRunConfiguration;
|
||||
import com.intellij.execution.configuration.EnvironmentVariablesComponent;
|
||||
import com.intellij.execution.configurations.*;
|
||||
@@ -301,6 +302,15 @@ public abstract class AbstractPythonRunConfiguration<T extends AbstractPythonRun
|
||||
return getConfigurationModule().getModule();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public final Module getModuleNotNull() throws ExecutionException {
|
||||
final Module module = getModule();
|
||||
if (module == null) {
|
||||
throw new ExecutionException("No module set for configuration, please choose one");
|
||||
}
|
||||
return module;
|
||||
}
|
||||
|
||||
public boolean isUseModuleSdk() {
|
||||
return myUseModuleSdk;
|
||||
}
|
||||
|
||||
@@ -16,28 +16,14 @@
|
||||
package com.jetbrains.python.testing.universalTests
|
||||
|
||||
import com.intellij.execution.ExecutionException
|
||||
import com.intellij.openapi.diagnostic.Logger
|
||||
import com.intellij.openapi.module.ModuleUtil
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.vfs.VfsUtil
|
||||
import com.intellij.openapi.vfs.VirtualFile
|
||||
import com.intellij.psi.PsiErrorElement
|
||||
import com.intellij.psi.PsiFileFactory
|
||||
import com.intellij.psi.PsiFileSystemItem
|
||||
import com.intellij.psi.PsiManager
|
||||
import com.intellij.psi.util.QualifiedName
|
||||
import com.jetbrains.commandInterface.commandLine.CommandLineLanguage
|
||||
import com.jetbrains.commandInterface.commandLine.CommandLinePart
|
||||
import com.jetbrains.commandInterface.commandLine.psi.CommandLineArgument
|
||||
import com.jetbrains.commandInterface.commandLine.psi.CommandLineFile
|
||||
import com.jetbrains.commandInterface.commandLine.psi.CommandLineOption
|
||||
import com.jetbrains.extensions.getQName
|
||||
import com.jetbrains.python.PyNames
|
||||
import com.jetbrains.python.psi.PyFile
|
||||
import com.jetbrains.python.psi.PyQualifiedNameOwner
|
||||
import com.jetbrains.python.psi.PyUtil
|
||||
import com.jetbrains.python.psi.resolve.fromModule
|
||||
import com.jetbrains.python.psi.resolve.resolveModuleAt
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
@@ -45,79 +31,6 @@ import java.util.*
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* For each element finds its name calculated against closest folder inside of module sources.
|
||||
* Having "foo.spam.eggs" where "foo" is plain dir but "spam" and 'eggs' are packages (with init)
|
||||
* will return "foo" as folder and 'spam.eggs' as name
|
||||
*/
|
||||
internal fun findPathWithPackagesByName(element: PyQualifiedNameOwner): Pair<VirtualFile, QualifiedName>? {
|
||||
val module = ModuleUtil.findModuleForPsiElement(element) ?: return null
|
||||
val elementQNameStr = element.qualifiedName ?: return null
|
||||
var elementQName = QualifiedName.fromDottedString(elementQNameStr)
|
||||
var fileQName = (element.containingFile as PyFile).getQName() ?: return null
|
||||
val context = fromModule(module)
|
||||
|
||||
val psiManager = PsiManager.getInstance(element.project)
|
||||
|
||||
val root = findPathWithPackagesByFsItem(element.containingFile)?:return null
|
||||
val newFolder = psiManager.findDirectory(root) ?: return null
|
||||
//var currentName = QualifiedName.fromDottedString(elementQName)
|
||||
while (resolveModuleAt(fileQName, newFolder, context).isEmpty() && fileQName.componentCount > 0) {
|
||||
fileQName = fileQName.removeHead(1)
|
||||
elementQName = elementQName.removeHead(1)
|
||||
}
|
||||
if (elementQName.componentCount > 0) {
|
||||
return Pair(newFolder.virtualFile, elementQName)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as [findPathWithPackagesByName] but uses fs item instead.
|
||||
* For foo/eggs/spam.py will return 'foo' if "eggs" has init.py or "foo/eggs" if not
|
||||
*
|
||||
*/
|
||||
internal fun findPathWithPackagesByFsItem(elementPath: PsiFileSystemItem): VirtualFile? {
|
||||
var currentDir = if (elementPath.isDirectory) {
|
||||
elementPath.virtualFile
|
||||
}
|
||||
else {
|
||||
elementPath?.parent?.virtualFile ?: return null
|
||||
}
|
||||
|
||||
val projectDir = findVFSItemRoot(elementPath.virtualFile, elementPath.project) ?: return null
|
||||
while (VfsUtil.isAncestor(projectDir, currentDir, false) && (currentDir.findChild(PyNames.INIT_DOT_PY) != null)) {
|
||||
currentDir = currentDir.parent
|
||||
}
|
||||
return currentDir
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds root closest to some file
|
||||
*/
|
||||
internal fun findVFSItemRoot(virtualFile: VirtualFile, project: Project): VirtualFile? {
|
||||
val module = ModuleUtil.findModuleForFile(virtualFile, project)
|
||||
if (module == null) {
|
||||
Logger.getInstance(PyUniversalTestConfiguration::class.java).warn("No module for " + virtualFile)
|
||||
return null
|
||||
}
|
||||
return PyUtil.getSourceRoots(module)
|
||||
.map {
|
||||
val path = VfsUtil.getRelativePath(virtualFile, it)
|
||||
if (path != null) com.intellij.openapi.util.Pair(path, it)
|
||||
else {
|
||||
null
|
||||
}
|
||||
}
|
||||
.filterNotNull()
|
||||
.sortedBy {
|
||||
it.first.length
|
||||
}
|
||||
.map(com.intellij.openapi.util.Pair<String, VirtualFile>::second)
|
||||
.firstOrNull()
|
||||
}
|
||||
|
||||
|
||||
//TODO: Migrate to [ParametersListUtil#parse] but support single quotes
|
||||
/**
|
||||
* Emulates command line processor (cmd, bash) by parsing command line to arguments that can be provided as argv.
|
||||
|
||||
@@ -22,7 +22,6 @@ import com.intellij.execution.configurations.RunProfileState
|
||||
import com.intellij.execution.runners.ExecutionEnvironment
|
||||
import com.intellij.openapi.options.SettingsEditor
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.psi.PsiElement
|
||||
import com.jetbrains.python.PythonHelper
|
||||
import com.jetbrains.python.testing.PythonTestConfigurationsModel
|
||||
import com.jetbrains.python.testing.VFSTestFrameworkListener
|
||||
@@ -60,8 +59,6 @@ class PyUniversalNoseTestConfiguration(project: Project, factory: PyUniversalNos
|
||||
|
||||
override fun isFrameworkInstalled() = VFSTestFrameworkListener.getInstance().isNoseTestInstalled(sdk)
|
||||
|
||||
//https://github.com/nose-devs/nose/issues/1042
|
||||
override fun packageOnlyIfInitPy(anchor: PsiElement) = true
|
||||
}
|
||||
|
||||
object PyUniversalNoseTestFactory : PyUniversalTestFactory<PyUniversalNoseTestConfiguration>() {
|
||||
|
||||
@@ -21,7 +21,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.psi.types.TypeEvalContext
|
||||
import com.jetbrains.python.testing.PythonTestConfigurationsModel
|
||||
import com.jetbrains.python.testing.VFSTestFrameworkListener
|
||||
|
||||
@@ -58,18 +57,6 @@ class PyUniversalPyTestConfiguration(project: Project, factory: PyUniversalPyTes
|
||||
|
||||
override fun isFrameworkInstalled() = VFSTestFrameworkListener.getInstance().isPyTestInstalled(sdk)
|
||||
|
||||
override fun getWorkingDirectorySafe(): String {
|
||||
val dirProvidedByUser = super.getWorkingDirectory()
|
||||
if (! dirProvidedByUser.isNullOrEmpty()) {
|
||||
return dirProvidedByUser
|
||||
}
|
||||
// If dir is not set then find closest src because pytest resolves files against workdir and no sys.path
|
||||
val module = module ?: return super.getWorkingDirectorySafe()
|
||||
val context = TypeEvalContext.userInitiated(project, null)
|
||||
val targetElement = target.asPsiElement(module, context, null) ?: return super.getWorkingDirectorySafe()
|
||||
val root = findVFSItemRoot(targetElement.containingFile.virtualFile, project)?: return super.getWorkingDirectorySafe()
|
||||
return root.path
|
||||
}
|
||||
}
|
||||
|
||||
object PyUniversalPyTestFactory : PyUniversalTestFactory<PyUniversalPyTestConfiguration>() {
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
package com.jetbrains.python.testing.universalTests
|
||||
|
||||
import com.google.gson.Gson
|
||||
import com.intellij.execution.ExecutionException
|
||||
import com.intellij.execution.Location
|
||||
import com.intellij.execution.PsiLocation
|
||||
import com.intellij.execution.RunnerAndConfigurationSettings
|
||||
@@ -30,7 +31,6 @@ 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.diagnostic.Logger
|
||||
import com.intellij.openapi.module.Module
|
||||
import com.intellij.openapi.module.impl.scopes.ModuleWithDependenciesScope
|
||||
import com.intellij.openapi.options.SettingsEditor
|
||||
@@ -41,7 +41,6 @@ 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.openapi.vfs.VirtualFileSystem
|
||||
import com.intellij.psi.PsiDirectory
|
||||
import com.intellij.psi.PsiElement
|
||||
import com.intellij.psi.PsiFileSystemItem
|
||||
@@ -51,9 +50,15 @@ 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.jetbrains.extensions.getQName
|
||||
import com.jetbrains.extenstions.QNameResolveContext
|
||||
import com.jetbrains.extenstions.splitNameParts
|
||||
import com.jetbrains.extenstions.toElement
|
||||
import com.jetbrains.python.PyBundle
|
||||
import com.jetbrains.python.psi.*
|
||||
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
|
||||
@@ -83,6 +88,7 @@ internal fun getAdditionalArgumentsPropertyName() = PyUniversalTestConfiguration
|
||||
/**
|
||||
* 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: ConfigurationTarget, element: PsiElement) : PsiLocation<PsiElement>(element) {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
@@ -126,10 +132,13 @@ private object PyUniversalTestsLocator : SMTestLocator {
|
||||
null
|
||||
}
|
||||
|
||||
//TODO: Doc we will not bae able to resolve if different SDK
|
||||
val qualifiedName = QualifiedName.fromDottedString(path)
|
||||
// Assume qname id good and resolve it directly
|
||||
val element = qualifiedName.toElement(scope.module,
|
||||
TypeEvalContext.codeAnalysis(project, null), folderToStart = folder)
|
||||
val element = qualifiedName.toElement(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
|
||||
@@ -177,11 +186,14 @@ abstract class PyUniversalTestSettingsEditor(private val form: PyUniversalTestFo
|
||||
override fun createEditor(): JComponent = form.panel
|
||||
}
|
||||
|
||||
enum class TestTargetType(val optionName: String) {
|
||||
PYTHON("--target"), PATH("--path"), CUSTOM("")
|
||||
enum class TestTargetType {
|
||||
PYTHON, PATH, CUSTOM
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Default target path (run all tests ion project folder)
|
||||
*/
|
||||
private val DEFAULT_PATH = "."
|
||||
/**
|
||||
* Target depends on target type. It could be path to file/folder or python target
|
||||
*/
|
||||
@@ -202,11 +214,14 @@ data class ConfigurationTarget(@ConfigField var target: String, @ConfigField var
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts target to PSI element if possible
|
||||
* Converts target to PSI element if possible resolving it against roots and working directory
|
||||
*/
|
||||
fun asPsiElement(module: Module, context: TypeEvalContext, folderToStart: VirtualFile? = null): PsiElement? {
|
||||
fun asPsiElement(configuration: PyUniversalTestConfiguration): PsiElement? {
|
||||
if (targetType == TestTargetType.PYTHON) {
|
||||
return QualifiedName.fromDottedString(target).toElement(module, context, folderToStart)
|
||||
val context = TypeEvalContext.userInitiated(configuration.project, null)
|
||||
val workDir = configuration.getWorkingDirectoryAsVirtual()
|
||||
val name = QualifiedName.fromDottedString(target)
|
||||
return name.toElement(QNameResolveContext(configuration.moduleNotNull, configuration.sdk, context, workDir, true))
|
||||
}
|
||||
return null
|
||||
}
|
||||
@@ -214,12 +229,86 @@ data class ConfigurationTarget(@ConfigField var target: String, @ConfigField var
|
||||
/**
|
||||
* Converts target to file if possible
|
||||
*/
|
||||
fun asVirtualFile(fileSystem: VirtualFileSystem): VirtualFile? {
|
||||
fun asVirtualFile(): VirtualFile? {
|
||||
if (targetType == TestTargetType.PATH) {
|
||||
return fileSystem.findFileByPath(target)
|
||||
return LocalFileSystem.getInstance().findFileByPath(target)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun generateArgumentsLine(configuration: PyUniversalTestConfiguration): List<String> =
|
||||
when (targetType) {
|
||||
TestTargetType.CUSTOM -> emptyList()
|
||||
TestTargetType.PYTHON -> getArgumentsForPythonTarget(configuration)
|
||||
TestTargetType.PATH -> listOf("--path", target)
|
||||
}
|
||||
|
||||
private fun getArgumentsForPythonTarget(configuration: PyUniversalTestConfiguration): List<String> {
|
||||
val element = asPsiElement(configuration) ?:
|
||||
throw ExecutionException("Can't resolve $target. Try to remove configuration and generate is again")
|
||||
|
||||
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 = TypeEvalContext.userInitiated(configuration.project, null)
|
||||
val qNameResolveContext = QNameResolveContext(
|
||||
module = configuration.module!!,
|
||||
evalContext = context,
|
||||
folderToStart = LocalFileSystem.getInstance().findFileByPath(configuration.workingDirectorySafe),
|
||||
allowInaccurateResult = true
|
||||
)
|
||||
val qualifiedNameParts = QualifiedName.fromDottedString(target).splitNameParts(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
|
||||
|
||||
if (!configuration.isFSPartOfTargetShouldBeSeparated()) {
|
||||
// Here generate qname instead of file/path::element_name
|
||||
|
||||
// Try to set path relative to work dir (better than path from closest root)
|
||||
// If we can resolve element by this path relative to working directory then use it
|
||||
val qNameInsideOfDirectory = qualifiedNameParts.getElementNamePrependingFile()
|
||||
if (qNameInsideOfDirectory.toElement(qNameResolveContext.copy(allowInaccurateResult = false)) != null) {
|
||||
return listOf("--target", qNameInsideOfDirectory.toString())
|
||||
}
|
||||
// Use "full" (path from closest root) otherwise
|
||||
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())
|
||||
}
|
||||
else {
|
||||
|
||||
// Here generate file/path::element_name
|
||||
val pyTarget = qualifiedNameParts.elementName
|
||||
|
||||
val elementFile = element.containingFile.virtualFile
|
||||
val workingDir = elementFile.fileSystem.findFileByPath(configuration.workingDirectorySafe)
|
||||
|
||||
val fileSystemPartOfTarget = (if (workingDir != null) VfsUtil.getRelativePath(elementFile, workingDir) else null)
|
||||
?: elementFile.path
|
||||
|
||||
return listOf("--target", "$fileSystemPartOfTarget::$pyTarget")
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return directory which target is situated
|
||||
*/
|
||||
fun getElementDirectory(configuration: PyUniversalTestConfiguration): 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
|
||||
}
|
||||
val fileOrDir = asVirtualFile() ?: asPsiElement(configuration)?.containingFile?.virtualFile ?: return null
|
||||
return if (fileOrDir.isDirectory) fileOrDir else fileOrDir.parent
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -242,7 +331,7 @@ abstract class PyUniversalTestConfiguration(project: Project,
|
||||
: AbstractPythonTestRunConfiguration<PyUniversalTestConfiguration>(project, configurationFactory), PyRerunAwareConfiguration,
|
||||
RefactoringListenerProvider {
|
||||
@DelegationProperty
|
||||
val target = ConfigurationTarget(".", TestTargetType.PATH)
|
||||
val target = ConfigurationTarget(DEFAULT_PATH, TestTargetType.PATH)
|
||||
@ConfigField
|
||||
var additionalArguments = ""
|
||||
|
||||
@@ -263,6 +352,25 @@ abstract class PyUniversalTestConfiguration(project: Project,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* For real launch use [getWorkingDirectorySafe] instead
|
||||
*/
|
||||
internal fun getWorkingDirectoryAsVirtual(): VirtualFile? {
|
||||
if (!workingDirectory.isNullOrEmpty()) {
|
||||
return LocalFileSystem.getInstance().findFileByPath(workingDirectory)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
override fun getWorkingDirectorySafe(): String {
|
||||
val dirProvidedByUser = super.getWorkingDirectory()
|
||||
if (!dirProvidedByUser.isNullOrEmpty()) {
|
||||
return dirProvidedByUser
|
||||
}
|
||||
|
||||
return target.getElementDirectory(this)?.path ?: super.getWorkingDirectorySafe()
|
||||
}
|
||||
|
||||
/**
|
||||
* Renames python target if python symbol, module or folder renamed
|
||||
*/
|
||||
@@ -294,21 +402,15 @@ abstract class PyUniversalTestConfiguration(project: Project,
|
||||
val myModule = module
|
||||
val targetElement: PsiElement?
|
||||
|
||||
val workingDirectoryFile = if (workingDirectory.isNotEmpty()) {
|
||||
LocalFileSystem.getInstance().findFileByPath(workingDirectory)
|
||||
}
|
||||
else {
|
||||
null
|
||||
}
|
||||
val workingDirectoryFile = getWorkingDirectoryAsVirtual()
|
||||
|
||||
if (myModule != null) {
|
||||
targetElement = target.asPsiElement(myModule, TypeEvalContext.userInitiated(project, null), workingDirectoryFile)
|
||||
targetElement = target.asPsiElement(this)
|
||||
}
|
||||
else {
|
||||
targetElement = null
|
||||
}
|
||||
val targetFile = target.asVirtualFile(LocalFileSystem.getInstance())
|
||||
|
||||
val targetFile = target.asVirtualFile()
|
||||
|
||||
|
||||
if (targetElement != null && PsiTreeUtil.isAncestor(element, targetElement, false)) {
|
||||
@@ -339,7 +441,7 @@ abstract class PyUniversalTestConfiguration(project: Project,
|
||||
private fun getTestSpecForPythonTarget(location: Location<*>): List<String> {
|
||||
|
||||
if (location is PyTargetBasedPsiLocation) {
|
||||
return listOf(location.target.targetType.optionName, location.target.target)
|
||||
return location.target.generateArgumentsLine(this)
|
||||
}
|
||||
|
||||
if (location !is PsiLocation) {
|
||||
@@ -349,7 +451,9 @@ abstract class PyUniversalTestConfiguration(project: Project,
|
||||
return emptyList()
|
||||
}
|
||||
val qualifiedName = (location.psiElement as PyQualifiedNameOwner).qualifiedName ?: return emptyList()
|
||||
return listOf(TestTargetType.PYTHON.optionName, qualifiedName)
|
||||
|
||||
// Resolve name as python qname as last resort
|
||||
return ConfigurationTarget(qualifiedName, TestTargetType.PYTHON).generateArgumentsLine(this)
|
||||
}
|
||||
|
||||
override fun getTestSpec(location: Location<*>, failedTest: AbstractTestProxy): String? {
|
||||
@@ -366,17 +470,14 @@ abstract class PyUniversalTestConfiguration(project: Project,
|
||||
locations: MutableList<Pair<Location<*>, AbstractTestProxy>>): List<String> {
|
||||
val result = ArrayList<String>()
|
||||
// Set used to remove duplicate targets
|
||||
locations.map { it.first }.toSet().map { getTestSpecForPythonTarget(it) }.filterNotNull().forEach { result.addAll(it) }
|
||||
locations.map { it.first }.distinctBy { it.psiElement }.map { getTestSpecForPythonTarget(it) }.filterNotNull().forEach {
|
||||
result.addAll(it)
|
||||
}
|
||||
return result + generateRawArguments()
|
||||
}
|
||||
|
||||
fun getTestSpec(): List<String> {
|
||||
// For custom we only need to provide additional (raw) args
|
||||
// Provide target otherwise
|
||||
if (target.targetType == TestTargetType.CUSTOM) {
|
||||
return generateRawArguments()
|
||||
}
|
||||
return listOf(target.targetType.optionName, target.target) + generateRawArguments()
|
||||
return target.generateArgumentsLine(this) + generateRawArguments()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -393,7 +494,7 @@ abstract class PyUniversalTestConfiguration(project: Project,
|
||||
override fun suggestedName() =
|
||||
when (target.targetType) {
|
||||
TestTargetType.PATH -> {
|
||||
val name = target.asVirtualFile(LocalFileSystem.getInstance())?.name
|
||||
val name = target.asVirtualFile()?.name
|
||||
"$testFrameworkName in " + (name ?: target.target)
|
||||
}
|
||||
TestTargetType.PYTHON -> {
|
||||
@@ -411,7 +512,7 @@ abstract class PyUniversalTestConfiguration(project: Project,
|
||||
protected open fun getCustomRawArgumentsString() = ""
|
||||
|
||||
fun reset() {
|
||||
target.target = "."
|
||||
target.target = DEFAULT_PATH
|
||||
target.targetType = TestTargetType.PATH
|
||||
additionalArguments = ""
|
||||
}
|
||||
@@ -478,9 +579,13 @@ abstract class PyUniversalTestConfiguration(project: Project,
|
||||
}
|
||||
|
||||
/**
|
||||
* When checking if configuration is ok we need to know if folders could be packages: i.e. if foo.bar requires init.py in foo to work
|
||||
* There are 2 ways to provide target to runner:
|
||||
* * As full qname (package1.module1.Class1.test_foo)
|
||||
* * As filesystem path (package1/module1.py::Class1.test_foo) full or relative to working directory
|
||||
*
|
||||
* Second approach is prefered if this flag is set. It is generally better because filesystem path does not need __init__.py
|
||||
*/
|
||||
open fun packageOnlyIfInitPy(anchor: PsiElement) = (!LanguageLevel.forElement(anchor).isPy3K)
|
||||
internal open fun isFSPartOfTargetShouldBeSeparated(): Boolean = true
|
||||
}
|
||||
|
||||
private fun isTestFile(file: PyFile): Boolean {
|
||||
@@ -531,8 +636,9 @@ object PyUniversalTestsConfigurationProducer : AbstractPythonTestConfigurationPr
|
||||
location.target.copyTo(configuration.target)
|
||||
}
|
||||
else {
|
||||
val targetForConfig = getTargetForConfig(configuration, sourceElement.get(), true) ?: return false
|
||||
targetForConfig.copyTo(configuration.target)
|
||||
val targetForConfig = getTargetForConfig(configuration, sourceElement.get()) ?: return false
|
||||
targetForConfig.first.copyTo(configuration.target)
|
||||
configuration.workingDirectory = targetForConfig.second
|
||||
}
|
||||
configuration.setGeneratedName()
|
||||
return true
|
||||
@@ -540,13 +646,13 @@ object PyUniversalTestsConfigurationProducer : AbstractPythonTestConfigurationPr
|
||||
|
||||
|
||||
/**
|
||||
* Find concrete element to be used as test target.
|
||||
* @return configuration name and its target
|
||||
* Creates [ConfigurationTarget] to make configuration work with provided element.
|
||||
* Also reports working dir what should be set to configuration to work correctly
|
||||
* @return [target, workingDirectory]
|
||||
*/
|
||||
private fun getTargetForConfig(configuration: PyUniversalTestConfiguration,
|
||||
baseElement: PsiElement, fixConfiguration: Boolean = false): ConfigurationTarget? {
|
||||
baseElement: PsiElement): Pair<ConfigurationTarget, String?>? {
|
||||
|
||||
val fixPackages = (fixConfiguration && configuration.packageOnlyIfInitPy(baseElement))
|
||||
|
||||
var element = baseElement
|
||||
// Go up until we reach top of the file
|
||||
@@ -555,34 +661,24 @@ object PyUniversalTestsConfigurationProducer : AbstractPythonTestConfigurationPr
|
||||
do {
|
||||
if (configuration.couldBeTestTarget(element)) {
|
||||
when (element) {
|
||||
is PyQualifiedNameOwner -> { // Function, class, method
|
||||
var qualifiedName = element.qualifiedName
|
||||
if (qualifiedName == null) {
|
||||
Logger.getInstance(PyUniversalTestConfiguration::class.java).warn("$element has no qualified name")
|
||||
return null
|
||||
}
|
||||
if (fixPackages) {
|
||||
val pathResult = findPathWithPackagesByName(element)
|
||||
if (pathResult == null) {
|
||||
Logger.getInstance(PyUniversalTestConfiguration::class.java).warn("Can't resolve")
|
||||
return null
|
||||
}
|
||||
configuration.workingDirectory = pathResult.first.path
|
||||
qualifiedName = pathResult.second.toString()
|
||||
} else {
|
||||
val virtualFile = element.containingFile.virtualFile
|
||||
if (virtualFile != null) {
|
||||
configuration.workingDirectory = findVFSItemRoot(virtualFile, element.project)?.path
|
||||
}
|
||||
}
|
||||
return ConfigurationTarget(qualifiedName, TestTargetType.PYTHON)
|
||||
is PyQualifiedNameOwner -> { // Function, class, method
|
||||
|
||||
val module = configuration.module?: return null
|
||||
val elementFolder = element.containingFile.virtualFile.parent?: return null
|
||||
|
||||
val context = QNameResolveContext(module,
|
||||
evalContext = TypeEvalContext.userInitiated(configuration.project, null),
|
||||
folderToStart = elementFolder)
|
||||
val parts = element.splitNameParts(context) ?: return null
|
||||
val qualifiedName = parts.getElementNamePrependingFile()
|
||||
return Pair(ConfigurationTarget(qualifiedName.toString(), TestTargetType.PYTHON),
|
||||
elementFolder.path)
|
||||
}
|
||||
is PsiFileSystemItem -> {
|
||||
val path = element.virtualFile
|
||||
if (fixPackages) {
|
||||
configuration.workingDirectory = findPathWithPackagesByFsItem(element)?.path ?: return null
|
||||
}
|
||||
return ConfigurationTarget(path.path, TestTargetType.PATH)
|
||||
val virtualFile = element.virtualFile
|
||||
val path = virtualFile
|
||||
val workingDirectory = (if (virtualFile.isDirectory) virtualFile else virtualFile.parent).path
|
||||
return Pair(ConfigurationTarget(path.path, TestTargetType.PATH), workingDirectory)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -593,10 +689,17 @@ object PyUniversalTestsConfigurationProducer : AbstractPythonTestConfigurationPr
|
||||
}
|
||||
|
||||
|
||||
override fun isConfigurationFromContext(configuration: PyUniversalTestConfiguration?, context: ConfigurationContext?): Boolean {
|
||||
override fun isConfigurationFromContext(configuration: PyUniversalTestConfiguration, context: ConfigurationContext?): Boolean {
|
||||
|
||||
val location = context?.location
|
||||
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 = getTargetForConfig(configuration!!, psiElement) ?: return false
|
||||
return configuration.target == targetForConfig
|
||||
val targetForConfig = getTargetForConfig(configuration, psiElement) ?: return false
|
||||
return configuration.target == targetForConfig.first
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,6 @@ import com.intellij.execution.runners.ExecutionEnvironment
|
||||
import com.intellij.openapi.options.SettingsEditor
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.vfs.LocalFileSystem
|
||||
import com.intellij.psi.PsiElement
|
||||
import com.jetbrains.python.PythonHelper
|
||||
import com.jetbrains.python.testing.PythonTestConfigurationsModel
|
||||
|
||||
@@ -76,9 +75,8 @@ class PyUniversalUnitTestConfiguration(project: Project, factory: PyUniversalUni
|
||||
|
||||
override fun isFrameworkInstalled() = true //Unittest is always available
|
||||
|
||||
// See loader.py: is_not_importable = not os.path.isfile(os.path.join(start_dir, '__init__.py'))
|
||||
// https://mail.python.org/pipermail/python-dev/2017-March/147656.html
|
||||
override fun packageOnlyIfInitPy(anchor: PsiElement) = true
|
||||
// Unittest does not support filesystem path. It needs qname resolvable against root or workdir
|
||||
override fun isFSPartOfTargetShouldBeSeparated() = false
|
||||
}
|
||||
|
||||
object PyUniversalUnitTestFactory : PyUniversalTestFactory<PyUniversalUnitTestConfiguration>() {
|
||||
|
||||
10
python/testData/testRunner/env/nose/subfolder_test/tests/test_foo.py
vendored
Normal file
10
python/testData/testRunner/env/nose/subfolder_test/tests/test_foo.py
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
from unittest import TestCase
|
||||
|
||||
|
||||
def test_test():
|
||||
assert False
|
||||
|
||||
|
||||
class FooTest(TestCase):
|
||||
def test_test(self):
|
||||
self.fail()
|
||||
0
python/testData/testRunner/env/unit/package_in_folder/tests/foo/__init__.py
vendored
Normal file
0
python/testData/testRunner/env/unit/package_in_folder/tests/foo/__init__.py
vendored
Normal file
9
python/testData/testRunner/env/unit/package_in_folder/tests/foo/test_tets.py
vendored
Normal file
9
python/testData/testRunner/env/unit/package_in_folder/tests/foo/test_tets.py
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
from unittest import TestCase
|
||||
|
||||
|
||||
class FooTest(TestCase):
|
||||
def test_test(self):
|
||||
pass
|
||||
|
||||
def test_2_test(self):
|
||||
self.fail("D")
|
||||
@@ -16,7 +16,6 @@
|
||||
package com.jetbrains.env.python.testing;
|
||||
|
||||
import com.intellij.openapi.vfs.LocalFileSystem;
|
||||
import com.intellij.openapi.vfs.VfsUtilCore;
|
||||
import com.intellij.openapi.vfs.VirtualFile;
|
||||
import com.intellij.psi.*;
|
||||
import com.jetbrains.python.psi.PyClass;
|
||||
@@ -43,7 +42,6 @@ final class CreateConfigurationMultipleCasesTask<T extends PyUniversalTestConfig
|
||||
}
|
||||
|
||||
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
protected List<PsiElement> getPsiElementsToRightClickOn() {
|
||||
@@ -67,7 +65,6 @@ final class CreateConfigurationMultipleCasesTask<T extends PyUniversalTestConfig
|
||||
// When namespace packages are supported we can run tests in for of foo.spam.Bar even if foo is plain dir
|
||||
// otherwise we need to set "foo" as working dir and run spam.Bar.
|
||||
|
||||
final boolean namespacePackagesSupported = ! configuration.packageOnlyIfInitPy(element);
|
||||
|
||||
final LocalFileSystem fileSystem = LocalFileSystem.getInstance();
|
||||
final ConfigurationTarget target = configuration.getTarget();
|
||||
@@ -81,57 +78,34 @@ final class CreateConfigurationMultipleCasesTask<T extends PyUniversalTestConfig
|
||||
assert elementName != null;
|
||||
|
||||
if (element instanceof PsiDirectory && elementName.endsWith("package_test")) {
|
||||
|
||||
if (!namespacePackagesSupported) {
|
||||
Assert.assertEquals("Bad configuration for folder", "tests_package/package_test",
|
||||
VfsUtilCore.getRelativeLocation(target.asVirtualFile(fileSystem), workingDirectory));
|
||||
}
|
||||
|
||||
Assert.assertEquals("Working directory for folder should be same as folder", target.asVirtualFile(), workingDirectory);
|
||||
Assert.assertThat("Bad target", configuration.getTarget().getTarget(), Matchers.endsWith("package_test"));
|
||||
|
||||
}
|
||||
else if (element instanceof PyFile &&elementName.endsWith("test_in_package.py")) {
|
||||
if (!namespacePackagesSupported) {
|
||||
Assert.assertEquals("Bad configuration for file", "tests_package/package_test/test_in_package.py",
|
||||
VfsUtilCore.getRelativeLocation(target.asVirtualFile(fileSystem), workingDirectory));
|
||||
}
|
||||
|
||||
else if (element instanceof PyFile && elementName.endsWith("test_in_package.py")) {
|
||||
final VirtualFile targetFile = target.asVirtualFile();
|
||||
assert targetFile != null : "Failed to create virtual file for " + target;
|
||||
Assert.assertEquals("Working directory for file should be same as file's parent", targetFile.getParent(), workingDirectory);
|
||||
Assert.assertThat("Bad target", configuration.getTarget().getTarget(), Matchers.endsWith("test_in_package.py"));
|
||||
|
||||
}
|
||||
else if (element instanceof PyClass && elementName.endsWith("TestLogic")) {
|
||||
|
||||
if (!namespacePackagesSupported) {
|
||||
Assert.assertEquals("Bad working dir for class", projectRoot, workingDirectory);
|
||||
}
|
||||
|
||||
Assert.assertEquals("Bad configuration for class", "tests_package.package_test.test_in_package.TestLogic", target.getTarget());
|
||||
final PsiFile targetFile = element.getContainingFile();
|
||||
Assert.assertEquals("Bad working dir for class", targetFile.getVirtualFile().getParent(), workingDirectory);
|
||||
Assert.assertEquals("Bad configuration for class", "test_in_package.TestLogic", target.getTarget());
|
||||
}
|
||||
else if (element instanceof PsiDirectory && elementName.endsWith("tests_folder")) {
|
||||
if (!namespacePackagesSupported) {
|
||||
Assert.assertEquals("Bad configuration for no package folder", workingDirectory, target.asVirtualFile(fileSystem));
|
||||
}
|
||||
|
||||
Assert.assertEquals("Bad configuration for no package folder", workingDirectory, target.asVirtualFile());
|
||||
Assert.assertThat("Bad target", configuration.getTarget().getTarget(), Matchers.endsWith("tests_folder"));
|
||||
}
|
||||
else if (element instanceof PyFile && elementName.endsWith("test_lonely.py")) {
|
||||
if (!namespacePackagesSupported) {
|
||||
Assert.assertEquals("Bad configuration for no package file", "test_lonely.py",
|
||||
VfsUtilCore.getRelativeLocation(target.asVirtualFile(fileSystem), workingDirectory));
|
||||
}
|
||||
|
||||
final VirtualFile targetFile = target.asVirtualFile();
|
||||
assert targetFile != null : "Failed to create virtual file for " + target;
|
||||
Assert.assertEquals("Bad configuration for no package file", targetFile.getParent(), workingDirectory);
|
||||
Assert.assertThat("Bad target", configuration.getTarget().getTarget(), Matchers.endsWith("test_lonely.py"));
|
||||
}
|
||||
else if (element instanceof PyClass && elementName.endsWith("TestLonely")) {
|
||||
|
||||
if (!namespacePackagesSupported) {
|
||||
Assert.assertEquals("Bad configuration for no package class", workingDirectory, projectRoot.findChild("tests_folder"));
|
||||
Assert.assertEquals("Bad configuration for class no package", "test_lonely.TestLonely", target.getTarget());
|
||||
} else {
|
||||
Assert.assertEquals("Bad configuration for class no package", "tests_folder.test_lonely.TestLonely", target.getTarget());
|
||||
}
|
||||
|
||||
|
||||
Assert.assertEquals("Bad working directory for no package class", element.getContainingFile().getVirtualFile().getParent(),
|
||||
workingDirectory);
|
||||
Assert.assertEquals("Bad configuration for class no package", "test_lonely.TestLonely", target.getTarget());
|
||||
}
|
||||
else {
|
||||
throw new AssertionError("Unexpected configuration " + configuration);
|
||||
|
||||
@@ -98,7 +98,7 @@ public abstract class CreateConfigurationTestTask<T extends RunConfiguration> ex
|
||||
final Optional<ConfigurationFromContext> maybeConfig = configurationsFromContext.stream()
|
||||
.filter(o -> expectedConfigurationType.isAssignableFrom(o.getConfiguration().getClass()))
|
||||
.findFirst();
|
||||
Assert.assertTrue("No configuration of expected type created", maybeConfig.isPresent());
|
||||
Assert.assertTrue("No configuration of expected type created for element " + elementToRightClickOn, maybeConfig.isPresent());
|
||||
RunnerAndConfigurationSettings runnerAndConfigurationSettings = maybeConfig.get().getConfigurationSettings();
|
||||
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ abstract class PyUnitTestProcessWithConsoleTestTask extends PyProcessWithConsole
|
||||
protected final String myScriptName;
|
||||
|
||||
PyUnitTestProcessWithConsoleTestTask(@NotNull final String relativePathToTestData, @NotNull final String scriptName) {
|
||||
super(relativePathToTestData, SdkCreationType.EMPTY_SDK);
|
||||
super(relativePathToTestData, SdkCreationType.SDK_PACKAGES_ONLY);
|
||||
myScriptName = scriptName;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,12 +6,10 @@ import com.jetbrains.env.PyEnvTestCase;
|
||||
import com.jetbrains.env.PyProcessWithConsoleTestTask;
|
||||
import com.jetbrains.env.python.testing.CreateConfigurationTestTask.PyConfigurationCreationTask;
|
||||
import com.jetbrains.env.ut.PyNoseTestProcessRunner;
|
||||
import com.jetbrains.env.ut.PyTestTestProcessRunner;
|
||||
import com.jetbrains.python.sdkTools.SdkCreationType;
|
||||
import com.jetbrains.python.testing.PythonTestConfigurationsModel;
|
||||
import com.jetbrains.python.testing.universalTests.PyUniversalNoseTestConfiguration;
|
||||
import com.jetbrains.python.testing.universalTests.PyUniversalNoseTestFactory;
|
||||
import com.jetbrains.python.testing.universalTests.PyUniversalPyTestConfiguration;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
@@ -29,7 +27,7 @@ public final class PythonNoseTestingTest extends PyEnvTestCase {
|
||||
// Ensures setup/teardown does not break anything
|
||||
@Test
|
||||
public void testSetupTearDown() throws Exception {
|
||||
runPythonTest(new SetupTearDownTestTask<PyNoseTestProcessRunner>(){
|
||||
runPythonTest(new SetupTearDownTestTask<PyNoseTestProcessRunner>() {
|
||||
@NotNull
|
||||
@Override
|
||||
protected PyNoseTestProcessRunner createProcessRunner() throws Exception {
|
||||
@@ -38,6 +36,16 @@ public final class PythonNoseTestingTest extends PyEnvTestCase {
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRerunSubfolder() throws Exception {
|
||||
runPythonTest(new RerunSubfolderTask<PyNoseTestProcessRunner>(2) {
|
||||
@NotNull
|
||||
@Override
|
||||
protected PyNoseTestProcessRunner createProcessRunner() throws Exception {
|
||||
return new PyNoseTestProcessRunner(".", 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Ensure slow test is not run when --attr="!slow" is provided
|
||||
@Test
|
||||
@@ -64,9 +72,9 @@ public final class PythonNoseTestingTest extends PyEnvTestCase {
|
||||
@NotNull String stderr,
|
||||
@NotNull String all) {
|
||||
Assert.assertEquals("--slow runner borken", "Test tree:\n" +
|
||||
"[root]\n" +
|
||||
".test_with_slow\n" +
|
||||
"..test_fast(+)\n",
|
||||
"[root]\n" +
|
||||
".test_with_slow\n" +
|
||||
"..test_fast(+)\n",
|
||||
runner.getFormattedTestTree());
|
||||
}
|
||||
});
|
||||
@@ -76,7 +84,8 @@ public final class PythonNoseTestingTest extends PyEnvTestCase {
|
||||
@Test
|
||||
public void testMultipleCases() throws Exception {
|
||||
runPythonTest(
|
||||
new CreateConfigurationMultipleCasesTask<>(PythonTestConfigurationsModel.PYTHONS_NOSETEST_NAME, PyUniversalNoseTestConfiguration.class));
|
||||
new CreateConfigurationMultipleCasesTask<>(PythonTestConfigurationsModel.PYTHONS_NOSETEST_NAME,
|
||||
PyUniversalNoseTestConfiguration.class));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -85,7 +94,8 @@ public final class PythonNoseTestingTest extends PyEnvTestCase {
|
||||
@Test
|
||||
public void testTestsInSubFolderResolvable() throws Exception {
|
||||
runPythonTest(
|
||||
new PyUnitTestProcessWithConsoleTestTask.PyTestsInSubFolderRunner<PyNoseTestProcessRunner>("test_metheggs", "test_funeggs", "test_first") {
|
||||
new PyUnitTestProcessWithConsoleTestTask.PyTestsInSubFolderRunner<PyNoseTestProcessRunner>("test_metheggs", "test_funeggs",
|
||||
"test_first") {
|
||||
@NotNull
|
||||
@Override
|
||||
protected PyNoseTestProcessRunner createProcessRunner() throws Exception {
|
||||
@@ -147,7 +157,7 @@ public final class PythonNoseTestingTest extends PyEnvTestCase {
|
||||
public void testConfigurationProducerOnDirectory() throws Exception {
|
||||
runPythonTest(
|
||||
new CreateConfigurationByFileTask.CreateConfigurationTestAndRenameFolderTask<>(PythonTestConfigurationsModel.PYTHONS_NOSETEST_NAME,
|
||||
PyUniversalNoseTestConfiguration.class));
|
||||
PyUniversalNoseTestConfiguration.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -50,6 +50,17 @@ public final class PythonPyTestingTest extends PyEnvTestCase {
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRerunSubfolder() throws Exception {
|
||||
runPythonTest(new RerunSubfolderTask<PyTestTestProcessRunner>(2) {
|
||||
@NotNull
|
||||
@Override
|
||||
protected PyTestTestProcessRunner createProcessRunner() throws Exception {
|
||||
return new PyTestTestProcessRunner(".", 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Ensure slow test is not run when -m "not slow" is provided
|
||||
@Test
|
||||
public void testMarkerWithSpaces() throws Exception {
|
||||
@@ -302,7 +313,7 @@ public final class PythonPyTestingTest extends PyEnvTestCase {
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure project dir is used as curdir even if not set explicitly
|
||||
* Ensure element dir is used as curdir even if not set explicitly
|
||||
*/
|
||||
@Test
|
||||
public void testCurrentDir() throws Exception {
|
||||
@@ -328,7 +339,7 @@ public final class PythonPyTestingTest extends PyEnvTestCase {
|
||||
@NotNull final String stdout,
|
||||
@NotNull final String stderr,
|
||||
@NotNull final String all) {
|
||||
final String projectDir = myFixture.getProject().getBaseDir().getPath();
|
||||
final String projectDir = myFixture.getTempDirFixture().getTempDirPath();
|
||||
Assert.assertThat("No directory found in output", runner.getConsole().getText(),
|
||||
Matchers.containsString(String.format("Directory %s", PathUtil.toSystemDependentName(projectDir))));
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@ import com.intellij.testFramework.EdtTestUtil;
|
||||
import com.intellij.testFramework.fixtures.CodeInsightTestFixture;
|
||||
import com.jetbrains.env.EnvTestTagsRequired;
|
||||
import com.jetbrains.env.PyEnvTestCase;
|
||||
import com.jetbrains.env.Staging;
|
||||
import com.jetbrains.env.ut.PyScriptTestProcessRunner;
|
||||
import com.jetbrains.env.ut.PyUnitTestProcessRunner;
|
||||
import com.jetbrains.python.PyBundle;
|
||||
@@ -51,7 +50,7 @@ public final class PythonUnitTestingTest extends PyEnvTestCase {
|
||||
// Ensures setup/teardown does not break anything
|
||||
@Test
|
||||
public void testSetupTearDown() throws Exception {
|
||||
runPythonTest(new SetupTearDownTestTask<PyUnitTestProcessRunner>(){
|
||||
runPythonTest(new SetupTearDownTestTask<PyUnitTestProcessRunner>() {
|
||||
@NotNull
|
||||
@Override
|
||||
protected PyUnitTestProcessRunner createProcessRunner() throws Exception {
|
||||
@@ -60,6 +59,59 @@ public final class PythonUnitTestingTest extends PyEnvTestCase {
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRerunSubfolder() throws Exception {
|
||||
runPythonTest(new RerunSubfolderTask<PyUnitTestProcessRunner>(1) {
|
||||
@NotNull
|
||||
@Override
|
||||
protected PyUnitTestProcessRunner createProcessRunner() throws Exception {
|
||||
return new PyUnitTestProcessRunner(".", 1) {
|
||||
@Override
|
||||
protected void configurationCreatedAndWillLaunch(@NotNull PyUniversalUnitTestConfiguration configuration) throws IOException {
|
||||
super.configurationCreatedAndWillLaunch(configuration);
|
||||
// Unittest can't find tests in folders with out of init.py, even in py2k, so we set working dir explicitly
|
||||
configuration.setWorkingDirectory(toFullPath("tests"));
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@EnvTestTagsRequired(tags = "python3") // Rerun for this scenario does not work for unitttest and py2
|
||||
//https://github.com/JetBrains/teamcity-messages/issues/129
|
||||
@Test
|
||||
public void testPackageInsideFolder() throws Exception {
|
||||
runPythonTest(new PyUnitTestProcessWithConsoleTestTask("testRunner/env/unit/package_in_folder", "tests") {
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
protected PyUnitTestProcessRunner createProcessRunner() throws Exception {
|
||||
// Full pass is required because it is folder
|
||||
return new PyUnitTestProcessRunner(toFullPath(myScriptName), 2) {
|
||||
@Override
|
||||
protected void configurationCreatedAndWillLaunch(@NotNull PyUniversalUnitTestConfiguration configuration) throws IOException {
|
||||
super.configurationCreatedAndWillLaunch(configuration);
|
||||
configuration.setWorkingDirectory(null); //Unset working dir: should be set to tests automatically
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void checkTestResults(@NotNull PyUnitTestProcessRunner runner,
|
||||
@NotNull String stdout,
|
||||
@NotNull String stderr,
|
||||
@NotNull String all) {
|
||||
if (runner.getCurrentRerunStep() == 0) {
|
||||
Assert.assertEquals(runner.getFormattedTestTree(), 2, runner.getAllTestsCount());
|
||||
}
|
||||
else {
|
||||
Assert.assertEquals(runner.getFormattedTestTree(), 1, runner.getAllTestsCount());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@EnvTestTagsRequired(tags = "python3") // No subtest in py2
|
||||
@Test
|
||||
public void testSubtest() throws Exception {
|
||||
@@ -107,7 +159,8 @@ public final class PythonUnitTestingTest extends PyEnvTestCase {
|
||||
@Test
|
||||
public void testMultipleCases() throws Exception {
|
||||
runPythonTest(
|
||||
new CreateConfigurationMultipleCasesTask<>(PythonTestConfigurationsModel.PYTHONS_UNITTEST_NAME, PyUniversalUnitTestConfiguration.class));
|
||||
new CreateConfigurationMultipleCasesTask<>(PythonTestConfigurationsModel.PYTHONS_UNITTEST_NAME,
|
||||
PyUniversalUnitTestConfiguration.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -182,7 +235,7 @@ public final class PythonUnitTestingTest extends PyEnvTestCase {
|
||||
public void testConfigurationProducerOnDirectory() throws Exception {
|
||||
runPythonTest(
|
||||
new CreateConfigurationByFileTask.CreateConfigurationTestAndRenameFolderTask<>(PythonTestConfigurationsModel.PYTHONS_UNITTEST_NAME,
|
||||
PyUniversalUnitTestConfiguration.class));
|
||||
PyUniversalUnitTestConfiguration.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -248,6 +301,15 @@ public final class PythonUnitTestingTest extends PyEnvTestCase {
|
||||
Assert.assertThat("Wrong number of failed tests", runner.getFailedTestsCount(), equalTo(1));
|
||||
final int expectedNumberOfTests = (runner.getCurrentRerunStep() == 0 ? 2 : 1);
|
||||
Assert.assertThat("Wrong number tests", runner.getAllTestsCount(), equalTo(expectedNumberOfTests));
|
||||
if (runner.getCurrentRerunStep() == 1) {
|
||||
// Make sure derived tests are launched, not abstract
|
||||
Assert.assertEquals("Wrong tests after rerun",
|
||||
"Test tree:\n" +
|
||||
"[root]\n" +
|
||||
".rerun_derived\n" +
|
||||
"..TestDerived\n" +
|
||||
"...test_a(-)\n", runner.getFormattedTestTree());
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
|
||||
48
python/testSrc/com/jetbrains/env/python/testing/RerunSubfolderTask.java
vendored
Normal file
48
python/testSrc/com/jetbrains/env/python/testing/RerunSubfolderTask.java
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* 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.env.python.testing;
|
||||
|
||||
import com.jetbrains.env.PyProcessWithConsoleTestTask;
|
||||
import com.jetbrains.env.ut.PyScriptTestProcessRunner;
|
||||
import com.jetbrains.python.sdkTools.SdkCreationType;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
/**
|
||||
* @author Ilya.Kazakevich
|
||||
*/
|
||||
abstract class RerunSubfolderTask<T extends PyScriptTestProcessRunner<?>>
|
||||
extends PyProcessWithConsoleTestTask<T> {
|
||||
|
||||
private final int myExpectedFailedTests;
|
||||
|
||||
protected RerunSubfolderTask(final int expectedFailedTests) {
|
||||
super("/testRunner/env/nose/subfolder_test", SdkCreationType.EMPTY_SDK);
|
||||
myExpectedFailedTests = expectedFailedTests;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected void checkTestResults(@NotNull T runner,
|
||||
@NotNull String stdout,
|
||||
@NotNull String stderr,
|
||||
@NotNull String all) {
|
||||
assertEquals(stderr, myExpectedFailedTests, runner.getFailedTestsCount());
|
||||
assertEquals(stderr, myExpectedFailedTests, runner.getAllTestsCount());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user