From be0a2a737eb3b0c8a827ab0001495fdb62f1bc02 Mon Sep 17 00:00:00 2001 From: chbndrhnns Date: Sat, 6 Dec 2025 15:30:13 +0000 Subject: [PATCH] [python] PY-72251 Missing test run gutter icon for nested test classes Merge-request: IJ-MR-181131 Merged-by: Egor Eliseev GitOrigin-RevId: bae956b8083159c8f28391d058306afef60bf349 --- .../python/testing/PythonUnitTestDetectors.kt | 21 +++++++++++-------- .../testData/pyTestLineMarker/nestedTest.py | 8 +++++++ .../python/testing/PyTestRunLineMarkerTest.kt | 9 ++++++++ 3 files changed, 29 insertions(+), 9 deletions(-) create mode 100644 python/testData/pyTestLineMarker/nestedTest.py diff --git a/python/python-psi-impl/src/com/jetbrains/python/testing/PythonUnitTestDetectors.kt b/python/python-psi-impl/src/com/jetbrains/python/testing/PythonUnitTestDetectors.kt index 1f020601ec3e..25e511cb9191 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/testing/PythonUnitTestDetectors.kt +++ b/python/python-psi-impl/src/com/jetbrains/python/testing/PythonUnitTestDetectors.kt @@ -54,21 +54,24 @@ fun isTestClass(clazz: PyClass, context: TypeEvalContext): Boolean { return false } - object : Processor { - var hasTestFunction: Boolean = false - private set - + // Check if any method is a test method or setUp + var hasTestMethod = false + clazz.visitMethods(object : Processor { override fun process(function: PyFunction): Boolean { if (isTestFunction(function) || function.name?.equals("setUp") == true) { - hasTestFunction = true + hasTestMethod = true return false } return true } - }.apply { - clazz.visitMethods(this, true, context) - return hasTestFunction - } + }, true, context) + + // Check nested classes + val hasTestInNested = clazz.statementList.statements?.any { stmt -> + stmt is PyClass && isTestClass(stmt, context) + } ?: false + + return hasTestMethod || hasTestInNested } fun isTestFile( diff --git a/python/testData/pyTestLineMarker/nestedTest.py b/python/testData/pyTestLineMarker/nestedTest.py new file mode 100644 index 000000000000..106d5dba557e --- /dev/null +++ b/python/testData/pyTestLineMarker/nestedTest.py @@ -0,0 +1,8 @@ +# Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +import pytest + +class TestOutermost: + class TestAlsoOuter: + class TestInner: + def test_(self): + pass \ No newline at end of file diff --git a/python/testSrc/com/jetbrains/python/testing/PyTestRunLineMarkerTest.kt b/python/testSrc/com/jetbrains/python/testing/PyTestRunLineMarkerTest.kt index 65ff332e55e9..4439ae7266d1 100644 --- a/python/testSrc/com/jetbrains/python/testing/PyTestRunLineMarkerTest.kt +++ b/python/testSrc/com/jetbrains/python/testing/PyTestRunLineMarkerTest.kt @@ -12,6 +12,7 @@ open class PyTestRunLineMarkerTest : PyTestCase() { const val TESTS_DIR = "/pyTestLineMarker/" const val PYTHON_FILE = "pythonFile.py" const val FIXTURE_FILE = "fixtureTest.py" + const val PYTHON_FILE_WITH_NESTED_CLASSES = "nestedTest.py" } override fun getTestDataPath(): String = super.getTestDataPath() + TESTS_DIR @@ -73,4 +74,12 @@ open class PyTestRunLineMarkerTest : PyTestCase() { assertNotNull(fixtureElement) assertInfoNotFound(fixtureElement!!, lineMarkerContributor) } + + fun testNestedTestClass() { + val lineMarkerContributor = PyTestLineMarkerContributor() + val element = getCaretElement(PYTHON_FILE_WITH_NESTED_CLASSES) + if (element != null) { + assertInfoFound(element, lineMarkerContributor) + } + } } \ No newline at end of file