PY-71926 Refine test detection criteria

Add a new setting Python Integrated Tools: Detect tests in Jupyter Notebooks.
Exclude Jupyter Notebook files from the scope for test detection by default.
Add tests


Merge-request: IJ-MR-134248
Merged-by: Egor Eliseev <Egor.Eliseev@jetbrains.com>

GitOrigin-RevId: 0bee082bde4fa608cb1907b8fbd64b97bb9755a0
This commit is contained in:
Egor Eliseev
2024-07-26 17:24:30 +00:00
committed by intellij-monorepo-bot
parent 571ab91be6
commit 994775243f
8 changed files with 133 additions and 12 deletions

View File

@@ -741,6 +741,14 @@ The Python plug-in provides smart editing for Python scripts. The feature set of
<extensionPoint qualifiedName="Pythonid.customProcessHandlerProvider"
interface="com.jetbrains.python.run.PyCustomProcessHandlerProvider"
dynamic="true"/>
<extensionPoint qualifiedName="com.jetbrains.python.testing.pyTestLineMarkerContributorCustomizer"
interface="com.jetbrains.python.testing.PyTestLineMarkerContributorCustomizer"
dynamic="true"/>
<extensionPoint qualifiedName="com.jetbrains.python.configuration.pyIntegratedToolsTestPanelCustomizer"
interface="com.jetbrains.python.configuration.PyIntegratedToolsTestPanelCustomizer"
dynamic="true"/>
</extensionPoints>
<extensions defaultExtensionNs="Pythonid">

View File

@@ -121,28 +121,23 @@
</component>
</children>
</grid>
<grid id="edcb2" binding="myTestsPanel" layout-manager="GridLayoutManager" row-count="1" column-count="2" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="0" left="0" bottom="0" right="0"/>
<grid id="edcb2" binding="myTestsPanel" layout-manager="BorderLayout" hgap="0" vgap="0">
<constraints>
<grid row="2" column="0" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
</constraints>
<properties/>
<border type="none"/>
<children>
<component id="68c44" class="javax.swing.JComboBox" binding="myTestRunnerComboBox">
<constraints>
<grid row="0" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="2" anchor="8" fill="1" indent="0" use-parent-layout="false"/>
</constraints>
<properties/>
</component>
<component id="3f484" class="javax.swing.JLabel">
<constraints>
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<constraints border-constraint="West"/>
<properties>
<text resource-bundle="messages/PyBundle" key="form.integrated.tools.default.test.runner"/>
</properties>
</component>
<component id="68c44" class="javax.swing.JComboBox" binding="myTestRunnerComboBox">
<constraints border-constraint="Center"/>
<properties/>
</component>
</children>
</grid>
<grid id="9d84d" binding="myPipEnvPanel" layout-manager="GridLayoutManager" row-count="1" column-count="2" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">

View File

@@ -19,6 +19,7 @@ import com.intellij.openapi.project.DefaultProjectFactory;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.projectRoots.Sdk;
import com.intellij.openapi.roots.ProjectRootManager;
import com.intellij.openapi.ui.DialogPanel;
import com.intellij.openapi.ui.TextFieldWithBrowseButton;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VirtualFile;
@@ -30,6 +31,7 @@ import com.intellij.ui.components.JBTextField;
import com.intellij.util.FileContentUtil;
import com.intellij.util.FileContentUtilCore;
import com.intellij.util.ObjectUtils;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.ui.JBUI;
import com.jetbrains.python.PyBundle;
import com.jetbrains.python.PythonFileType;
@@ -77,6 +79,8 @@ public class PyIntegratedToolsConfigurable implements SearchableConfigurable {
private JPanel myTestsPanel;
private TextFieldWithBrowseButton myPipEnvPathField;
private JPanel myPipEnvPanel;
@NotNull
private final Collection<@NotNull DialogPanel> myCustomizePanels = PyIntegratedToolsTestPanelCustomizer.Companion.createPanels();
public PyIntegratedToolsConfigurable() {
@@ -184,6 +188,9 @@ public class PyIntegratedToolsConfigurable implements SearchableConfigurable {
public JComponent createComponent() {
myModel = PyTestRunConfigurationsModel.Companion.create(myModule);
myTestRunnerComboBox.setRenderer(new PyTestRunConfigurationRenderer(PythonSdkUtil.findPythonSdk(myModule)));
for (@NotNull DialogPanel panel : myCustomizePanels) {
myTestsPanel.add(BorderLayout.AFTER_LAST_LINE, panel);
}
updateConfigurations();
initErrorValidation();
@@ -224,7 +231,7 @@ public class PyIntegratedToolsConfigurable implements SearchableConfigurable {
if (!myPipEnvPathField.getText().equals(StringUtil.notNullize(PipenvKt.getPipEnvPath(PropertiesComponent.getInstance())))) {
return true;
}
return false;
return ContainerUtil.exists(myCustomizePanels, panel -> panel.isModified());
}
@Override
@@ -256,6 +263,10 @@ public class PyIntegratedToolsConfigurable implements SearchableConfigurable {
DaemonCodeAnalyzer.getInstance(myProject).restart();
PipenvKt.setPipEnvPath(PropertiesComponent.getInstance(), StringUtil.nullize(myPipEnvPathField.getText()));
for (@NotNull DialogPanel panel : myCustomizePanels) {
panel.apply();
}
}
public void reparseFiles(final List<String> extensions) {
@@ -298,6 +309,10 @@ public class PyIntegratedToolsConfigurable implements SearchableConfigurable {
}
}
}
for (@NotNull DialogPanel panel : myCustomizePanels) {
panel.reset();
}
}
@Override

View File

@@ -0,0 +1,20 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.configuration
import com.intellij.openapi.extensions.ExtensionPointName
import com.intellij.openapi.ui.DialogPanel
import com.intellij.util.concurrency.annotations.RequiresEdt
interface PyIntegratedToolsTestPanelCustomizer {
companion object {
private val EP_NAME: ExtensionPointName<PyIntegratedToolsTestPanelCustomizer> =
ExtensionPointName.create("com.jetbrains.python.configuration.pyIntegratedToolsTestPanelCustomizer")
@RequiresEdt
@JvmStatic
fun createPanels(): List<DialogPanel> = EP_NAME.extensionList.map { it.createPanel() }
}
@RequiresEdt
fun createPanel(): DialogPanel
}

View File

@@ -3,6 +3,7 @@ package com.jetbrains.python.testing
import com.intellij.execution.lineMarker.RunLineMarkerContributor
import com.intellij.icons.AllIcons
import com.intellij.openapi.extensions.ExtensionPointName
import com.intellij.openapi.project.DumbAware
import com.intellij.psi.PsiElement
import com.intellij.psi.impl.source.tree.LeafPsiElement
@@ -18,6 +19,9 @@ class PyTestLineMarkerContributor : RunLineMarkerContributor(), DumbAware {
return null
}
val testElement = element.parent ?: return null
if (!PyTestLineMarkerContributorCustomizer.shouldProcessElement(testElement)) {
return null
}
val typeEvalContext = TypeEvalContext.codeAnalysis(element.project, element.containingFile)
if ((testElement is PyClass || testElement is PyFunction)
@@ -27,3 +31,15 @@ class PyTestLineMarkerContributor : RunLineMarkerContributor(), DumbAware {
return null
}
}
interface PyTestLineMarkerContributorCustomizer {
companion object {
private val EP_NAME: ExtensionPointName<PyTestLineMarkerContributorCustomizer> =
ExtensionPointName.create("com.jetbrains.python.testing.pyTestLineMarkerContributorCustomizer")
@JvmStatic
fun shouldProcessElement(testElement: PsiElement): Boolean = EP_NAME.extensionList.all { it.isTestableElement(testElement) }
}
fun isTestableElement(testElement: PsiElement): Boolean = true
}

View File

@@ -0,0 +1 @@
from _pytest.fixtures import fixture

View File

@@ -0,0 +1,9 @@
import pytest
def foo(x):
return x + 1
def te<caret>st_foo():
assert foo(1) == 2

View File

@@ -0,0 +1,57 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.testing
import com.intellij.execution.lineMarker.RunLineMarkerContributor
import com.intellij.execution.lineMarker.RunLineMarkerContributor.Info
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.jetbrains.python.fixtures.PyTestCase
import junit.framework.TestCase
open class PyTestRunLineMarkerTest : PyTestCase() {
companion object {
const val TESTS_DIR = "/pyTestLineMarker/"
const val PYTHON_FILE = "pythonFile.py"
}
override fun getTestDataPath(): String = super.getTestDataPath() + TESTS_DIR
override fun setUp() {
super.setUp()
TestRunnerService.getInstance(myFixture.module).selectedFactory =
PythonTestConfigurationType.getInstance().pyTestFactory
myFixture.copyDirectoryToProject("", "")
}
protected fun getCaretElement(fileName: String): PsiElement? {
val psiFile = configureByFile(fileName)
TestCase.assertNotNull("Can't find test file", psiFile)
val element = psiFile?.findElementAt(myFixture.caretOffset)
TestCase.assertNotNull("Can't find caret element", element)
return element
}
protected open fun configureByFile(fileName: String): PsiFile? = myFixture.configureByFile(fileName)
private fun getInfo(element: PsiElement, lineMarkerContributor: RunLineMarkerContributor): Info? = lineMarkerContributor.getInfo(element)
protected fun assertInfoFound(element: PsiElement, lineMarkerContributor: RunLineMarkerContributor) {
val info = getInfo(element, lineMarkerContributor)
TestCase.assertNotNull("Info is not found", info)
if (info != null) {
TestCase.assertNotNull("Run icon is not found", info.icon)
}
}
protected fun assertInfoNotFound(element: PsiElement, lineMarkerContributor: RunLineMarkerContributor) {
TestCase.assertNull("Info is found", getInfo(element, lineMarkerContributor))
}
fun testPythonFile() {
val lineMarkerContributor = PyTestLineMarkerContributor()
val element = getCaretElement(PYTHON_FILE)
if (element != null) {
assertInfoFound(element, lineMarkerContributor)
}
}
}