diff --git a/platform/platform-api/src/com/intellij/ui/components/JBTextField.java b/platform/platform-api/src/com/intellij/ui/components/JBTextField.java index 3955083b535b..22a1b0e79a56 100644 --- a/platform/platform-api/src/com/intellij/ui/components/JBTextField.java +++ b/platform/platform-api/src/com/intellij/ui/components/JBTextField.java @@ -15,6 +15,7 @@ */ package com.intellij.ui.components; +import com.intellij.ui.TextAccessor; import com.intellij.util.ui.ComponentWithEmptyText; import com.intellij.util.ui.JBInsets; import com.intellij.util.ui.StatusText; @@ -26,7 +27,7 @@ import javax.swing.plaf.TextUI; import java.awt.*; import java.awt.event.MouseEvent; -public class JBTextField extends JTextField implements ComponentWithEmptyText { +public class JBTextField extends JTextField implements ComponentWithEmptyText, TextAccessor { private TextComponentEmptyText myEmptyText; public JBTextField() { diff --git a/platform/platform-impl/src/com/intellij/ui/EditorTextField.java b/platform/platform-impl/src/com/intellij/ui/EditorTextField.java index a02d823ee3a4..ecc71a463008 100644 --- a/platform/platform-impl/src/com/intellij/ui/EditorTextField.java +++ b/platform/platform-impl/src/com/intellij/ui/EditorTextField.java @@ -53,7 +53,7 @@ import java.util.List; /** * @author max */ -public class EditorTextField extends NonOpaquePanel implements DocumentListener, TextComponent, DataProvider, +public class EditorTextField extends NonOpaquePanel implements DocumentListener, TextComponent, DataProvider, TextAccessor, DocumentBasedComponent, FocusListener, MouseListener { private static final Logger LOG = Logger.getInstance("#com.intellij.ui.EditorTextField"); public static final Key SUPPLEMENTARY_KEY = Key.create("Supplementary"); @@ -234,6 +234,7 @@ public class EditorTextField extends NonOpaquePanel implements DocumentListener, } } + @Override public void setText(@Nullable final String text) { ApplicationManager.getApplication().runWriteAction(() -> CommandProcessor.getInstance().executeCommand(getProject(), () -> { myDocument.replaceString(0, myDocument.getTextLength(), StringUtil.notNullize(text)); diff --git a/python/src/com/jetbrains/extensions/TargetWithVariantExt.kt b/python/src/com/jetbrains/extensions/TargetWithVariantExt.kt new file mode 100644 index 000000000000..0caea52ceff0 --- /dev/null +++ b/python/src/com/jetbrains/extensions/TargetWithVariantExt.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2000-2018 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. + */ +package com.jetbrains.extensions + +import com.intellij.openapi.vfs.LocalFileSystem +import com.intellij.openapi.vfs.VfsUtil +import com.intellij.openapi.vfs.VirtualFile +import com.jetbrains.python.run.AbstractPythonRunConfiguration +import com.jetbrains.python.run.targetBasedConfiguration.PyRunTargetVariant +import com.jetbrains.python.run.targetBasedConfiguration.TargetWithVariant +import com.jetbrains.python.run.targetBasedConfiguration.targetAsPsiElement +import com.jetbrains.python.run.targetBasedConfiguration.targetAsVirtualFile + + +/** + * @see targetAsPsiElement + */ +fun TargetWithVariant.asPsiElement(configuration: AbstractPythonRunConfiguration<*>, + workingDirectory: VirtualFile? + = LocalFileSystem.getInstance().findFileByPath(configuration.getWorkingDirectorySafe())) = + target?.let { targetAsPsiElement(targetVariant, it, configuration, workingDirectory) } + + +/** + * @see targetAsVirtualFile + */ +fun TargetWithVariant.asVirtualFile() = target?.let { targetAsVirtualFile(targetVariant, it) } + +/** + * Sanity check for "target" value. Does not resolve target, only check its syntax + * CUSTOM type is not checked. + */ +fun TargetWithVariant.isWellFormed() = when (targetVariant) { + PyRunTargetVariant.PYTHON -> Regex("^[a-zA-Z0-9._]+[a-zA-Z0-9_]$").matches(target ?: "") + PyRunTargetVariant.PATH -> !VfsUtil.isBadName(target) + else -> true +} \ No newline at end of file diff --git a/python/src/com/jetbrains/python/run/AbstractPythonRunConfiguration.java b/python/src/com/jetbrains/python/run/AbstractPythonRunConfiguration.java index 2907031bc0fb..a670ef3740a3 100644 --- a/python/src/com/jetbrains/python/run/AbstractPythonRunConfiguration.java +++ b/python/src/com/jetbrains/python/run/AbstractPythonRunConfiguration.java @@ -133,6 +133,9 @@ public abstract class AbstractPythonRunConfiguration createConfigurationEditor(); + /** + * Always call super when overwriting this method + */ @Override public void checkConfiguration() throws RuntimeConfigurationException { super.checkConfiguration(); diff --git a/python/src/com/jetbrains/python/run/PyConfigurationRefactoring.kt b/python/src/com/jetbrains/python/run/PyConfigurationRefactoring.kt new file mode 100644 index 000000000000..cbe4aebe2094 --- /dev/null +++ b/python/src/com/jetbrains/python/run/PyConfigurationRefactoring.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2000-2018 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. + */ +package com.jetbrains.python.run + +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.psi.PsiElement +import com.intellij.refactoring.listeners.UndoRefactoringElementAdapter + +/** + * Tools to support refactoring for configurations + * that implements [com.intellij.execution.configurations.RefactoringListenerProvider] + */ + + +/** + * @see CompositeRefactoringElementListener + */ +abstract class UndoRefactoringCompletionListener : UndoRefactoringElementAdapter() { + public abstract override fun refactored(element: PsiElement, oldQualifiedName: String?) +} + +/** + * Chains several [com.intellij.refactoring.listeners.RefactoringElementListener] + */ +class CompositeRefactoringElementListener(private vararg val listeners: UndoRefactoringCompletionListener) : UndoRefactoringElementAdapter() { + override fun refactored(element: PsiElement, oldQualifiedName: String?) { + listeners.forEach { it.refactored(element, oldQualifiedName) } + } + + /** + * Creates new listener adding provided one + */ + operator fun plus(listener: UndoRefactoringCompletionListener) = CompositeRefactoringElementListener(*arrayOf(listener) + listeners) +} + + +/** + * Renames working directory if folder physically renamed + */ +class PyWorkingDirectoryRenamer(private val workingDirectoryFile: VirtualFile?, + private val conf: AbstractPythonRunConfiguration<*>) : UndoRefactoringCompletionListener() { + override fun refactored(element: PsiElement, oldQualifiedName: String?) { + workingDirectoryFile?.let { + conf.setWorkingDirectory(it.path) + } + } +} + diff --git a/python/src/com/jetbrains/python/run/PythonRunConfiguration.java b/python/src/com/jetbrains/python/run/PythonRunConfiguration.java index e990ed944a0f..49687a3eaea5 100644 --- a/python/src/com/jetbrains/python/run/PythonRunConfiguration.java +++ b/python/src/com/jetbrains/python/run/PythonRunConfiguration.java @@ -59,6 +59,7 @@ public class PythonRunConfiguration extends AbstractPythonRunConfiguration return new PythonScriptCommandLineState(this, env); } + @Override public void checkConfiguration() throws RuntimeConfigurationException { super.checkConfiguration(); diff --git a/python/src/com/jetbrains/python/run/targetBasedConfiguration/PyRunTargetVariant.kt b/python/src/com/jetbrains/python/run/targetBasedConfiguration/PyRunTargetVariant.kt new file mode 100644 index 000000000000..2f8dcbfaa88d --- /dev/null +++ b/python/src/com/jetbrains/python/run/targetBasedConfiguration/PyRunTargetVariant.kt @@ -0,0 +1,64 @@ +/* + * Copyright 2000-2018 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. + */ +package com.jetbrains.python.run.targetBasedConfiguration + +import com.intellij.openapi.vfs.LocalFileSystem +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.psi.PsiElement +import com.intellij.psi.util.QualifiedName +import com.jetbrains.extenstions.ModuleBasedContextAnchor +import com.jetbrains.extenstions.QNameResolveContext +import com.jetbrains.extenstions.resolveToElement +import com.jetbrains.python.psi.types.TypeEvalContext +import com.jetbrains.python.run.AbstractPythonRunConfiguration +import com.jetbrains.python.run.PythonRunConfigurationForm + +/** + * Types of target (symbol, path or custom) many python runners may have + */ +enum class PyRunTargetVariant(private val customName: String? = null) { + PYTHON(PythonRunConfigurationForm.MODULE_NAME), PATH(PythonRunConfigurationForm.SCRIPT_PATH), CUSTOM; + + fun getCustomName() = customName ?: name.toLowerCase().capitalize() +} + +/** + * Converts target to PSI element if possible resolving it against roots and working directory + */ +fun targetAsPsiElement(targetType: PyRunTargetVariant, + target: String, + configuration: AbstractPythonRunConfiguration<*>, + workingDirectory: VirtualFile? = LocalFileSystem.getInstance().findFileByPath( + configuration.getWorkingDirectorySafe())) + : PsiElement? { + if (targetType == PyRunTargetVariant.PYTHON) { + val module = configuration.getModule() ?: return null + val context = TypeEvalContext.userInitiated(configuration.getProject(), null) + + val name = QualifiedName.fromDottedString(target) + return name.resolveToElement(QNameResolveContext(ModuleBasedContextAnchor(module), configuration.getSdk(), + context, workingDirectory, true)) + } + return null +} + +/** + * Converts target to file if possible + */ +fun targetAsVirtualFile(targetType: PyRunTargetVariant, target: String): VirtualFile? { + if (targetType == PyRunTargetVariant.PATH) { + return LocalFileSystem.getInstance().findFileByPath(target) + } + return null +} + + +/** + * Implement it to obtain extension methods for [targetAsPsiElement] + * and [targetAsVirtualFile] + */ +interface TargetWithVariant { + val target: String? + val targetVariant: PyRunTargetVariant +} diff --git a/python/src/com/jetbrains/python/run/targetBasedConfiguration/PyTargetTypeRefactoring.kt b/python/src/com/jetbrains/python/run/targetBasedConfiguration/PyTargetTypeRefactoring.kt new file mode 100644 index 000000000000..12904e2476a3 --- /dev/null +++ b/python/src/com/jetbrains/python/run/targetBasedConfiguration/PyTargetTypeRefactoring.kt @@ -0,0 +1,69 @@ +/* + * Copyright 2000-2018 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. + */ +package com.jetbrains.python.run.targetBasedConfiguration + +import com.intellij.openapi.vfs.VfsUtil +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiFileSystemItem +import com.intellij.psi.PsiNamedElement +import com.intellij.psi.util.PsiTreeUtil +import com.jetbrains.extensions.getQName +import com.jetbrains.python.psi.PyQualifiedNameOwner +import com.jetbrains.python.run.UndoRefactoringCompletionListener + + +/** + * Creates listener that renames target. + * @param elementUnderRefactoring "element" argument of [com.intellij.execution.configurations.RefactoringListenerProvider.getRefactoringElementListener] + * @param targetPsiElement configuration psi element (see [targetAsPsiElement]] or null if configuration is not qname-based + * @param targetVirtualFile configuration virtual file element (see [targetAsVirtualFile]] or null if configuration is not file-based + * @param setTarget lambda to set target on rename + * @return renamer or null if [elementUnderRefactoring] does not affect current configuration + */ +fun createRefactoringListenerIfPossible(elementUnderRefactoring: PsiElement, + targetPsiElement: PsiElement?, + targetVirtualFile: VirtualFile?, + setTarget: (String) -> Unit) + : UndoRefactoringCompletionListener? { + if (targetPsiElement != null && PsiTreeUtil.isAncestor(elementUnderRefactoring, targetPsiElement, false)) { + return PyElementTargetRenamer(targetPsiElement, setTarget) + } + if (targetVirtualFile != null && elementUnderRefactoring is PsiFileSystemItem && VfsUtil.isAncestor( + elementUnderRefactoring.virtualFile, targetVirtualFile, false)) { + return PyVirtualFileRenamer(targetVirtualFile, setTarget) + } + return null +} + + +/** + * Renames python target if python symbol, module or folder renamed + */ +private class PyElementTargetRenamer(private val originalElement: PsiElement, private val setTarget: (String) -> Unit) : + UndoRefactoringCompletionListener() { + override fun refactored(element: PsiElement, oldQualifiedName: String?) { + if (originalElement is PyQualifiedNameOwner) { + originalElement.qualifiedName?.let { setTarget(it) } + return + } + if (originalElement is PsiFileSystemItem) { + originalElement.getQName()?.let { setTarget(it.toString()) } + return + } + else if (originalElement is PsiNamedElement) { + originalElement.name?.let { setTarget(it) } + } + } +} + +/** + * Renames folder target if file or folder really renamed + */ +private class PyVirtualFileRenamer(private val virtualFile: VirtualFile, private val setTarget: (String) -> Unit) : + UndoRefactoringCompletionListener() { + override fun refactored(element: PsiElement, oldQualifiedName: String?) { + setTarget(virtualFile.path) + } +} diff --git a/python/src/com/jetbrains/python/run/targetBasedConfiguration/package-info.java b/python/src/com/jetbrains/python/run/targetBasedConfiguration/package-info.java new file mode 100644 index 000000000000..a08d4dab959d --- /dev/null +++ b/python/src/com/jetbrains/python/run/targetBasedConfiguration/package-info.java @@ -0,0 +1,12 @@ +/** + * Copyright 2000-2018 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. + * + * For configurations that support target types: File system path and Qualified name path. + * Package has enum for this type and utility classes to resolve, refactoring etc + * + * + * There are also tools to support configuration refactoring for configurations based on + * {@link com.jetbrains.python.run.targetBasedConfiguration.PyRunTargetVariant} + * Configuration may be "qname-based" (PYTHON) or "file-path based" (SCRIPT_PATH) + */ +package com.jetbrains.python.run.targetBasedConfiguration; \ No newline at end of file diff --git a/python/src/com/jetbrains/python/testing/PyNoseTest.kt b/python/src/com/jetbrains/python/testing/PyNoseTest.kt index 5aef88e04d4a..6d380e704d1e 100644 --- a/python/src/com/jetbrains/python/testing/PyNoseTest.kt +++ b/python/src/com/jetbrains/python/testing/PyNoseTest.kt @@ -24,6 +24,7 @@ import com.intellij.openapi.options.SettingsEditor import com.intellij.openapi.project.Project import com.jetbrains.python.PyNames import com.jetbrains.python.PythonHelper +import com.jetbrains.python.run.targetBasedConfiguration.PyRunTargetVariant /** * Nose runner @@ -32,7 +33,7 @@ import com.jetbrains.python.PythonHelper class PyNoseTestSettingsEditor(configuration: PyAbstractTestConfiguration) : PyAbstractTestSettingsEditor( PyTestSharedForm.create(configuration, PyTestSharedForm.CustomOption( - PyNoseTestConfiguration::regexPattern.name, TestTargetType.PATH))) + PyNoseTestConfiguration::regexPattern.name, PyRunTargetVariant.PATH))) class PyNoseTestExecutionEnvironment(configuration: PyNoseTestConfiguration, environment: ExecutionEnvironment) : PyTestExecutionEnvironment(configuration, environment) { diff --git a/python/src/com/jetbrains/python/testing/PyTest.kt b/python/src/com/jetbrains/python/testing/PyTest.kt index 653430e6addb..b3158d1c99b8 100644 --- a/python/src/com/jetbrains/python/testing/PyTest.kt +++ b/python/src/com/jetbrains/python/testing/PyTest.kt @@ -22,6 +22,7 @@ import com.intellij.openapi.options.SettingsEditor import com.intellij.openapi.project.Project import com.jetbrains.python.PyNames import com.jetbrains.python.PythonHelper +import com.jetbrains.python.run.targetBasedConfiguration.PyRunTargetVariant /** * Py.test runner @@ -30,7 +31,7 @@ import com.jetbrains.python.PythonHelper class PyTestSettingsEditor(configuration: PyAbstractTestConfiguration) : PyAbstractTestSettingsEditor( PyTestSharedForm.create(configuration, PyTestSharedForm.CustomOption( - PyTestConfiguration::keywords.name, TestTargetType.PATH, TestTargetType.PYTHON))) + PyTestConfiguration::keywords.name, PyRunTargetVariant.PATH, PyRunTargetVariant.PYTHON))) class PyPyTestExecutionEnvironment(configuration: PyTestConfiguration, environment: ExecutionEnvironment) : PyTestExecutionEnvironment(configuration, environment) { diff --git a/python/src/com/jetbrains/python/testing/PyTestLegacyInterop.kt b/python/src/com/jetbrains/python/testing/PyTestLegacyInterop.kt index 0b987a05e4c2..5d625fb739b1 100644 --- a/python/src/com/jetbrains/python/testing/PyTestLegacyInterop.kt +++ b/python/src/com/jetbrains/python/testing/PyTestLegacyInterop.kt @@ -36,6 +36,7 @@ import com.jetbrains.python.psi.PyQualifiedNameOwner import com.jetbrains.python.psi.PyUtil import com.jetbrains.python.psi.types.TypeEvalContext import com.jetbrains.python.run.PythonConfigurationFactoryBase +import com.jetbrains.python.run.targetBasedConfiguration.PyRunTargetVariant import com.jetbrains.python.testing.AbstractPythonLegacyTestRunConfiguration.TestType import com.jetbrains.python.testing.doctest.PythonDocTestConfigurationProducer import com.jetbrains.python.testing.nosetestLegacy.PythonNoseTestRunConfiguration @@ -262,15 +263,15 @@ private abstract class LegacyConfigurationManager< val virtualFile = getVirtualFileByPath(legacyConfig.scriptName) ?: return val pyFile = virtualFile.asPyFile(legacyConfig.project) ?: return val qualifiedName = getElementFromConfig(pyFile)?.qualifiedName ?: return - newConfig.target.targetType = TestTargetType.PYTHON + newConfig.target.targetVariant = PyRunTargetVariant.PYTHON newConfig.target.target = qualifiedName } TestType.TEST_FOLDER -> { - newConfig.target.targetType = TestTargetType.PATH + newConfig.target.targetVariant = PyRunTargetVariant.PATH newConfig.target.target = legacyConfig.folderName } TestType.TEST_SCRIPT -> { - newConfig.target.targetType = TestTargetType.PATH + newConfig.target.targetVariant = PyRunTargetVariant.PATH newConfig.target.target = legacyConfig.scriptName } else -> { @@ -297,7 +298,7 @@ private class LegacyConfigurationManagerPyTest(newConfig: PyTestConfiguration) : newConfig.additionalArguments = legacyConfig.params // Default is PATH - newConfig.target.targetType = TestTargetType.PATH + newConfig.target.targetVariant = PyRunTargetVariant.PATH val oldKeywords = legacyConfig.keywords @@ -305,7 +306,7 @@ private class LegacyConfigurationManagerPyTest(newConfig: PyTestConfiguration) : if (virtualFile.isDirectory) { // If target is directory, then it can't point to any symbol newConfig.target.target = virtualFile.path - newConfig.target.targetType = TestTargetType.PATH + newConfig.target.targetVariant = PyRunTargetVariant.PATH newConfig.keywords = oldKeywords return } @@ -318,7 +319,7 @@ private class LegacyConfigurationManagerPyTest(newConfig: PyTestConfiguration) : //Give up with interpreting newConfig.keywords = oldKeywords newConfig.target.target = script.virtualFile.path - newConfig.target.targetType = TestTargetType.PATH + newConfig.target.targetVariant = PyRunTargetVariant.PATH return } val classOrFunctionName = keywordsList[0] @@ -329,13 +330,13 @@ private class LegacyConfigurationManagerPyTest(newConfig: PyTestConfiguration) : script.findTopLevelFunction(classOrFunctionName), PyQualifiedNameOwner::class.java) ?: return newConfig.target.target = classOrFunction.qualifiedName ?: return - newConfig.target.targetType = TestTargetType.PYTHON + newConfig.target.targetVariant = PyRunTargetVariant.PYTHON } if (keywordsList.size == 2) { // Class and method clazz ?: return val method = clazz.findMethodByName(keywordsList[1], true, TypeEvalContext.userInitiated(newConfig.project, script)) ?: return newConfig.target.target = method.qualifiedName ?: return - newConfig.target.targetType = TestTargetType.PYTHON + newConfig.target.targetVariant = PyRunTargetVariant.PYTHON } diff --git a/python/src/com/jetbrains/python/testing/PyTestSharedForm.java b/python/src/com/jetbrains/python/testing/PyTestSharedForm.java index 26dced64c331..3635eacacc99 100644 --- a/python/src/com/jetbrains/python/testing/PyTestSharedForm.java +++ b/python/src/com/jetbrains/python/testing/PyTestSharedForm.java @@ -16,16 +16,12 @@ package com.jetbrains.python.testing; import com.google.common.collect.ObjectArrays; -import com.intellij.openapi.fileChooser.FileChooserDescriptor; -import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory; import com.intellij.openapi.module.Module; import com.intellij.openapi.project.Project; -import com.intellij.openapi.ui.TextBrowseFolderListener; import com.intellij.openapi.ui.TextFieldWithBrowseButton; import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vfs.LocalFileSystem; -import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiDirectory; import com.intellij.ui.IdeBorderFactory; import com.intellij.ui.TextAccessor; @@ -35,15 +31,14 @@ import com.intellij.uiDesigner.core.GridConstraints; import com.intellij.util.ThreeState; import com.intellij.util.ui.JBUI; import com.jetbrains.PySymbolFieldWithBrowseButton; -import com.jetbrains.extensions.python.FileChooserDescriptorExtKt; import com.jetbrains.extenstions.ContextAnchor; import com.jetbrains.extenstions.ModuleBasedContextAnchor; import com.jetbrains.extenstions.ProjectSdkContextAnchor; -import com.jetbrains.python.PyNames; import com.jetbrains.python.psi.types.TypeEvalContext; import com.jetbrains.python.run.AbstractPyCommonOptionsForm; import com.jetbrains.python.run.PyBrowseActionListener; import com.jetbrains.python.run.PyCommonOptionsFormFactory; +import com.jetbrains.python.run.targetBasedConfiguration.PyRunTargetVariant; import com.jetbrains.reflection.ReflectionUtilsKt; import com.jetbrains.reflection.SimplePropertiesProvider; import org.jetbrains.annotations.NotNull; @@ -156,7 +151,7 @@ public final class PyTestSharedForm implements SimplePropertiesProvider { @NotNull final CustomOption... customOptions) { final PyTestSharedForm form = new PyTestSharedForm(configuration.getModule(), configuration); - for (final TestTargetType testTargetType : TestTargetType.values()) { + for (final PyRunTargetVariant testTargetType : PyRunTargetVariant.values()) { final JBRadioButton button = new JBRadioButton(StringUtil.capitalize(testTargetType.getCustomName().toLowerCase(Locale.getDefault()))); button.setActionCommand(testTargetType.name()); @@ -174,7 +169,7 @@ public final class PyTestSharedForm implements SimplePropertiesProvider { setBorderToPanel(form.myPanel, configuration.getTestFrameworkName()); form.addCustomOptions( - ObjectArrays.concat(customOptions, new CustomOption(PyTestsSharedKt.getAdditionalArgumentsPropertyName(), TestTargetType.values())) + ObjectArrays.concat(customOptions, new CustomOption(PyTestsSharedKt.getAdditionalArgumentsPropertyName(), PyRunTargetVariant.values())) ); configuration.copyTo(ReflectionUtilsKt.getProperties(form, null, true)); return form; @@ -226,7 +221,7 @@ public final class PyTestSharedForm implements SimplePropertiesProvider { // We should always use system-independent path because only this type of path is processed correctly // when stored (folder changed to macros to prevent hard code) final String targetText = getActiveTextField().getText().trim(); - return getTargetType() == TestTargetType.PATH ? FileUtil.toSystemIndependentName(targetText) : targetText; + return getTargetType() == PyRunTargetVariant.PATH ? FileUtil.toSystemIndependentName(targetText) : targetText; } @@ -235,7 +230,7 @@ public final class PyTestSharedForm implements SimplePropertiesProvider { } private void onTargetTypeChanged() { - final TestTargetType targetType = getTargetType(); + final PyRunTargetVariant targetType = getTargetType(); for (final OptionHolder optionHolder : myCustomOptions.values()) { optionHolder.setType(targetType); @@ -246,31 +241,31 @@ public final class PyTestSharedForm implements SimplePropertiesProvider { cons.fill = GridBagConstraints.HORIZONTAL; cons.weightx = 1; - if (targetType == TestTargetType.PATH) { + if (targetType == PyRunTargetVariant.PATH) { myPanelForTargetFields.add(myPathTarget, cons); } - else if (targetType == TestTargetType.PYTHON) { + else if (targetType == PyRunTargetVariant.PYTHON) { myPanelForTargetFields.add(myPythonTarget, cons); } } @NotNull private TextAccessor getActiveTextField() { - return (getTargetType() == TestTargetType.PATH ? myPathTarget : myPythonTarget); + return (getTargetType() == PyRunTargetVariant.PATH ? myPathTarget : myPythonTarget); } @SuppressWarnings("WeakerAccess") // Accessor for property @NotNull - public TestTargetType getTargetType() { - return TestTargetType.valueOf(myButtonGroup.getSelection().getActionCommand()); + public PyRunTargetVariant getTargetType() { + return PyRunTargetVariant.valueOf(myButtonGroup.getSelection().getActionCommand()); } @SuppressWarnings("unused") // Mutator for property - public void setTargetType(@NotNull final TestTargetType target) { + public void setTargetType(@NotNull final PyRunTargetVariant target) { final Enumeration elements = myButtonGroup.getElements(); while (elements.hasMoreElements()) { final AbstractButton button = elements.nextElement(); - if (TestTargetType.valueOf(button.getActionCommand()) == target) { + if (PyRunTargetVariant.valueOf(button.getActionCommand()) == target) { myButtonGroup.setSelected(button.getModel(), true); break; } @@ -287,10 +282,10 @@ public final class PyTestSharedForm implements SimplePropertiesProvider { /** * Types to display this option for */ - private final EnumSet mySupportedTypes; + private final EnumSet mySupportedTypes; CustomOption(@NotNull final String name, - @NotNull final TestTargetType... supportedTypes) { + @NotNull final PyRunTargetVariant... supportedTypes) { myName = name; mySupportedTypes = EnumSet.copyOf(Arrays.asList(supportedTypes)); } @@ -312,7 +307,7 @@ public final class PyTestSharedForm implements SimplePropertiesProvider { myOptionValue = optionValue; } - private void setType(@NotNull final TestTargetType type) { + private void setType(@NotNull final PyRunTargetVariant type) { final boolean visible = myOption.mySupportedTypes.contains(type); myOptionLabel.setVisible(visible); myOptionValue.setVisible(visible); diff --git a/python/src/com/jetbrains/python/testing/PyTestsShared.kt b/python/src/com/jetbrains/python/testing/PyTestsShared.kt index be11ae698615..0300d4a2f9ea 100644 --- a/python/src/com/jetbrains/python/testing/PyTestsShared.kt +++ b/python/src/com/jetbrains/python/testing/PyTestsShared.kt @@ -28,7 +28,6 @@ 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.ModuleUtilCore import com.intellij.openapi.module.impl.scopes.ModuleWithDependenciesScope import com.intellij.openapi.options.SettingsEditor import com.intellij.openapi.project.Project @@ -43,14 +42,14 @@ import com.intellij.openapi.vfs.VirtualFileManager 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.asPsiElement +import com.jetbrains.extensions.asVirtualFile import com.jetbrains.extensions.getQName +import com.jetbrains.extensions.isWellFormed import com.jetbrains.extenstions.ModuleBasedContextAnchor import com.jetbrains.extenstions.QNameResolveContext import com.jetbrains.extenstions.getElementAndResolvableName @@ -61,6 +60,9 @@ import com.jetbrains.python.psi.PyFunction import com.jetbrains.python.psi.PyQualifiedNameOwner import com.jetbrains.python.psi.types.TypeEvalContext import com.jetbrains.python.run.* +import com.jetbrains.python.run.targetBasedConfiguration.PyRunTargetVariant +import com.jetbrains.python.run.targetBasedConfiguration.TargetWithVariant +import com.jetbrains.python.run.targetBasedConfiguration.createRefactoringListenerIfPossible import com.jetbrains.reflection.DelegationProperty import com.jetbrains.reflection.Properties import com.jetbrains.reflection.Property @@ -87,7 +89,7 @@ val factories: Array = arrayOf( fun processTCMessage(text: String): String { val parsedMessage = ServiceMessage.parse(text.trim()) ?: return text // Not a TC message return when (parsedMessage) { - is TestStdOut -> parsedMessage.stdOut // TC with stdout + is TestStdOut -> parsedMessage.stdOut // TC with stdout is TestStdErr -> parsedMessage.stdErr // TC with stderr else -> "" // TC with out of any output } @@ -152,6 +154,7 @@ private fun findConfigurationFactoryFromSettings(module: Module): ConfigurationF // folder provided by python side. Resolve test names versus it private val PATH_URL = java.util.regex.Pattern.compile("^python<([^<>]+)>$") + /** * Resolves url into element */ @@ -190,7 +193,7 @@ private fun getElementByUrl(protocol: String, // so we cut them out of path not to provide unsupported targets to runners val pathNoParentheses = QualifiedName.fromComponents( qualifiedName.components.filter { !it.contains('(') }).toString() - PyTargetBasedPsiLocation(ConfigurationTarget(pathNoParentheses, TestTargetType.PYTHON), element) + PyTargetBasedPsiLocation(ConfigurationTarget(pathNoParentheses, PyRunTargetVariant.PYTHON), element) } else { null @@ -222,6 +225,7 @@ object PyTestsLocator : SMTestLocator { } } + abstract class PyTestExecutionEnvironment(configuration: T, environment: ExecutionEnvironment) : PythonTestCommandLineStateBase(configuration, environment) { @@ -256,12 +260,6 @@ abstract class PyAbstractTestSettingsEditor(private val sharedForm: PyTestShared override fun createEditor(): javax.swing.JComponent = sharedForm.panel } -enum class TestTargetType(private val customName: String? = null) { - PYTHON(PythonRunConfigurationForm.MODULE_NAME), PATH(PythonRunConfigurationForm.SCRIPT_PATH), CUSTOM; - - fun getCustomName() = customName ?: name -} - /** * Default target path (run all tests ion project folder) */ @@ -270,61 +268,39 @@ private val DEFAULT_PATH = "" /** * Target depends on target type. It could be path to file/folder or python target */ -data class ConfigurationTarget(@ConfigField var target: String, - @ConfigField var targetType: TestTargetType) { +data class ConfigurationTarget(@ConfigField override var target: String, + @ConfigField override var targetVariant: PyRunTargetVariant) : TargetWithVariant { fun copyTo(dst: ConfigurationTarget) { // TODO: do we have such method it in Kotlin? dst.target = target - dst.targetType = targetType + dst.targetVariant = targetVariant } /** * Validates configuration and throws exception if target is invalid */ fun checkValid() { - if (targetType != TestTargetType.CUSTOM && target.isEmpty()) { + if (targetVariant != PyRunTargetVariant.CUSTOM && target.isEmpty()) { throw RuntimeConfigurationWarning("Target not provided") } - if (targetType == TestTargetType.PYTHON && !Regex("^[a-zA-Z0-9._]+[a-zA-Z0-9_]$").matches(target)) { - throw RuntimeConfigurationWarning("Provide a qualified name of function, class or a module") + if (targetVariant == PyRunTargetVariant.PYTHON && !isWellFormed()) { + throw RuntimeConfigurationError("Provide a qualified name of function, class or a module") } } - /** - * Converts target to PSI element if possible resolving it against roots and working directory - */ - fun asPsiElement(configuration: PyAbstractTestConfiguration): PsiElement? { - if (targetType == TestTargetType.PYTHON) { - val module = configuration.module ?: return null - val context = TypeEvalContext.userInitiated(configuration.project, null) - val workDir = configuration.getWorkingDirectoryAsVirtual() - val name = QualifiedName.fromDottedString(target) - return name.resolveToElement(QNameResolveContext(ModuleBasedContextAnchor(module), configuration.sdk, context, workDir, true)) - } - return null - } - - /** - * Converts target to file if possible - */ - fun asVirtualFile(): VirtualFile? { - if (targetType == TestTargetType.PATH) { - return LocalFileSystem.getInstance().findFileByPath(target) - } - return null - } + fun asPsiElement(configuration: PyAbstractTestConfiguration) = + asPsiElement(configuration, configuration.getWorkingDirectoryAsVirtual()) fun generateArgumentsLine(configuration: PyAbstractTestConfiguration): List = - when (targetType) { - TestTargetType.CUSTOM -> emptyList() - TestTargetType.PYTHON -> getArgumentsForPythonTarget(configuration) - TestTargetType.PATH -> listOf("--path", target.trim()) + when (targetVariant) { + PyRunTargetVariant.CUSTOM -> emptyList() + PyRunTargetVariant.PYTHON -> getArgumentsForPythonTarget(configuration) + PyRunTargetVariant.PATH -> listOf("--path", target.trim()) } private fun getArgumentsForPythonTarget(configuration: PyAbstractTestConfiguration): List { - val element = asPsiElement(configuration) ?: - throw ExecutionException( - "Can't resolve $target. Try to remove configuration and generate it again") + val element = asPsiElement(configuration) ?: throw ExecutionException( + "Can't resolve $target. Try to remove configuration and generate it again") if (element is PsiDirectory) { // Directory is special case: we can't run it as package for now, so we run it as path @@ -338,9 +314,9 @@ data class ConfigurationTarget(@ConfigField var target: String, folderToStart = LocalFileSystem.getInstance().findFileByPath(configuration.workingDirectorySafe), allowInaccurateResult = true ) - val qualifiedNameParts = QualifiedName.fromDottedString(target.trim()).tryResolveAndSplit(qNameResolveContext) ?: - throw 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 @@ -358,9 +334,8 @@ data class ConfigurationTarget(@ConfigField var target: String, return listOf("--target", elementAndName.name.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?") + 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()) } @@ -419,7 +394,7 @@ abstract class PyAbstractTestConfiguration(project: Project, : AbstractPythonTestRunConfiguration(project, configurationFactory), PyRerunAwareConfiguration, RefactoringListenerProvider { @DelegationProperty - val target = ConfigurationTarget(DEFAULT_PATH, TestTargetType.PATH) + val target = ConfigurationTarget(DEFAULT_PATH, PyRunTargetVariant.PATH) @ConfigField var additionalArguments = "" @@ -441,16 +416,6 @@ abstract class PyAbstractTestConfiguration(project: Project, @DelegationProperty val legacyConfigurationAdapter = PyTestLegacyConfigurationAdapter(this) - /** - * Renames working directory if folder physically renamed - */ - private open inner class PyConfigurationRenamer(private val workingDirectoryFile: VirtualFile?) : UndoRefactoringElementAdapter() { - override fun refactored(element: PsiElement, oldQualifiedName: String?) { - if (workingDirectoryFile != null) { - workingDirectory = workingDirectoryFile.path - } - } - } /** * For real launch use [getWorkingDirectorySafe] instead @@ -471,51 +436,17 @@ abstract class PyAbstractTestConfiguration(project: Project, return target.getElementDirectory(this)?.path ?: super.getWorkingDirectorySafe() } - /** - * Renames python target if python symbol, module or folder renamed - */ - 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 PyQualifiedNameOwner) { - target.target = originalElement.qualifiedName ?: return - } - else if (originalElement is PsiNamedElement) { - target.target = originalElement.name ?: return - } - } - } - - /** - * Renames folder target if file or folder really renamed - */ - 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: PsiElement?): RefactoringElementListener? { - val targetElement = target.asPsiElement(this) - val workingDirectoryFile = getWorkingDirectoryAsVirtual() - val targetFile = target.asVirtualFile() - - - if (targetElement != null && PsiTreeUtil.isAncestor(element, targetElement, false)) { - return PyElementTargetRenamer(targetElement, workingDirectoryFile) + if (element == null) return null + var renamer = CompositeRefactoringElementListener(PyWorkingDirectoryRenamer(getWorkingDirectoryAsVirtual(), this)) + createRefactoringListenerIfPossible(element, target.asPsiElement(this), target.asVirtualFile(), { target.target = it })?.let { + renamer = renamer.plus(it) } - if (targetFile != null && element is PsiFileSystemItem && VfsUtil.isAncestor( - element.virtualFile, targetFile, false)) { - return PyVirtualFileRenamer(targetFile, workingDirectoryFile) - } - return null + return renamer } + override fun checkConfiguration() { super.checkConfiguration() if (!isFrameworkInstalled()) { @@ -548,7 +479,7 @@ abstract class PyAbstractTestConfiguration(project: Project, val qualifiedName = (location.psiElement as PyQualifiedNameOwner).qualifiedName ?: return emptyList() // Resolve name as python qname as last resort - return ConfigurationTarget(qualifiedName, TestTargetType.PYTHON).generateArgumentsLine(this) + return ConfigurationTarget(qualifiedName, PyRunTargetVariant.PYTHON).generateArgumentsLine(this) } override fun getTestSpec(location: Location<*>, @@ -588,12 +519,12 @@ abstract class PyAbstractTestConfiguration(project: Project, } override fun suggestedName() = - when (target.targetType) { - TestTargetType.PATH -> { + when (target.targetVariant) { + PyRunTargetVariant.PATH -> { val name = target.asVirtualFile()?.name "$testFrameworkName in " + (name ?: target.target) } - TestTargetType.PYTHON -> { + PyRunTargetVariant.PYTHON -> { "$testFrameworkName for " + target.target } else -> { @@ -609,7 +540,7 @@ abstract class PyAbstractTestConfiguration(project: Project, fun reset() { target.target = DEFAULT_PATH - target.targetType = TestTargetType.PATH + target.targetVariant = PyRunTargetVariant.PATH additionalArguments = "" } @@ -660,8 +591,8 @@ abstract class PyAbstractTestConfiguration(project: Project, * Checks if element could be test target for this config. * Function is used to create tests by context. * - * If yes, and element is [PsiElement] then it is [TestTargetType.PYTHON]. - * If file then [TestTargetType.PATH] + * If yes, and element is [PsiElement] then it is [PyRunTargetVariant.PYTHON]. + * If file then [PyRunTargetVariant.PATH] */ fun couldBeTestTarget(element: PsiElement): Boolean { @@ -694,21 +625,6 @@ object PyTestsConfigurationProducer : AbstractPythonTestConfigurationProducer { @@ -817,7 +733,7 @@ object PyTestsConfigurationProducer : AbstractPythonTestConfigurationProducer element else -> return null }?.virtualFile?.path ?: return null - return Pair(ConfigurationTarget(path.path, TestTargetType.PATH), workingDirectory) + return Pair(ConfigurationTarget(path.path, PyRunTargetVariant.PATH), workingDirectory) } } } diff --git a/python/src/com/jetbrains/python/testing/PyUnitTest.kt b/python/src/com/jetbrains/python/testing/PyUnitTest.kt index ac3816f2429c..2c67485d60aa 100644 --- a/python/src/com/jetbrains/python/testing/PyUnitTest.kt +++ b/python/src/com/jetbrains/python/testing/PyUnitTest.kt @@ -26,6 +26,7 @@ import com.intellij.openapi.project.Project import com.intellij.openapi.vfs.LocalFileSystem import com.jetbrains.python.PyNames import com.jetbrains.python.PythonHelper +import com.jetbrains.python.run.targetBasedConfiguration.PyRunTargetVariant /** * unittest @@ -34,7 +35,7 @@ import com.jetbrains.python.PythonHelper class PyUnitTestSettingsEditor(configuration: PyAbstractTestConfiguration) : PyAbstractTestSettingsEditor( PyTestSharedForm.create(configuration, - PyTestSharedForm.CustomOption(PyUnitTestConfiguration::pattern.name, TestTargetType.PATH) + PyTestSharedForm.CustomOption(PyUnitTestConfiguration::pattern.name, PyRunTargetVariant.PATH) )) class PyUnitTestExecutionEnvironment(configuration: PyUnitTestConfiguration, environment: ExecutionEnvironment) : @@ -66,7 +67,7 @@ class PyUnitTestConfiguration(project: Project, factory: PyUnitTestFactory) : override fun getCustomRawArgumentsString(forRerun: Boolean): String { // Pattern can only be used with folders ("all in folder" in legacy terms) - if ((!pattern.isNullOrEmpty()) && target.targetType != TestTargetType.CUSTOM) { + if ((!pattern.isNullOrEmpty()) && target.targetVariant != PyRunTargetVariant.CUSTOM) { val path = LocalFileSystem.getInstance().findFileByPath(target.target) ?: return "" // "Pattern" works only for "discovery" mode and for "rerun" we are using "python" targets ("concrete" tests) return if (path.isDirectory && !forRerun) "-p $pattern" else "" @@ -81,7 +82,7 @@ class PyUnitTestConfiguration(project: Project, factory: PyUnitTestFactory) : * @return configuration should use runner for setup.py */ internal fun isSetupPyBased(): Boolean { - val setupPy = target.targetType == TestTargetType.PATH && target.target.endsWith(PyNames.SETUP_DOT_PY) + val setupPy = target.targetVariant == PyRunTargetVariant.PATH && target.target.endsWith(PyNames.SETUP_DOT_PY) return setupPy } @@ -90,7 +91,7 @@ class PyUnitTestConfiguration(project: Project, factory: PyUnitTestFactory) : override fun checkConfiguration() { super.checkConfiguration() - if (target.targetType == TestTargetType.PATH && target.target.endsWith(".py") && !pattern.isNullOrEmpty()) { + if (target.targetVariant == PyRunTargetVariant.PATH && target.target.endsWith(".py") && !pattern.isNullOrEmpty()) { throw RuntimeConfigurationWarning("Pattern can only be used to match files in folder. Can't use pattern for file.") } } diff --git a/python/testSrc/com/jetbrains/env/python/testing/CreateConfigurationMultipleCasesTask.java b/python/testSrc/com/jetbrains/env/python/testing/CreateConfigurationMultipleCasesTask.java index 11fb5d368af4..32b4a8af63e6 100644 --- a/python/testSrc/com/jetbrains/env/python/testing/CreateConfigurationMultipleCasesTask.java +++ b/python/testSrc/com/jetbrains/env/python/testing/CreateConfigurationMultipleCasesTask.java @@ -18,12 +18,13 @@ package com.jetbrains.env.python.testing; import com.intellij.openapi.vfs.LocalFileSystem; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.*; +import com.jetbrains.extensions.TargetWithVariantExtKt; 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 com.jetbrains.python.run.targetBasedConfiguration.PyRunTargetVariant; import org.hamcrest.Matchers; import org.jetbrains.annotations.NotNull; import org.junit.Assert; @@ -89,11 +90,11 @@ class CreateConfigurationMultipleCasesTask